コマンドライン用スクリプトにもClean Architectureを適用する

Lobi事業部サービス基盤チームの長田です。

今回はコマンドラインから実行するスクリプトのはなしです。

オペレーション用のスクリプト

どのプロジェクトにも、Webアプリケーションが実行するわけではなく、かと言って定期実行するわけでもない、 とはいえ管理画面にするまでもない小さなオペレーションがひとつやふたつや100個くらいあると思います。 スクリプトとして書いてしまうのは、たまにしか実行しないからとか、 管理画面としてつくるのがめんどくさい からかとか、そういうものが理由としてあげられるのではないでしょうか。 実際、手っ取り早く要求を満たせるのでスクリプトをホイホイ書きがちです。

課題

管理されていないスクリプトの山

Lobiはそこそこ運用年数が長いこともあって、このようなオペレーション用のスクリプトが 300 以上存在します。 中には(というか大半が)何に使うのかわからない古のスクリプトもありますし、頻繁に使うスクリプトでもテストがなかったりします。

手軽に書けるので、手軽に忘れがちです。 300以上のスクリプトのうち、用途が重複しているものがいくつあることか・・・。

ほんとに動くの?

スクリプトを書いた当時はもちろん動くでしょう。 が、そのスクリプトはいまも動くのでしょうか?

個別にスクリプトファイルを用意していると、コードをそれぞれのファイルに書きがちです。 「大したことをやっていないからスクリプトそのものに書いてしまおう」という気持ちが出てきます。

スクリプト上のコードは非常にテストしづらく、テストが書かれないこともままあります。 テストがない=CIされないコードは、実行できるのかどうかが実行してみないとわからない状態になってしまいます。

Lobiでは「本番環境で動作する全てのコードはコードレビューを通過しなければならない」というルールがあります。

スクリプト類も全てコードレビュー対象なので、危険なコードが本番環境上で実行されることはまずありません。 ただしそれはその時点で問題ないかどうかのレビューであって、今後も問題がないかのレビューではありません。

解決策

Clean Architecture

Lobiでは最近DDDを、ひいてはClean Architectureを実践しようという動きがあって、 コマンドラインから実行するスクリプトにもこれを適用してみてはどうかと考えました。

  • Frameworks & Drivers -> コマンドライン
  • Interface Adapters -> ユースケースを呼び出すためのコード
  • Application Business Rules -> ユースケースを記述したコード
  • Enterprize Business Rules -> DDDにおけるサービスやモデル

スクリプトが実行するべき処理をユースケースとして記述すればテストもしやすいですし、 そこから利用するサービスやモデルをしっかり定義しておけば管理画面化する際に流用が効きます。

スクリプトが重複してしまう問題も、命名の際に他のユースケースを見ながら考えることになるので発生しづらくなります。

なにより、ちょっとめんどくさいので「それなら管理画面を作ったほうがいいな」という気になります。

具体例

具体的なコード例を示します。 全てPerlで書かれています。 説明を簡単にするため、一部コードは省略しています。

  • cli.pl
    • Interface Adaptersにあたる
    • コマンドラインから実行される唯一のスクリプト
    • すべての処理はこのスクリプトから呼び出される
  • Lobi::CLI::Role::Base
    • Interface Adapters共通の設定
  • Lobi::CLI::Foo::DoSomething
    • Interface Adapterにあたる
    • コマンドライン引数を適切な形式に変換してユースケースに渡すだけ
  • Lobi::Usecase::Foo
    • Application Business Rulesにあたる
    • サービスやモデルを呼び出し目的の処理を行う

cli.pl

#!/usr/bin/env perl
use strict;
use warnings;

use Mouse::Util qw{load_class};

my $name = shift @ARGV;
my $class = "Lobi::CLI::${name}";
load_class $class;

$class->new_with_options->run();

Lobi::CLI::Role::Base

package Lobi::CLI::Role::Base;
use strict;
use warnings;

use Mouse::Role;

with "MouseX::Getopt";

has deploy => (
    is      => "ro",
    isa     => "Bool",
    default => 0,
);

requires qw{run};

1;

