isyumi_netブログ

isyumi_netがプログラミングのこととかを書くブログ

コマンド化して副作用を追い出そう

多くのプログラムにはこのような外の世界にデータを書き込む処理が登場します。

  • メール送信
  • DBに保存
  • ファイルに書き込み
  • HTTPリクエストにレスポンス

このような処理をここではIOと呼びます。IOが混じるコードのテストは複雑です。多くの場合実機確認することになります。DBに狙った通りにデータが書き込まれたか確認するフレームワークなどありますが、難易度が高いです。また、多くの場合このようなフレームワークは動作が遅いです。手早くシンプルにテストをかけるようにするためにどうすればいいでしょうか。

まず考え方ですが、IOの混じるコードをIO処理の部分とそれ以外の部分に分けて考えます。そして

  • 何を書き込むか計算する処理をテストできるように書く
  • DBやファイルへきちんと書き込めているかは実機確認する

という方針でコードを切り分けていきます。下のコードを見てください。

void main() {
doOperation();
}

void doOperation() {
// なんか複雑な処理
var x = calc(); // なんか複雑な計算
// 複雑な処理
file.put(createData()); // 何かのデータをファイルに保存

db.insert(x);// DBになんか保存
}

calc() {
}

 

 短くてメリットが分かりづらいですが、//なんか複雑な処理 にはすごく複雑な処理が書かれているとしてください。doOperation関数内でファイルへの書き込みとDBへの書き込みが実行されています。この場合、動作確認時にxの値やcreateDataの戻り値を確認するにはDBを見なければいけません。そこで、DBやファイルへの書き込みをコマンド化したオブジェクトを導入し、それを戻り値とします。そして、戻した先で実際のIO処理をします。

void main() {
// 戻り値をコマンドで取得
var commands = doOperation();

// コマンドを一個ずつ実行していく
for (var command in commands) {
if (command is DBInsertCommand) {
db.insert(command.data);
} else if (command is FileWriteCommand) {
file.put(command.data);
}
}
}

// メソッドの中で複雑な計算結果をfileに書き込んだりDBに保存したりする
List<Command> doOperation() {
var commands = [];

// なんか複雑な処理
// 複雑な処理

// 何かのデータをファイルに保存するコマンドを作成
commands.add(new FileWriteCommand()..data = createData());

var x = calc(); // なんか複雑な計算
// DBにInsertするコマンドを作成
commands.add(new DBInsertCommand()..data = calc());

return commands;
}

class Command {}

class DBInsertCommand extends Command {
int data;
}

class FileWriteCommand extends Command {
int data;
}

 

ここではDBInsertCommandとFileWriteCommandをListに入れて戻し、mainメソッド内で実行しています。この一手間によりxやcreateDataの値を簡単にテストできるようになりました。

このようにコマンド化して副作用を追い出していきましょう。

 

補足

command is DBInsertCommand

command is FileWriteCommand

と書いてあるところですがこれはダウンキャストが成功するかどうかでコマンドの方を確認しています。

オプショナル型や代数的データ型がある言語はいい感じに書けると思います。

補足2

一度Listに入れてまとめて返すというのはあまりパフォーマンスが良くないです。その言語のStream型やジェネレータ構文などがないか調べて見ましょう。