1. yonetty

    Posted

    yonetty
Changes in title
+Java8 ラムダ式&Streamによるデザインパターン再考 - Commandパターン -
Changes in tags
Changes in body
Source | HTML | Preview
@@ -0,0 +1,299 @@
+# はじめに
+このシリーズでは、Java8で導入されたラムダ式・Stream APIを利用した、デザインパターンの実装方法を検討していきます。[前回の記事](http://qiita.com/yonetty/items/e99b01470c3416af761c)は`Template Method`パターンを取り上げました。
+
+# 今回のテーマ
+今回は`Command`パターンを取り上げます。
+サンプルプログラムでは、二次元座標上を動く駒(`Peace`)を動かす命令を`Command`パターンで記述し、それをラムダ式バージョンにしてみます。
+
+操作対象の`Piece`クラスはx座標、y座標、方角(`Direction`)をフィールドとして持ち、前後への移動(`moveForward()`、`moveBackward()`)と方角の設定(`setDirection(Direction))`)によって状態を変化させます。
+初期状態は原点(0,0)で北(上)を向いているものとします。
+
+```java:Piece.java
+public class Piece {
+ private Direction direction = Direction.NORTH;
+ private int x = 0;
+ private int y = 0;
+
+ public void moveForward() {
+ this.x += this.direction.x();
+ this.y += this.direction.y();
+ }
+
+ public void moveBackward() {
+ this.x -= this.direction.x();
+ this.y -= this.direction.y();
+ }
+
+ public Direction getDirection() {
+ return direction;
+ }
+
+ public void setDirection(Direction direction) {
+ this.direction = direction;
+ }
+
+ public int getX() {
+ return x;
+ }
+
+ public int getY() {
+ return y;
+ }
+
+ public static enum Direction {
+ NORTH(Math.PI / 2), EAST(0), SOUTH(Math.PI / -2), WEST(Math.PI);
+
+ final private double radian;
+ final private int x;
+ final private int y;
+
+ Direction(double radian) {
+ this.radian = radian;
+ this.x = (int) Math.cos(radian);
+ this.y = (int) Math.sin(radian);
+ }
+
+ public static Direction valueOf(double radian) {
+ return Stream.of(Direction.values())
+ .filter(d -> d.x == (int) Math.cos(radian) && d.y == (int) Math.sin(radian))
+ .findAny()
+ .orElseThrow(IllegalArgumentException::new);
+ }
+
+ public int x() {
+ return this.x;
+ }
+
+ public int y() {
+ return this.y;
+ }
+
+ public double radian() {
+ return this.radian;
+ }
+ }
+}
+```
+
+# 従来の実装方法
+まずは`Command`のインタフェースを定義します。`Piece`オブジェクトを受け取って処理を行う`execute(Piece)`メソッドを持たせます。
+
+```java:PieceCommand.java
+public interface PieceCommand {
+
+ void execute(Piece piece);
+}
+```
+
+具体的な`Command`実装として、指定したマス数前後に移動する命令(`Forward`、`Backward`)と左右への方向転換(`TurnRight`、`TurnLeft`)を用意します。
+なお、左右への方向展開は±90度(`π/2`ラジアン)の回転として、現在の方角から新たな方角を求めています。
+
+```java:Forward.java
+
+public class Forward implements PieceCommand {
+ private final int step;
+
+ public Forward(int step) {
+ this.step = step;
+ }
+
+ @Override
+ public void execute(Piece piece) {
+ for (int i = 0; i < step; i++) {
+ piece.moveForward();
+ }
+ }
+}
+```
+
+```java:TurnLeft.java
+public class TurnLeft implements PieceCommand {
+
+ @Override
+ public void execute(Piece piece) {
+ double radian = piece.getDirection().radian() + Math.PI / 2;
+ piece.setDirection(Piece.Direction.valueOf(radian));
+ }
+
+}
+```
+
+これらの命令を使って`Piece`を移動させるコード例は以下のようになります。
+
+```java:Usage
+ PieceCommand cmd1 = new Forward(5);
+ cmd1.execute(piece);
+
+ assertEquals(Piece.Direction.NORTH, piece.getDirection());
+ assertEquals(0, piece.getX());
+ assertEquals(5, piece.getY());
+
+ PieceCommand cmd2 = new TurnRight();
+ cmd2.execute(piece);
+
+ assertEquals(Piece.Direction.EAST, piece.getDirection());
+ assertEquals(0, piece.getX());
+ assertEquals(5, piece.getY());
+
+ PieceCommand cmd3 = new Backward(3);
+ cmd3.execute(piece);
+
+ assertEquals(Piece.Direction.EAST, piece.getDirection());
+ assertEquals(-3, piece.getX());
+ assertEquals(5, piece.getY());
+
+ PieceCommand cmd4 = new TurnLeft();
+ cmd4.execute(piece);
+
+ assertEquals(Piece.Direction.NORTH, piece.getDirection());
+ assertEquals(-3, piece.getX());
+ assertEquals(5, piece.getY());
+```
+
+# ラムダ式を使った実装
+それでは次にラムダ式を使って`Command`パターンを実装してみます。
+まず、各々の命令は`Piece`オブジェクトを受け取って、前後移動や方向転換の操作を行いますから、Java8標準で提供される関数型インターフェース`Consumer<T>`によって実現しましょう。
+前節の`PieceCommand`は`Consumer<Piece>`型に置き換わることとなります。
+そして、各命令を表す関数を取得するための`Factory`を用意することにします。
+
+前節の`Forward.java`の実装は敢えて昔ながらのfor構文を用いていましたが、これも`Stream API`を使用した書き方に直しています。
+
+```java:PieceCommandFactory.java
+public class PieceCommandFactory {
+
+ static Consumer<Piece> ofForward(final int step) {
+ return p -> {
+ IntStream.range(0, step)
+ .forEach(i -> p.moveForward());
+ };
+ }
+
+ static Consumer<Piece> ofBackward(final int step) {
+ return p -> {
+ IntStream.range(0, step)
+ .forEach(i -> p.moveBackward());
+ };
+ }
+
+ static Consumer<Piece> ofTurnRight() {
+ return p -> {
+ double radian = p.getDirection().radian() - Math.PI / 2;
+ p.setDirection(Piece.Direction.valueOf(radian));
+ };
+ }
+
+ static Consumer<Piece> ofTurnLeft() {
+ return p -> {
+ double radian = p.getDirection().radian() + Math.PI / 2;
+ p.setDirection(Piece.Direction.valueOf(radian));
+ };
+ }
+
+}
+```
+
+ラムダ式バージョンを用いたコード例は以下のとおりです。
+
+```java:Usage
+ Consumer<Piece> cmd1 = PieceCommandFactory.ofForward(5);
+ cmd1.accept(piece);
+
+ assertEquals(Piece.Direction.NORTH, piece.getDirection());
+ assertEquals(0, piece.getX());
+ assertEquals(5, piece.getY());
+
+ Consumer<Piece> cmd2 = PieceCommandFactory.ofTurnRight();
+ cmd2.accept(piece);
+
+ assertEquals(Piece.Direction.EAST, piece.getDirection());
+ assertEquals(0, piece.getX());
+ assertEquals(5, piece.getY());
+
+ Consumer<Piece> cmd3 = PieceCommandFactory.ofBackward(3);
+ cmd3.accept(piece);
+
+ assertEquals(Piece.Direction.EAST, piece.getDirection());
+ assertEquals(-3, piece.getX());
+ assertEquals(5, piece.getY());
+
+ Consumer<Piece> cmd4 = PieceCommandFactory.ofTurnLeft();
+ cmd4.accept(piece);
+
+ assertEquals(Piece.Direction.NORTH, piece.getDirection());
+ assertEquals(-3, piece.getX());
+ assertEquals(5, piece.getY());
+```
+
+# 関数の合成
+ラムダ式バージョンのメリットとして、関数の合成が可能となります。
+具体的は`Consumer<T>`に定義されたdefaultメソッド`andThen(Consumer<? super T>)`を利用します。
+
+```java:UsegeOfAndThen
+ Consumer<Piece> composite = PieceCommandFactory.ofForward(5)
+ .andThen(PieceCommandFactory.ofTurnRight())
+ .andThen(PieceCommandFactory.ofBackward(3))
+ .andThen(PieceCommandFactory.ofTurnLeft());
+ composite.accept(piece);
+```
+
+合成した結果は同じ`Consumer<Piece>`型の関数ですから、`PieceCommandFactory`のstaticメソッドとして定義したものと同様、`Piece`を操作するコマンド(命令)として扱えます。
+以下のように合成によって新たな関数を定義して利用することができます。
+
+```java:Composite
+ //「右向け右」、を繰り返すと「回れ右」
+ Consumer<Piece> reverse = PieceCommandFactory.ofTurnRight()
+ .andThen(PieceCommandFactory.ofTurnRight());
+```
+
+ところで関数の合成はどのように実装されているのでしょうか?
+`Consumer<T>`のソースを覗いてみると以下のような実装となっています。
+
+```java:Consumer.java
+ default Consumer<T> andThen(Consumer<? super T> after) {
+ Objects.requireNonNull(after);
+ return (T t) -> { accept(t); after.accept(t); };
+ }
+```
+
+かなりシンプルですね。事前条件チェックを除けば1行です。
+`andThen`が属している`Consumer<T>`インスタンス自身の`accept`をコールした後に、引数で渡された`Consumer<T>`の`accept`を呼び出しています。
+複数の`andThen`を連結していった場合に、どのようにラムダ式が構成され、実行されるのかは一度頭の中でシミュレーションしてみるとよいと思います。
+
+# Reduce
+`andThen`を順々に連結していく代わりに、可変配列で受け取って合成された`Consumer<Piece>`を返却するユーティリティメソッドを以下のように実装できます。
+
+```java:PieceCommandFactory#chain
+ @SafeVarargs
+ static Consumer<Piece> chain(Consumer<Piece>... commands) {
+ return Stream.of(commands)
+ .reduce((c1, c2) -> c1.andThen(c2))
+ .orElse(p -> {
+ });
+ }
+```
+
+2つの関数を順次`andThen`で合成していくことで、全体としてひとつの関数にまとめられます。
+最後に`orElse`で何もしないラムダ式を指定しているので、仮に引数`commands`が`null`であったとしても安全に動作します。長さ1の場合も大丈夫です。
+
+```java:SafeChain
+ Consumer<Piece> composite = PieceCommandFactory.chain(PieceCommandFactory.ofForward(5));
+ composite.accept(piece);
+
+ assertEquals(Piece.Direction.NORTH, piece.getDirection());
+ assertEquals(0, piece.getX());
+ assertEquals(5, piece.getY());
+
+ Consumer<Piece> empty = PieceCommandFactory.chain();
+ empty.accept(piece);
+
+ assertEquals(Piece.Direction.NORTH, piece.getDirection());
+ assertEquals(0, piece.getX());
+ assertEquals(5, piece.getY());
+```
+
+# まとめ
+`Command`パターンの、複数の`Command`を逐次適用していくことで処理を進めていくという性質は関数型プログラミングにおける**関数の合成**という概念と相性がよさそうです。
+次は`Chain of Responsibility`(`責任の連鎖`)パターンを取り上げみようと考えています。
+
+
+