Lobi::CLI::Foo::DoSomething

package Lobi::CLI::Foo::DoSomething;
use strict;
use warnings;

use Mouse;

use Lobi::Usecase::Foo;
use Lobi::Domain::Model::Time;

with "Lobi::CLI::Role::Base";

has id => (
    is  => "ro",
    isa => "Int",
);

has date => (
    is  => "ro",
    isa => "Str",
);

sub run {
    my ($self) = @_;

    # ユースケースが受け取る形式に変換
    my $date = Lobi::Domain::Model::Time::from_string($self->date);

    return Lobi::Usecase::Foo::do_something(
        id   => $self->id,
        date => $date,
    );
}

1;

Lobi::Usecase::Foo

package Lobi::Usecase::Manager::Premium;
use strict;
use warnings;
use 5.012;

use Data::Validator;

sub do_something {
    state $rule = Data::Validator->new(
        id   => { isa => "Int" },
        date => { isa => "Lobi::Domain::Model::Time" },
    );
    my $args = $rule->validate(@_);

    # 何らかの処理
    ...
}

使い方としてはこんな感じです。

$ perl -Ilib script/op/cli.pl Foo::DoSomething --id=1234 --date='2017-11-24'

ちなみに

むかし書いたこんな記事を発掘しました。

バッチを手動実行するのはどんなとき? バッチ処理を手動で実行するのは、十中八九イレギュラーな状況が発生したときです。 ルーチンワークや実行の条件が決まっているものは何らかの方法で自動化できるはずです。

まったくもってそのとおりですね。

バッチファイルに処理の本体を書かない

はい。

おわり

スクリプトだったものをユースケースとして実装することで、 「課題」で挙げたものが全て解決できるわけではありません。 命名規則がなければ同じようなユースケースが量産されてしまうでしょうし、 そもそもよく使うなら管理画面にしなさいよという話もあります。 それでも「テストを書くこと」「層構造のアーキテクチャを意識すること」を 心構えとしてではなく仕組みとして強制することで、一定の効果が見込めると考えています。

今回の話題は、実はLobiの関連サービスであるLobi Tournamentで実績のある方式を Lobi本体サービス用にアレンジにして取り込んだものです。 小回りの効く若いプロジェクトとある程度の規模で歴史のあるプロジェクトがお互いに学びあって、 良いところを取り込み悪いところを知って事前に避けることができるのは良い環境だと思います。

カヤックではより良いコードを書くために試行錯誤したいエンジニアも募集しています!

Lobi で Android 4.1 をサポート終了した話

こんにちは!

ゲームコミュニティ事業部(Lobi)サービス基盤チームでアプリエンジニアをしているジェイソンです。

僕が開発に携わっている ゲーム攻略チャットSNS、マルチ掲示板 Lobi(ロビー) は、 その前身であるナカマップのリリースが2010年12月15日なので、つい先日運用8年目を迎えたことになります。

現在、ストアに公開されている最新バージョン、Lobi Android v12.0.0では、最低動作環境を以下のように変更しました。

  • (旧)Android 4.1 以降動作
  • (新)Android 4.2 以降動作

本記事では、最低動作環境をなぜ上げることにしたのか、上げることでどのようなメリット・デメリットがあったのかをご紹介していきます。

旧バージョンのOSのサポート方針

Android 4.1~4.1.1(Jelly Bean)は、2012/07/09に公開されてから、すでに5年以上が経過しているバージョンです。

Project Butterによる動作高速化や、Smart App Updateによるアプリの差分アップデートなど、当時はとても興奮したOSバージョンでした。

今でもこのバージョンを使っている方が一定数いるため、可能な限りサポート対象として、ストアからインストールできるようにしてあげたいところです。

しかし、Android 4.1.x をサポートし続けることによって、正確にはサポートするOSバージョンが増えることによって、 どのOSバージョンのOSでもアプリを正常動作させるため、テストや保守コストが増大してしまいます。 また、サポートライブラリでカバーしていない新しいAPIを使えないなどの理由で、バックポートライブラリの導入を検討したり、要件の見直しが必要となるケースが発生してしまいます。

