スマートフォン・ジン | Smartphone-Zine

引っ越し先→ https://smartphone-zine.com/

Android入門―本気で使える電卓アプリの開発―3

電卓の基礎ロジックの作成

最初に電卓の基礎部分を作ります。 単にA+B結果を表示するだけなのですが、 0から9の数値ボタン、四則演算ボタン、イコールのボタンとそれぞれ動作を割り当てなければなりません。 さらに今Aを入力中なのかBを入力中なのか、演算結果を表示中なのかを判定して各ボタンの動作を決めていかないといけないのです。簡単そうに思えて以外に複雑なんですね。 通常のプログラミングなら、このようなアプリケーションは状態をフラグで管理してifやcaseによる分岐を延々とコーディングしていくことになります。状態や動作が増えるにつれフラグは増え分岐は巨大となり収拾がつかなくなってしまいます。機能追加ともなれば全てのifやcase文に手を加える作業が待っています。これは大変です。 今回はデザインパターンを使用してifやcaseによる条件分岐を排除し、見通しのよい構成にしてみたいと思います。

GoFデザインパターン

GoFデザインパターンとはオブジェクト指向言語においてよく使われる設計の「パターン」であり「ノウハウ」です。プログラム設計の定石なのです。 今回、電卓の基礎部分では、SingletonパターンとStateパターンを使用します。パターンを使用することで、再利用しやすく、機能拡張しやすい構成になります。

Stateパターン

これは状態に対して動作を規定する場合に最適なデザインパターンです。Stateパターンでは状態をクラスで表します。状態が遷移している様子はクラスを差し替えて表現します。

Singletonパターン

これはそのクラスのインスタンスが1つしか生成されないことを保証するためのクラス設計です。 状態を表すクラスは1つだけで良いのです。もし状態が変わるたびにクラスを作成してしまったなら無駄なメモリを使ってしまいますので、Singletonパターンを用いて、状態を表すクラスはたった1つだけ生成されることを保証しましょう。

オートマトン理論と状態遷移表

遷移するルールと状態が遷移したときに何をするのかを定義しているものを有限オートマトンと呼びます。 それを図で表現したものが状態遷移図です。同じく表で表現したものを状態遷移表と呼びます。

電卓のオートマトンと状態遷移表

電卓には色々なボタンがありますね。これらは外部からの入力(イベント)を受け付るものであると考えます。イベントには次のようなものが考えられます。

  • イベント1)0~9のボタンが押される
  • イベント2)四則演算のボタン(×÷+-)が押される
  • イベント3)=のボタンが押される

そのほか、%のボタン、クリアボタンなどがありますね。これらもすべてイベントです。 電卓はこれらイベントの発生によって次々に状態が変化するわけです。 次に電卓の状態を考えましょう。電卓の場合、次の状態(State)が考えられます。

  • 状態1)数値Aの入力中
  • 状態2)四則演算の選択(×÷+-)
  • 状態3)数値Bの入力中
  • 状態4)演算した結果を表示中

次に電卓の動作です。電卓の基本動作になります。

  • 動作1)数値Aを保持する
  • 動作2)数値Bを保持する
  • 動作3)押された四則演算のボタンを保持する
  • 動作4)「数値A (× or ÷ or + or -) 数値B」を計算して表示する

このような状態と動作の紐付け、そして状態の遷移先を表にまとめると次のようになります。

図:状態遷移表
電卓 状態遷移表 状態 State
A:数値入力状態A NumberAState C:演算モード OperationState B:数値入力状態B NumberBState R:結果表示状態 ResultState
イベント event 0,~,9 入力をディスプレイに反映 →A ディスプレイクリア 入力をディスプレイに反映 →B 入力をディスプレイに反映 →B ディスプレイクリア 入力をディスプレイに反映 →A
四則演算 ÷×-+ ディスプレイを変数Aへ 演算子記憶 →C 演算子記憶 →C ディスプレイを変数Bへ反映 「変数A 演算子 変数B」をディスプレイ表示 ディスプレイ表示(演算結果)を変数Aへ 演算子記憶 →C 「変数A 演算子 変数B」をディスプレイ表示 ディスプレイ表示(演算結果)を変数Aへ 算子記憶 →C
入力中の値を表示 →R ÷,×の場合、「変数A 演算子 変数A」をディスプレイ表示 +,-の場合、変数Aをディスプレイ表示 →R 「変数A 演算子 変数B」をディスプレイ表示 →R なにもしない →R
Clear 変数Aクリア ディスプレイクリア →A 変数Aクリア ディスプレイクリア →A 変数Bクリア ディスプレイクリア →B 変数Aクリア 変数Bクリア ディスプレイクリア →A
AllClear 変数Aクリア 変数Bクリア ディスプレイクリア →A 変数Aクリア 変数Bクリア ディスプレイクリア →A 変数Aクリア 変数Bクリア ディスプレイクリア →A 変数Aクリア 変数Bクリア ディスプレイクリア →A

