基本的なデザインパターンを押えよう
デザインパターンとは
今回のテーマは「デザインパターン」です。デザインパターンとはクラス設計における定石集で、全部で23パターンあり、それぞれに特徴があります。中でもよく利用されるのはTemplate MethodパターンとStateパターンですが、SingletonパターンやCompositeパターンなどもよく利用されます。今回はTemplate MethodパターンとStateパターンについて、前回解説した例題とクラス図を使用して解説します。
デザインパターン(Template Method)を適用する
前回解説した例題を使って、デザインパターンが適用できないかを検討してみましょう。前回の例題とクラス図を再掲します。
「A社には社員として鈴木さん、佐藤さん、高橋さんがいます。3人は朝起きてから身支度をして、通勤電車に乗り、現場で仕事をします」
このクラス図は「「鈴木」「佐藤」「高橋」の各クラスがそれぞれ「身支度をする」メソッドを呼び、次に「通勤電車に乗る」メソッドを呼び、最後に「現場で仕事をする」メソッドを呼ぶ作りになっています。
各クラスはそれぞれ身支度の仕方や通勤電車の種類、仕事の内容は違いますが(例えば「鈴木」クラスの「身支度をする」メソッドは「洗面所で顔を洗う」、「通勤電車に乗る」メソッドは「京急線を使う」だが、「佐藤」クラスの「身支度をする」メソッドは「シャワーを浴びる」、「通勤電車に乗る」メソッドは「山手線を使う」など)、各メソッドの振る舞い(内容)は違っても「身支度をする」メソッドを呼んで「通勤電車に乗る」メソッドを呼び、最後に「現場で仕事をする」メソッドを呼ぶ、処理の流れ(順番)は一緒です。
これは、デザインパターンの処理の流れが一緒で振る舞いだけが異なる場合に有効なTemplate Methodパターンが適用できそうです。Template Methodパターンとは、親クラスで処理の流れ(アルゴリズム)を実装し、処理の流れで順次呼ばれるメソッドを抽象メソッド(定義だけでサブクラスに処理を強制的に実装させるためのメソッド)として宣言します。親クラスで宣言した抽象メソッドの具体的な処理はサブクラスで実装します。
上のサンプルのように、処理の内容は違っても処理の流れは一緒のような状況はプログラミングで割と起こるかと思います。何も考えないで作ると同じような処理の流れのロジックがあちこちに登場する冗長なプログラミングになりますが、Template Methodパターンを適用すると親クラスに処理の流れを記したテンプレートメソッドを用意し、そのテンプレートメソッドからは次々にメソッドが呼ばれていくため(テンプレートメソッドから呼ばれるメソッドをフックメソッドという)、サブクラスはフックメソッドをオーバーライド実装するだけとなります。
オーバーライドとは、親クラスで定義しているメソッドをサブクラスで再定義(上書き)することを言います。親クラスで定義しているメソッドの振る舞いをサブクラスでは異なる振る舞いにしたい場合に使用します。親クラスで定義しているメソッドが抽象メソッドの場合は、強制的にサブクラスでオーバライド実装することになります。
Template Methodパターンを適用すると、サブクラスはメソッドを順番に呼ぶ必要はありません。順番に呼ぶ責務は親クラスになります。
親クラスである社員クラスのプログラムを以下に書きます。
public abstract class 社員 {
public void 一日の行動() {
身支度をする();
通勤電車に乗る();
現場で仕事をする();
}
protected abstract void 身支度をする();
protected abstract void 通勤電車に乗る();
protected abstract void 現場で仕事をする();
}
一日の行動()メソッドがテンプレートメソッドです。テンプレートの文字通り、処理の流れのひな型を定義するので、サブクラスは処理の流れを考える必要はなく、メソッドの振る舞いを実装するだけになりました。
public class 鈴木 extends 社員 {
@Override
protected void 身支度をする() {
// 洗面所で顔を洗う
}
@Override
protected void 通勤電車に乗る() {
// 京急線を使う
}
@Override
protected void 現場で仕事をする() {
// テスト計画書を作る
}
}
public class 佐藤 extends 社員 {
@Override
protected void 身支度をする() {
// シャワーを浴びる
}
@Override
protected void 通勤電車に乗る() {
// 山手線を使う
}
@Override
protected void 現場で仕事をする() {
// プログラムを作る
}
}
public class 高橋 extends 社員 {
@Override
protected void 身支度をする() {
// 朝風呂に入る
}
@Override
protected void 通勤電車に乗る() {
// 総武線を使う
}
@Override
protected void 現場で仕事をする() {
// 設計書を作る
}
}
Stateパターンを適用する
次に、例題に以下の文章を追加します。
「このうち鈴木さんはフェーズによって仕事の内容が変わります。設計フェーズでは設計書の作成、プログラムフェーズではプログラムの作成、テストフェーズではテスト計画書の作成をします」
何も考えないとクラス図はそのままで、鈴木クラスの「現場で仕事をする」メソッドにif文でフェーズを分岐するプログラムを作ることを考えるでしょう。
public class 鈴木 extends 社員 {
private String フェーズ;
public 鈴木(String フェーズ) {
this.フェーズ = フェーズ;
}
@Override
protected void 身支度をする() {
// 洗面所で顔を洗う
}
@Override
protected void 通勤電車に乗る() {
// 京急線を使う
}
@Override
protected void 現場で仕事をする() {
if (フェーズ.equals("設計")) {
// 設計書を作る
} else if (フェーズ.equals("プログラム")) {
// プログラムを作る
} else if (フェーズ.equals("テスト")) {
// テスト計画書を作る
}
}
}
これでも要件を満たして動作しますが、if文を使うとフェーズの条件が増えた場合に、if文の分岐も増やさないといけません。
プログラムに手を入れるということは、そのクラスを再テストしなくてはいけなくなるため、非常に効率が悪いです。効率が悪いだけならまだしも、正しく動いているものを修正することでバグを生み出してしまうなど、if文の分岐は数が多いと可読性が低下します。
そこで、第2回で解説したJavaのようなオブジェクト指向言語の最大の特徴とも言える「ポリモーフィズム」を使わない手はありません。
追加された例題をもう一度よく読んでみます。「鈴木さんはフェーズによって仕事の内容が変わります。設計フェーズでは……」となっています。冒頭で幾つかよく使用されるデザインパターンを挙げましたが、このケースではその中で状態によって振る舞いが変わる場合はStateパターンが適用できそうです。
Stateパターンとは、文字通りモノについての「状態」をクラスで表現します。状態によって振る舞い(処理)が異なることがあります。例えば、挨拶は時間帯によって変わります。朝であれば「おはよう」ですが、昼は「こんにちは」になり、夜は「こんばんは」になります。
挨拶は「おはよう」で固定されているわけではなく、状態(この場合は時間帯が状態といえます)によって振る舞いが異なります。このような場合にStateパターンを適用すると個々のクラスで状態を表現するため、分岐が消えてシンプルな作りになります。
ちなみに、状態によって振る舞いが変わる場合はStateパターンですが、この「状態によって振る舞いが変わる」が例題の「フェーズによって仕事の内容が変わる」に当てはまり、Stateパターンの「状態」が例題の「フェーズ」に当てはまるわけです。
それでは試してみましょう。例題ではフェーズの状態によって仕事の内容が変わるので、仕事内容の変化はif文ではなく「フェーズ」というインターフェースを用意します。フェーズインターフェースには「仕事をする」宣言を定義し、交換可能な作りにします。
public interface フェーズ {
void 仕事をする();
}
フェーズインターフェースを実装したクラスは、状態(フェーズ)ごとの具体的な振る舞い(仕事内容)を記述します。
public class 設計フェーズ implements フェーズ {
@Override
public void 仕事をする() {
// 設計書を作る
}
}
public class プログラムフェーズ implements フェーズ {
@Override
public void 仕事をする() {
// プログラムを作る
}
}
public class テストフェーズ implements フェーズ {
@Override
public void 仕事をする() {
// テスト計画書を作る
}
}
鈴木クラスの「現場で仕事をする」メソッドは、次のようになります。
public class 鈴木 extends 社員 {
private フェーズ フェーズ;
public 鈴木(フェーズ フェーズ) {
this.フェーズ = フェーズ;
}
@Override
protected void 身支度をする() {
// 洗面所で顔を洗う
}
@Override
protected void 通勤電車に乗る() {
// 京急線を使う
}
@Override
protected void 現場で仕事をする() {
フェーズ.仕事をする();
}
}
仕事内容の変化はif文ではなく、受け取ったインスタンスによって異なる振る舞いをする、まさにポリモーフィズムで変えています。クラス図で表すと下図のようになります。
鈴木クラスからフェーズインターフェースには菱形のない実線だけで線を引いていますが、前回解説した包含は菱形の付いている実線で表します。包含は集約とも言い、「全体と部分」の意味を持つ場合に用いため、今回の「鈴木」と「フェーズ」は関係があるという意味の「関連」にします。関連は実線だけで表します。
ちなみに、継承、集約、関連以外に「依存」というものもあります。依存は関連よりもクラス間の依存度が低く、協調の長さが短い場合に用います。
プログラムで言うとインスタンス変数ではなく、メソッド内で定義するローカル変数で対象のクラスを表します。依存はクラス間の点線で表します。
一度、ここまでで期待通りに動作するかテストクラスを作って確認してみましょう。その前に、日本語で書いたクラスを論理モデルから物理モデルに進めて英語に直します。
/**
* 会社
*/
public class Company {
}
import java.util.List;
/**
* A社
*/
public class CompanyA extends Company {
private List employees;
public CompanyA(List employees) {
this.employees = employees;
}
public void instruct() {
employees.forEach(employee -> employee.actOneDay());
}
}
/**
* 社員
*/
public abstract class Employee {
public void actOneDay() {
attireOneself();
getToWork();
doTheWork();
}
// 身支度をする
protected abstract void attireOneself();
// 通勤電車に乗る
protected abstract void getToWork();
// 現場で仕事をする
protected abstract void doTheWork();
}
/**
* 鈴木
*/
public class Suzuki extends Employee {
private Phase phase;
public Suzuki(Phase phase) {
this.phase = phase;
}
@Override
protected void attireOneself() {
System.out.println("洗面所で顔を洗う");
}
@Override
protected void getToWork() {
System.out.println("京急線を使う");
}
@Override
protected void doTheWork() {
phase.doTheWork();
}
}
/**
* 佐藤
*/
public class Satou extends Employee {
@Override
protected void attireOneself() {
System.out.println("シャワーを浴びる");
}
@Override
protected void getToWork() {
System.out.println("山手線を使う");
}
@Override
protected void doTheWork() {
System.out.println("プログラムを作る");
}
}
/**
* 高橋
*/
public class Takahashi extends Employee {
@Override
protected void attireOneself() {
System.out.println("シャワーを浴びる");
}
@Override
protected void getToWork() {
System.out.println("山手線を使う");
}
@Override
protected void doTheWork() {
System.out.println("プログラムを作る");
}
}
/**
* フェーズ
*/
public interface Phase {
// 仕事をする
void doTheWork();
}
/**
* 設計フェーズ
*/
public class DesignPhase implements Phase {
@Override
public void doTheWork() {
System.out.println("設計書を作成する");
}
}
/**
* プログラムフェーズ
*/
public class ProgramPhase implements Phase {
@Override
public void doTheWork() {
System.out.println("プログラムを作成する");
}
}
/**
* テストフェーズ
*/
public class TestPhase implements Phase {
@Override
public void doTheWork() {
System.out.println("テスト計画書を作成する");
}
}
テストクラスを作成して、動作するか試してみましょう。
import java.util.ArrayList;
import java.util.List;
public class Test {
public static void main(String[] args) {
List employees = new ArrayList();
employees.add(new Suzuki(new DesignPhase()));
employees.add(new Satou());
employees.add(new Takahashi());
new CompanyA(employees).instruct();
}
}
実行すると、コンソールには期待通り、以下のように表示されました。
洗面所で顔を洗う
京急線を使う
設計書を作成する
シャワーを浴びる
山手線を使う
プログラムを作る
朝風呂に入る
総武線を使う
設計書を作る
あとは、鈴木クラスの「現場で仕事をする」メソッドが状態(フェーズ)の違いで異なる振る舞いをしてくれるかですね。
import java.util.ArrayList;
import java.util.List;
public class Test {
public static void main(String[] args) {
List employees = new ArrayList();
employees.add(new Suzuki(new ProgramPhase()));
employees.add(new Suzuki(new TestPhase()));
new CompanyA(employees).instruct();
}
}
こちらも実行して、期待通りの結果になるかコンソールを確認してみましょう。
洗面所で顔を洗う
京急線を使う
プログラムを作成する
洗面所で顔を洗う
京急線を使う
テスト計画書を作成する
上手いこと「現場で仕事をする」メソッドの振る舞いが変わりましたね。
より優れたクラス設計にするには
Stateパターンによって「鈴木さんはフェーズによって仕事の内容が変わる」という要件は満たしましたが、これは「スナップショット的な考え」でしかありません。スナップショット的な考えとは、「時間的な断面を切り取った状態の考え」、つまり「今回の要件を満たせばOK」という考えです。
それではダメで、あくまで全体、このプロジェクトの機能はどうなっていくのだろうと、もっと大局的に見ていかなければなりません。
クラス設計にしても、確かに「鈴木はフェーズによって仕事の内容が変わる」という今回の要件は満たしましたが、「仕様変更や次のフェーズでは佐藤クラスや高橋クラスにも同様にフェーズによって仕事の内容が変わるという要望が発生するかもしれない」と察知しないといけません。
そのような要望があった時に都度クラスやプログラムを変更していたのでは工数が掛かって仕方がないので、フェーズによって鈴木クラスの仕事内容が切り替わるのなら「今のうちに他のクラスも同様に切り替わる作りにしよう」と考えましょう。
したがって、クラス図は最初に挙げた鈴木クラスからフェーズインターフェースに関連を持ったクラス図ではなく、どの社員も仕事内容が切り替わるように、社員クラスからフェーズインターフェースに関連を持つ以下のクラス図の方が優れた設計と言えます。
ちなみに、社員が増えた場合は他のプログラムには一切手を付けることなく、抽象クラスである社員クラスを継承したクラスを追加するだけです。フェーズが増えて仕事内容が増えた場合は、フェーズインターフェースを実装したクラスを追加するだけです。こちらも他のプログラムの修正は一切不要です。
おわりに
いかがでしたか。今回はデザインパターンの中でもよく使われている基本的なTemplate MethodパターンとStateパターンについて解説しました。
次回はオブジェクト指向の設計原則について解説します。