そのためLobiでは、対象OSバージョンのDAUや新規流入がある水準を下回ったら、サポート終了として検討を進める運用をしています。 実際にサポート終了するのは、進行中の施策状況や、リリース計画に影響がでないタイミングとなります。

WebPのフルサポート

Lobi の Android/iOS アプリは、昨年末のバージョンからWebP形式の画像をサーバーから取得するようになりました。 これは過去にもご紹介しています。

Lobiで画像のWebp変換による通信量削減と調査のためにAWS Athenaを利用した話

しかし、Android 4.1.x 環境に限っては、従来通りJpeg形式の画像をサーバーから取得していました…

なぜなら、WebP形式の画像自体は Android 4.0 以降であればサポートしているものの、 当初は非可逆圧縮かつ、透明度が不要な画像でしか使えなかったのでした。 (Android 4.2.1 以降であれば、可逆圧縮・透明度ありなWebP形式をサポートしている)

Supported Media Formats

Android 4.1 サポート終了にともない、上記の分岐が不要となったため、画像取得周りがシンプルになりました。

また、アプリで扱う画像形式が統一されることで、キャッシュ効率の改善も期待できそうです。

Activity#isDestroyed

Activity#onDestroy の実行有無を判定するために、Activiyt#isDestroyed を使いたいことがありました。

しかしこのメソッドは Android 4.2 以降でないと使えず、いままでは以下のような分岐が必要でした。

if (DeviceUtil.hasJellyBeanMR1()) {      
    return !activity.isDestroyed();       
} else {      
    return true;     
}

Android 4.1 サポート終了にともない、以下のようにシンプルに書けるようになりました。

public static boolean checkActivityRunning(Activity activity) {
    if (activity == null) {
        return false;
    } else if (activity.isFinishing()) {
        return false;
    } else if (activity.isDestroyed()) { ← シンプル!
        return false;
    }

    return true;
}

UserAgentの取得

UserAgentを取得したいとき、Android 4.1 以前は以下のようにしなければいけませんでした。

new WebView(context).getSettings().getUserAgentString();

しかし Android 4.2 以降では、UserAgent取得用のメソッドが用意され、以下のようにシンプルに取得できるようになっています。 WebSettings

WebSettings.getDefaultUserAgent(Context);

Android 4.1 のサポート終了によって、不要となった分岐処理をまとめてみましたが、こう見るとそれほど多くはありませんね。

これはたまたま、Lobi Android に影響を与える変更が少なかったということもありますが、 Android Support Library を積極的に使っていることも一因かもしれません。

みんな大好き Android Support library

Android Support Library として、さまざまなライブラリが公開されており、それぞれが後方互換性や追加の機能を提供してくれます。

また、特定のOSで発生するバグなどが修正されている場合もあります。

Lobi Android のように、できる限り幅広いOSバージョンをサポートしたいアプリの場合、Android Support Library は非常に有用です。

Activity であれば ActivityCompat、ImageView であれば ImageViewCompat のように、 Android が提供しているクラスを使うときには *Compat が用意されていないかどうかチェックして、積極的に使っていきましょう。

最低動作環境を上げることによるデメリット

Android 4.1 環境の端末を使い続けている方は、今後Lobiアプリの新規インストールや、バージョンアップができなくなってしまいます。

そのため、対象となる端末でインストールできる最新バージョンで大きな不具合が発生している場合は、リリーススケジュールの調製をするなど、状況に応じた対応が求められます。

また移行期間として、最新バージョンのアプリをインストールできない人向けに、旧バージョンをインストール可能にしてあげるなど、Android 4.1 環境の端末を使い続けている方ができるだけ不快な思いをしないようにしています。

まとめ

最後は Android Support Library の紹介となってしまいましたが、Android 4.1 をサポート終了することによって嬉しいポイントをご紹介しました。 長期間にわたってサービスを継続させるためにも、旧OSバージョンのサポート方針について明確にすることはとても大切ですね。

旧OSバージョンのサポート終了について悩んでいるエンジニアの皆さまの一助になれば幸いです。

カヤックではサービス持続性を大切にしたいエンジニアを募集しています