Help us understand the problem. What is going on with this article?

Java8 ラムダ式&Streamによるデザインパターン再考 - Commandパターン -

More than 3 years have passed since last update.

はじめに

このシリーズでは、Java8で導入されたラムダ式・Stream APIを利用した、デザインパターンの実装方法を検討していきます。前回の記事Template Methodパターンを取り上げました。

今回のテーマ

今回はCommandパターンを取り上げます。
サンプルプログラムでは、二次元座標上を動く駒(Peace)を動かす命令をCommandパターンで記述し、それをラムダ式バージョンにしてみます。

操作対象のPieceクラスはx座標、y座標、方角(Direction)をフィールドとして持ち、前後への移動(moveForward()moveBackward())と方角の設定(setDirection(Direction)))によって状態を変化させます。
初期状態は原点(0,0)で北(上)を向いているものとします。

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)メソッドを持たせます。

PieceCommand.java
public interface PieceCommand {

    void execute(Piece piece);
}

具体的なCommand実装として、指定したマス数前後に移動する命令(ForwardBackward)と左右への方向転換(TurnRightTurnLeft)を用意します。
なお、左右への方向展開は±90度(π/2ラジアン)の回転として、現在の方角から新たな方角を求めています。

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();
        }
    }
}
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を移動させるコード例は以下のようになります。

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>によって実現しましょう。
前節のPieceCommandConsumer<Piece>型に置き換わることとなります。
そして、各命令を表す関数を取得するためのFactoryを用意することにします。

前節のForward.javaの実装は敢えて昔ながらのfor構文を用いていましたが、これもStream APIを使用した書き方に直しています。

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));
        };
    }

}

ラムダ式バージョンを用いたコード例は以下のとおりです。

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>)を利用します。

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を操作するコマンド(命令)として扱えます。
以下のように合成によって新たな関数を定義して利用することができます。

Composite
        //「右向け右」、を繰り返すと「回れ右」
        Consumer<Piece> reverse = PieceCommandFactory.ofTurnRight()
                .andThen(PieceCommandFactory.ofTurnRight());

ところで関数の合成はどのように実装されているのでしょうか?
Consumer<T>のソースを覗いてみると以下のような実装となっています。

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>を返却するユーティリティメソッドを以下のように実装できます。

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で何もしないラムダ式を指定しているので、仮に引数commandsnullであったとしても安全に動作します。長さ1の場合も大丈夫です。

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(責任の連鎖)パターンを取り上げみようと考えています。

yonetty
某SIerでアーキテクトとしてエンタープライズ向けシステム・製品の開発に携わっています。 Twitter: @tyonekubo
https://blog.ynkb.xyz/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした