読者です 読者をやめる 読者になる 読者になる

かとじゅんの技術日誌

技術の話をするところ

副作用を最小限に抑えるために必要なこと

若干、難しい話ですが、設計力を上げたいプログラマ向けなエントリ。私も道半ばです。一緒に考えていただければ幸いです。
堅牢なアプリケーションを実現する上で「副作用を最小限に抑える」という設計思想は、非常に重要な示唆を含んでいます。
その「副作用」とは、そもそもどんな定義なのでしょうか。
DDDでは"side effect"と定義していて、以下のように解説されています。

Domain-Driven Design: Tackling Complexity in the Heart of Software

Domain-Driven Design: Tackling Complexity in the Heart of Software


P250より引用

In standard English, the term side effect implies an unintended consequence, but in computer science, it means any effect on the state of the system.
For our purposes, let's narrow that meaning to any change in the state of the system that will affect future operations.

英語では、side effectという言葉は"意図的ではない結果"のことを示すが、コンピュータサイエンスでは、システムの状態へのあらゆる影響を意味する。
我々の目的においては「将来の操作に影響を及ぼすであろう、システムの状態に関するあらゆる変更」と定義する。

コマンドクエリ分離原則(CQS)は副作用を抑える

その副作用を最小限に抑えるという設計思想は、古くはBertrand Meyer氏が考案したコマンドクエリ分離原則(Command-Query Separation:CQS)に見ることができます。
CQSでは以下のような原則が示されています。

CQS: 「あらゆるメソッドは、アクションを実行するコマンドか、呼び出し元にデータを返すクエリかのいずれかであって、両方を行ってはならない。これは、質問をすることで回答を変化させてはならないということだ。」

例えば、「質問をすることで回答を変化」するようなことをSQLで考えるならば、SELECTしたらUPDATEされるようなものでしょうか。もちろん、こんなSQL文は実在しません。実在したら怖くて使えないはずですが。。

SELECT * FROM EMP -- SELECTするとUPDATEも起きるとしたら...

もちろん、SQLはこの原則に反しませんが、自分たちで作成するプログラムでは、注意しないと「命令」と「問合せ」の混同が起き、思わぬ「副作用」が発生するかもしれません。その悪影響は計り知れないため、「状態」の操作は細心の注意を払う必要があります。

状態と副作用

上記で紹介した「副作用」の文脈「将来の操作に影響を及ぼすであろう、システムの状態に関するあらゆる変更」には「状態」が含まれます。
そもそも「状態」はどういう目的で導入されたのか、「実装パターン」から知ることができます。

実装パターン

実装パターン


実装パターン P56から引用

コンピュータ処理の先駆者たちは、プログラミングのためのメタファーに、時間とともに変化する「状態」という考え方を取り入れました。人間の脳には、この「状態」を扱うための、先天的または後天的な、幅広いさまざまな戦略が備わっている。

そもそも、人間が「状態」を扱うメンタルモデルを持っているので、それをプログラミング言語にも取り入れることで、扱いやすくしたという自然な流れです。オブジェクト指向言語でも「状態」のカプセル化という設計手法は常套手段になっています。

しかし状態は、プログラマにとって問題にもなる。状態に対してなんらかの前提を抱いた途端、コードはリスクにさらされている。
間違った思い込みをすることもあるだろうし、あるいは状態のほうが変化するかもしれない。自動リファクタリングのような、望ましいプログラミングツールの多くは、状態という考えがなければ、今よりも容易に構築することができる。そして、並行処理と状態は相性が悪い。状態がなければ、並行処理プログラムにおける問題の多くは消滅する。

ここで言われているように「状態」を扱うことはいいことばかりではなく、このような問題も孕んでいます。複雑な「状態」に関連したコードを変更することは、プログラマを神経衰弱にさせます。(だからこそテストが重要) また、並行処理でスレッドセーフを考える場合も「状態」に関連する処理に細心の注意を払う必要があります。これが「副作用」の側面です。つまり、状態は扱い方を間違えると「薬にも毒にもなる」といえます。だからこそ、「副作用」が伴う命令と、「副作用」がない問合せの二つに分離して、その影響を最小化することが求められるわけです。

(純粋)関数型言語は副作用を扱いません。状態は変化せず不変だからです。しかし、Kent Beck氏は、それを人間の脳、広義の意味でメンタルモデルとの親和性を考えると魅力的ではないと語っています。

関数型のプログラミング言語では、変化する状態をまったく使用しない。こうした関数型言語で、多くの人に使われているものは今まで1つもない。我々の脳は、変化する状態に対応するように構造化され、条件付けられているので、状態は有用なメタファーだと私は思う。単一代入あるいは無変数のプログラミングでは、有効な思考戦略の多くが使えなくなるので、魅力的な選択肢とはならない.

