はじめに
このシリーズでは、Java8で導入されたラムダ式・Stream APIを利用した、デザインパターンの実装方法を検討していきます。前回の記事はTemplate Method
パターンを取り上げました。
今回のテーマ
今回はCommand
パターンを取り上げます。
サンプルプログラムでは、二次元座標上を動く駒(Peace
)を動かす命令をCommand
パターンで記述し、それをラムダ式バージョンにしてみます。
操作対象のPiece
クラスはx座標、y座標、方角(Direction
)をフィールドとして持ち、前後への移動(moveForward()
、moveBackward()
)と方角の設定(setDirection(Direction))
)によって状態を変化させます。
初期状態は原点(0,0)で北(上)を向いているものとします。
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)
メソッドを持たせます。
public interface PieceCommand {
void execute(Piece piece);
}
具体的なCommand
実装として、指定したマス数前後に移動する命令(Forward
、Backward
)と左右への方向転換(TurnRight
、TurnLeft
)を用意します。
なお、左右への方向展開は±90度(π/2
ラジアン)の回転として、現在の方角から新たな方角を求めています。
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();
}
}
}
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
を移動させるコード例は以下のようになります。
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
を使用した書き方に直しています。
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));
};
}
}
ラムダ式バージョンを用いたコード例は以下のとおりです。
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>)
を利用します。
Consumer<Piece> composite = PieceCommandFactory.ofForward(5)
.andThen(PieceCommandFactory.ofTurnRight())
.andThen(PieceCommandFactory.ofBackward(3))
.andThen(PieceCommandFactory.ofTurnLeft());
composite.accept(piece);
合成した結果は同じConsumer<Piece>
型の関数ですから、PieceCommandFactory
のstaticメソッドとして定義したものと同様、Piece
を操作するコマンド(命令)として扱えます。
以下のように合成によって新たな関数を定義して利用することができます。
//「右向け右」、を繰り返すと「回れ右」
Consumer<Piece> reverse = PieceCommandFactory.ofTurnRight()
.andThen(PieceCommandFactory.ofTurnRight());
ところで関数の合成はどのように実装されているのでしょうか?
Consumer<T>
のソースを覗いてみると以下のような実装となっています。
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>
を返却するユーティリティメソッドを以下のように実装できます。
@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の場合も大丈夫です。
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
(責任の連鎖
)パターンを取り上げみようと考えています。