状態遷移表が出来たならまあほとんど完成したも同然ですね。あとは各状態を、Stateインタフェースを実装したクラスで表現していきます。 クラス内にはその状態の場合にイベントが発生した場合の動作を記載しておきます。 状態遷移表の各マスに記された動作の実装はContextに定義し、必要に応じてStateからContextに委譲します。 そして電卓クラスはContextを実装します。電卓には状態を保持しますので、Stateクラスを持ちます。 ではソースを見ていきましょう。 全ソースのダウンロードはこちらです。 まずStateインタフェースです。

[java]
public interface State {
    /**
     * 数値ボタン
     *
     * @param context
     * @param num
     *            数値ボタン
     */
    public abstract void onInputNumber(Context context, Number num);
    /**
     * 四則演算ボタン
     *
     * @param context
     * @param op
     *            演算子
     */
    public abstract void onInputOperation(Context context,
            Operation op);
    /**
     * =ボタン
     *
     * @param context
     */
    public abstract void onInputEquale(Context context);
    /**
     * クリアボタン
     *
     * @param context
     */
    public abstract void onInputClear(Context context);
    /**
     * オールクリアボタン
     *
     * @param context
     */
    public abstract void onInputAllClear(Context context);
}
[/java]

Stateでは、電卓に対するイベントを処理します。イベントは「数値ボタン」「四則演算ボタン」「=ボタン」「クリアボタン」「オールクリアボタン」の5つを規定しています。 ではこれを実装する数値入力状態A(NumberAState )の実装をみてみましょう。

[java]
public class NumberAState implements State {
    private static NumberAState singleton = new NumberAState();
    private NumberAState() { // コンストラクタはprivate
    }
    public static State getInstance() { // 唯一のインスタンスを得る
        return singleton;
    }
    @Override
    public void onInputNumber(Context context, Number num) {
        context.addDisplayNumber(num);
    }
    @Override
    public void onInputOperation(Context context, Operation op) {
        context.saveDisplayNumberToA();
        context.setOp(op);
        context.changeState(OperationState.getInstance());// 次の状態
    }
    @Override
    public void onInputEquale(Context context) {
        context.saveDisplayNumberToA();
        context.showDisplay(context.getA());
        context.changeState(ResultState.getInstance());
    }
    @Override
    public void onInputClear(Context context) {
        context.clearA();
        context.clearDisplay();
    }
    @Override
    public void onInputAllClear(Context context) {
        context.clearA();
        context.clearB();
        context.clearDisplay();
    }
}
[/java]

最初の部分である、