蛇足ですが、これはおそらくHaskellなどの純粋関数型言語に対する評価だと思われますが、Scalaは「変化する状態をまったく使用しない」言語ではなく、原則的に不変としながらも、場合によっては可変を扱える言語です。そういう意味ではこの要求にも応えることができるのがScalaと言えるでしょう。
ここから学べる教訓としては、人間のメンタルモデルを無視するような「副作用を完全に排除する」ではなく、「必要なところでは副作用を起こすが、それ以外では極力副作用を抑える」設計戦略が求められるのではないでしょうか。つまり、「副作用を最小限に抑える」設計戦略が求めれます。

DDDのSIDE-EFFECT-FREE FUNCTIONSパターンに通じる

また、命令と問合せを分離する考えは、DDDでもSIDE-EFFECT-FREE FUNCTIONSというパターンで示されています。
DDDにおいても、プログラム上の操作は大雑把に命令と問合せ二つのカテゴリに分けられ、問合せはシステムの情報を取得することで、命令はシステムのあらゆる変更を及ぼす操作だと言及されています。このパターンは、CQSの原則に反しません。
どのように解決するかはP251の以下の下り。

Place as much of the logic of the program as possible into functions,operations that return results with no observable side effects.
Strictly segregate commands (methods that result in modifications to observable state) into very simple operations that do not return domain information.
Further control side effects by moving complex logic into VALUE OBJECTS when a concept fitting the responsibility presents itself.

JavaEE勉強会の訳がありました。

ロジックは、可能な限り(副作用なく戻り値を返すような)関数の中に置く。
命令は単純な操作として厳密に分離し、ドメイン情報は返さないようにする。
副作用をより厳密にコントロールする場合は、複雑なロジックをValueObjectに移動する。ValueObjectの概念に適合する場合のみ。
副作用のない関数は(特に不変なValueObjectにおいて)、操作を安全に結合できるようにする。

佐藤さんの分かりやすい説明も引用します。

● Side-Effect-Free Functions(副作用の無い関数)パターン
様々な副作用のあるメソッドを組み合わせてしまうと、どこでどんな影響が発生するか予測できなくなるため、プログラムの振る舞いを理解することが極端に難しくなってしまう。そのため、単に引数を加工して結果を返すだけのロジック部分はできる限り副作用の無い関数としてプログラム化し、実際に状態を変化させる操作(コマンド)を最小限に留める。副作用の無い関数同士ならば、組み合わせて使っても何が起こるか予測できなくなることはない。副作用の無い関数はまた、テストも容易である。値オブジェクトは状態が変わらないので、値オブジェクトがもつ振る舞いには基本的に副作用が無い。1メソッドだけでは表現できない複雑なロジックの場合は、値オブジェクトにそのロジックをもたせることで、そこに副作用が無いこと示すこともできる。

Javaのコードでどういうことか みてみましょう。
例えば、以下のmixInメソッドでは、絵の具(Paintクラス)と絵の具を混ぜあわせます。絵の具の量(volume)と、絵の具の色である顔料(red,yellow,blue)を更新します。つまり、副作用が伴うわけです。

// 絵の具を表すオブジェクト(副作用が伴う可変オブジェクト)
public class Paint {
    private int volume; // 絵の具の量を表す
    private int red;    // red, yellow, blueは絵の具の顔料を表す
    private int yellow;
    private int blue;

    public void mixIn(Paint other) {
        volume = volume + other.volume;
        double raito = other.volume / volume;
        // 以下、顔料の更新処理
        red = (int)((red + other.red) / ratio);       
        yellow = (int)((yellow + other.yellow) / ratio); 
        blue = (int)((blue + other.blue) / ratio); 
    }
}

そこで、副作用である顔料の更新処理をSIDE-EFFECT-FREE FUNCTIONSパターンを適用して、できるだけ副作用を抑えたコードにしてみます。
PaintのmixInではPigmentColorクラスが持つmixedWithメソッドを使って、顔料を混ぜています。PigmentColorクラスはバリューオブジェクトです。バリューオブジェクトは原則的に不変オブジェクトで、このPigmentColorも不変オブジェクトです。

// 絵の具を表すオブジェクト(副作用が伴う可変オブジェクト)
public class Paint {
    private int volume;
    private PigmentColor pigmentColor;
    // getter, setter 省略

    public Paint(PigmentColor pigmentColor, int volume) {
        this.volume = volume;
        this.pigmentColor = pigmentColor;
    }

