コマンドライン用スクリプトにも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本体サービス用にアレンジにして取り込んだものです。 小回りの効く若いプロジェクトとある程度の規模で歴史のあるプロジェクトがお互いに学びあって、 良いところを取り込み悪いところを知って事前に避けることができるのは良い環境だと思います。

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