[java]
private static NumberAState singleton = new NumberAState();
private NumberAState() { // コンストラクタはprivate
[/java]

が、Singletonパターンになります。よく見るとコンストラクタがprivateになっています。こうすることでクラスのインスタンスを新たにNewできなくなります。じゃあどうやって利用するのかというと、唯一のインスタンスを取得するためにgetInstanceメソッドを呼びます。

[java]
public static State getInstance() { // 唯一のインスタンスを得る
return singleton;
}
[/java]

この単純な仕組みにより、クラスのインスタンスが唯一つであることを保証しているわけです。 では、電卓のイベントの実装です。

[java]
public void onInputNumber(Context context, Number num) {
context.addDisplayNumber(num);
}
[/java]

onInputNumberメソッドでは数値ボタンが押されたときのイベントを処理しています。contextのaddDisplayNumberメソッドを呼び出して、押された数字をディスプレイに追加します。 数値ボタンが押された場合、次の状態も「数値入力状態A(NumberAState )」ですので状態遷移はありません。 次は四則演算のボタンが押された場合の処理です。状態遷移表には ディスプレイを変数Aへ 演算子記憶 →C と記載されていますのでその通り実装していきます。

[java]
public void onInputOperation(Context context, Operation op) {
    context.saveDisplayNumberToA();     // ディスプレイを変数Aへ
    context.setOp(op);                  // 押された演算子を保持
    context.changeState(OperationState.getInstance());// 次の状態へ遷移
}
[/java]

context.changeState(OperationState.getInstance())で、次の状態に遷移しています。四則演算のボタンが押された場合は、状態C:「演算モード(OperationState)」に遷移します。 その他の状態も実装してきます。 Stateを実装するクラスの各イベントメソッドでは、contextのメソッドを状態にあわせて呼び出しています。このcontextは電卓の基本機能そのものです。

[java]
public interface Context {
    // 状態遷移
    public abstract void changeState(State state);
    // 演算実行し結果をディスプレイに表示します。
    public abstract double doOperation();
    // ディスプレイ表示を更新します。
    void showDisplay();
    // ディスプレイ表示を引数の値で更新します。
    public abstract void showDisplay(double d);
    // ディスプレイ表示に数値を追加します。
    public abstract void addDisplayNumber(Number num);
    // ディスプレイ表示を変数Aへ保存します。
    public abstract void saveDisplayNumberToA();
    // ディスプレイ表示を変数Bへ保存します。
    public abstract void saveDisplayNumberToB();
    // 変数Aをクリアします。
    public abstract void clearA();
    // 変数Bをクリアします。
    public abstract void clearB();
    // 演算子を取得します。
    public abstract Operation getOp();
    // 演算子を設定します。
    public abstract void setOp(Operation op);
    // ディスプレイをクリアします。
    public abstract void clearDisplay();
    // メモリAからBへコピーします。
    public abstract void copyAtoB();
    // メモリAの取得です。
    public abstract double getA();
    // エラー表示を設定します。
    public abstract void setError();
    // エラー表示を解除します。
    public abstract void clearError();
    // +/-記号を反転します。
    public abstract void changeSign();
}
[/java]

メインとなる電卓クラスはContextを実装します。これらは各Stateからのコールバック関数となります。実際の挙動については電卓クラスに実装して、各StateからはContextに記載された動作を呼び出すように実装します。 また、電卓は唯一の出力デバイスとして液晶ディスプレイを持ちます。このディスプレイはクラスで表現しましょう。これにより、ディスプレイを差し替えて電卓の機能アップが容易に行えるように準備しておくためです。今回は単にテキストで表示する12桁のディスプレイを想定しましたが、後にグラフィックを用いたディスプレイクラスへと変更するのも面白いでしょう。 では、電卓クラスです。

[java]
public class Calc implements Context {
    private double A;// 電卓はメモリAを持ちます。
    private double B;// 電卓はメモリBを持ちます。
    private Operation op;// 電卓は演算子を持ちます。
    protected AbstractDisplay disp; // 電卓はディスプレイを持ちます。
    protected State state; // 電卓の状態を表すクラス
    public Calc() {
        A = 0d;
        B = 0d;
        op = null;
        changeState(NumberAState.getInstance());
        disp = new StringDisplay();
    }
    public void onButtonNumber(Number num) {
        state.onInputNumber(this, num);
    }
    public void onButtonOp(Operation op) {
        state.onInputOperation(this, op);
    }
    public void onButtonClear() {
        state.onInputClear(this);
    }
    public void onButtonAllClear() {
        state.onInputAllClear(this);
    }
    public void onButtonEquale() {
        state.onInputEquale(this);
    }
    @Override
    public void addDisplayNumber(Number num) {
        if (num == Number.ZERO || num == Number.DOUBLE_ZERO) {
            if (disp.displayChar.size() == 0 && !disp.commaMode) {
                disp.showDisplay(false);
                return;
            }
        }
        if (num == Number.COMMA && !disp.commaMode && disp.displayChar.size() == 0) {
            disp.onInputNumber(Number.ZERO);
        }
        disp.onInputNumber(num);
        disp.showDisplay(false);
    }
    @Override
    public void clearDisplay() {
        disp.clear();
        disp.showDisplay(false);
    }
    @Override
    public void clearA() {
        A = 0d;
    }
    @Override
    public void clearB() {
        B = 0d;
    }
    @Override
    public double doOperation() {
        double result = op.eval(A, B);
        showDisplay(result);
        return result;
    }
    @Override
    public void saveDisplayNumberToA() {
        A = disp.getNumber();
    }
    @Override
    public void saveDisplayNumberToB() {
        B = disp.getNumber();
    }
    @Override
    public void showDisplay() {
        disp.showDisplay(false);
    }
    @Override
    public void showDisplay(double d) {
        disp.setNumber(d);
        disp.showDisplay(true);
    }
    @Override
    public Operation getOp() {
        return op;
    }
    @Override
    public void setOp(Operation op) {
        this.op = op;
    }
    public double getA() {
        return A;
    }
    public double getB() {
        return B;
    }
    @Override
    public void changeState(State state) {
        this.state = state;
    }
    @Override
    public void copyAtoB() {
        B = A;
    }
    @Override
    public void clearError() {
        disp.clearError();
    }
    @Override
    public void setError() {
        disp.setError();
    }
    @Override
    public void changeSign() {
        if (disp.getNumber() != 0d) {
            disp.minus = !disp.minus;
            disp.showDisplay(false);
        }
    }
}
[/java]

いかがでしょう。メインとなる電卓クラスにはごちゃごちゃとしたIF文やCASE文も無く、実にすっきりしていますね。イベントが発生したら、stateのメソッドを呼び出しているだけです。あとは状態にあわせた適切な処理をstateが選択し実行してくれるという感じです。

[java]
 public void onButtonNumber(Number num) {
        state.onInputNumber(this, num);
    }
    public void onButtonOp(Operation op) {
        state.onInputOperation(this, op);
    }
    public void onButtonClear() {
        state.onInputClear(this);
    }
    public void onButtonAllClear() {
        state.onInputAllClear(this);
    }
    public void onButtonEquale() {
        state.onInputEquale(this);
    }
[/java]

電卓の液晶ディスプレイに数値を表示するStringDisplayは、内部に12桁の文字列を保持するクラスです。表示に関する処理はほとんとStringDisplayに委譲しています。StringDisplayの実装はダウンロードしたサンプルの中にありますので参考にしてください。ポイントは12桁の文字列をスタックで保持している点です。電卓の入力は後入れ先出し型のスタック形式なのです。スタックで保持することにより、今後BS(バックスペース)ボタンを追加したとき、「液晶ディスプレイに表示された数字を後ろから1つ削除する」といった操作に簡単に対応できることを狙っています。 さて、電卓クラスの唯一の機能である演算ですが、電卓クラスのdoOperationメソッドに実装されています。でも実際の演算らしきものが一切ありませんね。

[java]
public double doOperation() {
    double result = op.eval(A, B);
    showDisplay(result);
    return result;
}
[/java]

これは四則演算処理を列挙型で定義しているからです。javaの列挙型はその実態はクラスですので、処理を実装することも出来るわけです。これによりどの演算子であってもop.eval(A, B)という記述で四則演算ができてしまいます。 列挙型Operationのソースです。

[java]
public enum Operation {
    PLUS   { double eval(double x, double y) { return x + y; } },
    MINUS  { double eval(double x, double y) { return x - y; } },
    TIMES  { double eval(double x, double y) { return x * y; } },
    DIVIDE { double eval(double x, double y) { return x / y; } };
    abstract double eval(double x, double y);
}
[/java]

列挙型でメソッドの抽象を宣言し、定数ごとに具象メソッドでオーバーライドしています。そのようなメソッドを「定数固有」メソッドと呼びます。やや複雑な仕組ですが、知っておくととても便利です。 これで電卓の基本部分の説明は終わりです。 さてなかなかAndroidの話に入れませんね、電卓のベース部分が完成しましたので、次回からお待ちかねAndroidへの実装を行います。あと暫くお付き合い下さい。

参考文献

増補改訂版Java言語で学ぶデザインパターン入門 StateパターンでCSVを読む デザインパターンの使い方: State