    // 副作用を起こす命令として実装
    public void mixIn(Paint other) {
        volume = volume + other.volume;
        double raito = other.volume / volume;
        // 顔料の更新処理を関数化して副作用を起こさないにし、関数で得た新しいpigmentColorを入れ替える処理をmixInメソッドで行う。
        pigmentColor = pigmentColor.mixedWith(other.pigmentColor, ratio);
    }

}
// 顔料を表すバリューオブジェクト(副作用が伴わない不変オブジェクト)
public final class PigmentColor {
    private final int red;
    private final int yellow;
    private final int blue;
    // getter省略

    public PigmentColor(int red, int yellow, int blue) {
        this.red = red;
        this.yellow = yellow;
        this.blue = blue;
    }

    // 顔料を混ぜあわせて新たな顔料を生成して返す。副作用は起こさない関数として実装する。
    public PigmentColor mixedWith(PigmentColor other, double ratio) {
        int red = (int)((red + other.red) / ratio);
        int yellow = (int)((yellow + other.yellow) / ratio);
        int blue = (int)((blue + other.blue) / ratio);
        return new PigmentColor(red, yellow, blue);
    }
}

Paintクラスで直接顔料を更新するのではなく、その処理は顔料というバリューオブジェクトに移動しています。前述した通り、バリューオブジェクトは不変です。顔料の更新結果はthisには反映できないため、新たなPigmentColorオブジェクトを生成して返します。この処理は副作用が伴わない関数として実装します。関数は、引数であるpigmentColorやraitoの入力条件を受け取り、更新後のPigmentColorを出力として返しています。つまり、関数は”問合せ”です。
そして、PaintクラスのmixInメソッド内で、その顔料の更新結果をPaintのpigmentColorに反映する。この処理は副作用が伴う命令として実装します。

pigmentColor = pigmentColor.mixedWith(other.pigmentColor, ratio);

つまり、副作用が伴うロジックを、副作用が伴わない(問合せとしての)関数と、副作用を伴う命令を分けて用いることで、副作用を安全に局所化することができるわけです。
そして、以下に示すような導入の効果があります。

新たに導入されたPigment Colorは「副作用のない関数」を提供し、その結果は理解しやすく、テストしやすく、安全に使うことができ、他の操作と組み合わせやすいようになっている。
色の合成に関する複雑なロジックは完全にカプセル化されており、開発者がこのクラスを使う際には実装を理解する必要もない。

これは非常に実益のある設計パターンではないかと考えます。特に複雑なアプリケーションで、効力を発揮することは言うまでもありません。このパターンは言語を問いませんが、Scalaとの相性も抜群でしょう。

CQRSとは

DDDのコミュニティで、Greg Young氏が考案したコマンドクエリ責務分離(Command and Query Responsibility Segregation)というアーキテクチャパターンが話題になったことがあります。
これは文字通り、前述したCQSをアーキテクチャ全体に適用できるように発展させたパターンです。DDDをベースにしているため、DDDとは親和性が高いです。

id:digitalsoulさんのブログの翻訳が分かりやすいです。
Greg Young流CQRS - Mark Nijhof - Digital Romanticism

CQRSは、アーキテクチャレベルで副作用をうまく扱うための方法論のひとつと言えるでしょう。

これに対して、Gregは同様の原則を用いつつ、それをシステムのアーキテクチャ全体に適用し、システムの更新処理(Command)を参照処理(Query)から明確に分離したのです。このうち更新処理は、私たちがすでに「ドメイン」として知っているもので、システムの価値を生むすべてのふるまいを含んでいます。参照処理は特定のレポートを行う必要性に特化したものです。例えば、ユーザがドメインのふるまいを実行できるようにするアプリケーションの画面を考えてみて下さい。このような伝統的なレポートは、データベースの参照によって実現されてきました。

CQRSのフレームワーク Axon Framework

CQRSの具体例を学ぼうと思ったので、いろいろ調べていたら以下のJavaフレームワークを見つけました。
axonframework - Project Hosting on Google Code
中身はまだよく調べていませんが、このダイアグラムを見るとイメージがつかめるかもしれません。
http://code.google.com/p/axonframework/wiki/ArchitectureOverview

上段が命令で、下段が問合せで、上段の中央がドメイン層ですかね。この絵だけみるとコマンドが関連するところだけにドメイン層があって、問合せにはドメイン層は介在しないような絵になっていますが、サンプルコードをみた限りでは問合せにも薄いドメイン層があるのかな、という印象。

いずれにせよ、どんな言語、どんなアプリケーションにおいても、「副作用」とうまく付き合う必要があると思います。一度考えてみるとよいかもしれません。

あわせて読みたい
コマンドとクエリ分離原則 - Strategic Choice
求めるな、命じよ - Strategic Choice