Why デザインパターン
職業プログラマのみなさんおはこんばんわ。
大量にプログラムを書く仕事を想像してプログラマになられたかもしれませんが、大量にプログラムを読む仕事 をなさっているでしょうか?1
規模の大きな開発や、短期間での開発に対応するためには、既存プロジェクトのソース、オープンソースやフレームワークのソースなど、他人の書いたソースを読んで理解して利用する必要があるため、プログラムを書く機会より読む機会が増えていると思います。
ここで困ったことが一つあります。
『プログラムはコンピューターが理解できさえすればどのように書いてあっても動く』
ということです。
例えば、変数名を hoge
と定義したプログラムは動作しますが、書いた本人以外の他人がそのプログラムの意図を理解するのは困難でしょう。
あるいは、一つの関数が1000行のプログラムも動作しますが、他人が読むことは大変困難でしょう。悪いことに書いた本人ですら3ヶ月後には読めないこともあるでしょう。
これらは極端な事例ですが、**「誰が設計しても、このケースではこう設計するのが妥当」**というベストプラクティスに沿って職業プログラマ全員が読んだり書いたりすれば毎回コーディングや設計で悩んだり悩まされたりしなくてよくなるはずです。
これがデザインパターンが生まれた理由であり、わたし達が学習する理由かと思います。
残念なことに実装ありきの解説で敷居が高かったり、利用頻度がそこまで高くなかったりして微妙かなと思うところもありますが、知識として知っておいて損はないかと思いますので身近なユースケースを上げて説明したいと思います。
生成に関するパターン(1 - 5)
デザインパターン | ユースケース |
---|---|
1. Abstract Factory(抽象的な工場) | ココイチはカレー工場で、二郎はラーメン工場 |
2. Builder(建築者) | ベースの武器と素材や不要な武器と金を渡して武器強化 |
3. Factory Method(Factoryにオブジェクト生成メソッド) | 餅は餅屋に、カレーはカレー屋に |
4. Prototype(雛形) | セントラルキッチンでスープ製造 |
5. Singleton(単一) | セントラルキッチンは一つ |
構造に関するパターン(6 - 12)
デザインパターン | 超入門概要 |
---|---|
6. Adapter(適合) | 居抜き店舗 |
7. Bridge(橋) | 展示場とイベント |
8. Composite(複合物) | ゲームオブジェクト |
9. Decorator(装飾者) | PSストアセールと消費税(悪い例) |
10. Facade(窓口) | 区役所とウーバーイーツ |
11. Flyweight(軽量) | ネットフリックス |
12. Proxy(代理人) | カリスマ経営者率いるプログラミングスクール、オフショア窓口、美容院、メルカリ |
振る舞いに関するパターン(13 - 23)
デザインパターン | ユースケース |
---|---|
13. Chain of Responsibility(責任の鎖) | お前じゃ話にならん店長を呼んでこい |
14. Command(コマンド) | 手順書通りやって |
15. Interpreter(インタプリタ) | RPGゲームのスクリプト言語 |
16. Iterator(繰り返し) | 古い設計。関数型プログラムをしよう |
17. Mediator(指揮命令) | SESと指揮命令 |
18. Memento(記念品) | 記念品よりセーブデータ |
19. Observer(観測者) | 新刊お知らせ機能 |
20. State(状態) | キャラクタ選択 |
21. Strategy(戦略) | 戦略か戦術か、それが問題だ |
22. Template Method(テンプレートメソッド) | 学習指導要綱 |
23. Visitor(訪問者) | ウーバーとマックデリバリーとヤマトと佐川と… |
1. Abstract Factory(抽象的な工場)
1つ目からちょっと何言ってるか分からないですね。
晩ごはんを食べるプログラムを書くとしましょうか。
ココイチならカレー、二郎ならラーメンが食べられますが、まだ今日はどちらを食べたいか決まってません。
この様な時に、晩ごはん工場をAbstract Factory と見立てて、ココイチや二郎をFactoryと見立てて、プログラムを書くとはっきり食べたいモノが決まってなくてもプログラムが書けます。
また、急に気が変わってサイゼの辛味チキンが食べたくなったとしても、サイゼリヤを Factory として実装して、サイゼリヤを create するだけでその後晩ごはんを食べる処理などは修正が不要となります。
SupperFactory factory = new CocoichiFactory();
// 以下のコードは店(Factory)によらず共通
factory.createFood(800);
...
2. Builder(建築者)
2つ目もちょっと何言ってるか分からないですね。
『建築者に渡すべきものを渡して結果を取得する方法』だけ取り決めとくという考え方です。
身近な例だと、ソシャゲの武器強化などでしょうか。
Equipment
は Material
から派生しておくと、強化用素材だけではなく、不要な武器防具も強化用素材として利用できそうですね。
interface LeveledEquipmentBuilder {
void setTarget(Equipment equipment); // 強化対象
void addMaterial(Material material); // 強化用素材
void setMoney(int money); // 強化費用
Equipment build(); // 結果取得
}
課金アイテムなどで成功率を上げられるなら、addSuccessRate
といったインターフェイスも追加する必要があるでしょう。
武器強化機能は様々なソシャゲで見かけ今や実装必須の機能と言えるので再利用可能なコードにしておくと便利そうですね。
3. Factory Method(Factoryにオブジェクト生成メソッド)
Abstract Factory
の章で、カレーやラーメンをFood
として定義していましたが、カレーはカレーだしラーメンはラーメンです。
カレーにはごはんやナンがつきますが、ラーメンに小ライスがつくことはありますが、ナンがくっつくことはわたしが知る限りではないです。これらを Food
で管理するように設計した場合、ラーメンにナンをくっつけることもできてしまいます。なかなか興味深いFood
の完成となるわけですが、プログラムとしては危険です。
ココイチや二郎に Factory Method を持たせることで、うっかりカレー屋でラーメンを提供するのを防ぐことができます。
4. Prototype(雛形)
これは、町田商店のセントラルキッチンですね。
各店舗で同じ様にスープを1から炊かないで、セントラルキッチンでスープを作って各店舗に配送。
ひと味足りないと感じる方もいるかもしれませんが、大変合理的な考え方ですよね。
プログラムは手間暇かけても丹精込めても書いた通りにしか動かないので、プロトタイプ作ってコピーすればよいです。
class CentralKitchen {
Soup createSoup() {
// すごく大変な工程でスープを作る
return soup;
}
}
class ShopKitchen {
// セントラルキッチンで作ったスープを足していく。
// スープを作るメソッドは存在しない。
void addSoup(Soup soup);
}
...
// セントラルキッチンでスープを作って、各店舗に配送
ShopKitchen shopKitchens[] = new ShopKitchen[{店舗数}];
Soup amazingSoup = (new CentralKitchen()).createSoup()
for(ShopKitchen k : shopKitchens) {
k.addSoup(amazingSoup);
}
5. Singleton(単一)
Prototype
の章のサンプルコードで勘のよい方は気づいたかもしれませんが、プログラム上では、セントラルキッチンが作り放題ですね。これを出来なくするパターンがSingleton
です。
class CentralKitchen {
private static CentralKitchen instance = new CentralKitchen();
private CentralKitchen(){} // private にして外部から呼べなくする。
public static CentralKitchen getInstance() {
return instance;
}
}
...
Soup soup = CentralKitchen.getInstance().createSoup()
6. Adapter(適合)
美味しいラーメンを作る自信があるのでラーメン店を作りたいが、お金もないし、店舗のデザインなどについては全くの素人だとします。店舗のキッチン設備や什器がある程度整った『居抜き店舗』を見つけて改装すれば、1からラーメン屋を作るよりは手間がかからないことでしょう。これがAdapter
です。
class UnpopularDiner {
// 目も当てられないぐらい汚いキッチン
// ぐちゃぐちゃな店内
// なるべくそのまま利用したい
}
interface CommonNoodleShop {
// ラーメン屋として一般的に必要なメニューを定義
}
// 不人気店をベースにして作った、一般的なラーメンメニューを取り揃えた、わたしの素敵なラーメン店
class MyAwesomeNoodleShop extends UnpopularDiner implements CommonNoodleShop {
Ramen getShoyuRamen() {
// ラーメンを実装。
// UnpopularDiner の設備(プロパティ・メソッド)はすべて使える。
// 汚いキッチンを使って美味しいラーメンを作る。
// ...
return ramen;
}
}
実際のプロジェクトでは、極めて重要だが解析困難なビジネスロジックが記述された class に遭遇することがある。そのような class は 1[byte] でも修正したら動かなくなる絶妙な実装であるため、Adapter を用いて本体にはなるべく触らない様にすることで安全に再利用することを検討するかもしれません。
7. Bridge(橋)
様々な展示場がありイベントがあります。イベントの主催者や出展者は、展示場とは別の所で準備をして展示場に入り展示を行います。コミケ、ゲームショー、あるいはモーターショーといった展示の実装には、ビックサイト側は関与していません。この関係性を Bridge
で表すと下図のようになります。
// ビックサイトでコミケ開催
ComicmarketEvent comicmarketEvent = new ComicmarketEvent();
Exhibition e = new BigsightExhibition(comicmarketEvent);
e.doExhibition();
8. Composite(複合物)
枝と葉を同じ様に処理するとか、容器と中身を同じ様に処理すると再帰的処理が簡単になるパターン。
フォルダとファイルが一般的で分かりやすい例かと思いますが、ここではやはりゲームを例に説明したいと思います。
3Dのゲームを想像してください。世界(Scene
)の中には背景(Background
)、プレイヤー(Player
)、武器(Weapon
)、弾(Bullet
)、アイテムボックス(ItemBox
)やアイテム(Item
)などのオブジェクト等があると思います。item
はアイテムボックスの中にあったり地面(Ground
)に転がっていたりプレイヤーに拾われたりします。
プレイヤーはオブジェクトですが、武器を所持するオブジェクトであったりもします。
また、オブジェクトとして意識されることはあまりないと思いますが光源やカメラもあります。
これらのゲームオブジェクト(GameObject
)の配置や状態はゲームの進行状況によって頻繁に変更する必要がありますし、それらの操作を個別に考えるのは大変なので、Composite
パターンで表現されることが多い様に思います。
管理イメージを図にすると下図の様になります。
(instance と class 表現が混じっていますが…よろしく解釈してください。)
9. Decorator(装飾者)
PSストアセール、利用してますか?
ちょいちょい色々なセールをやってて、その都度いくつかのゲームがお得なプライスで購入できますね。
消費税の処理は通常価格と割引後の価格に対して適用されます。
これをdecorator
で実装すればセールの影響を受けない処理を書けそうで書けなさそうです。
確かに、消費税コード部分やセール部分は変更が不要だしシンプルですが、この実装ではセール毎にコード修正が必要となってしまいます。
//----------------------------------------
// 通常時コード
Product p = new Product();
Product taxedProduct = new TaxedProduct(p);
// 消費税を考慮した価格を取得
taxedProduct.getPrice();
//----------------------------------------
// セール時コード
Product p = new Product();
Product saledProduct = new SaledProduct(p);
Product taxedProduct = new TaxedProduct(saledProduct);
// 消費税を考慮した価格を取得
taxedProduct.getPrice();
ビジネス要件である、PSStore のセールがどの様な周期でどの様な製品に対して行われているかを理解しないで実装したのでしょうか。
セールや消費税適用前後で値段しか変わらないのにPSStoreに並べる商品自体をinterface
として定義している点も意味不明ですね。
では、PSStorePrice
にすればいいかというとそうでもなさそうです。
近視眼的にデザインパターンを適用しても、再利用性が上がらない上にメンテナンスを行う人などに不快感を与える可能性もあるので注意が必要そうです。
10. Facade(窓口)
区役所で住民票を取るとします。区役所は巨大な建物で大変多くの業務を行っているが、住民票発行の窓口に行けばとりあえず住民票を取る事がでます。
適切な窓口が設定されることで区役所の業務すべてを把握することなく円滑に目的を達成できるようにするためのパターンがFacade
です。
配達に関する機能を持っているため、単純な窓口ではないですがウーバーイーツを想像してみましょう。
利用者はウーバーイーツの裏にある複雑な仕組みを意識することなく簡単なインターフェースで様々な食料を手に入れる事ができるようになりました。
これがFacade(窓口)
を利用するメリットです。
コードをFacade
パターンで書けば分りやすくなるのではなく、 『Facade
パターンを利用した方が自然に記述できる外部設計に対してFacade
を適用する』 と考えると良いと思います。
人間向けの言語で書かれた外部設計を、コンピューターに分かる様に翻訳するのが職業プログラミングです。
クラス名にFacade
とつける時点で、外部設計からは読み取れない複雑な翻訳(実装)になっているかもしれません。
11. Flyweight(軽量)
ネットフリックスは、知らない人はあまりいないと思いますが動画見放題のサブスクサービスです。
そんなネットフリックスも、創業当時は実店舗を持ってDVDレンタルのビジネスを行っていたのですが、業態を大幅に転換し紆余曲折を経て現在のサービスを提供するに至りました。
現CEOのリード・ヘイスティングスや初代CEOのマーク・ランドルフが Flyweightパターン
を適用しようと思ったかどうかは知りませんが、 Flyweightパターン
で表現できそうです。
(参考)
ネットフリックスという世界的企業はいかに誕生したのか
12. Proxy(代理人)
コードレベルで考えると Adapter パターンとの違いが分かりづらいため、ここでも人間世界で起きている事例で考えます。
例えば、何かやらかした際に、法廷で争わなければいけなくなりますが、おそらく多くの人は法律の知識がないため弁護士を代理人として立てると思います。
このような、『何らかの意味ある代理行為』がデザインパターンで定義されていることかと思います。
代理行為には様々な種類がありますが、本章では以下に4種類列挙します。
カリスマ経営者率いるプログラミングスクール(A virtual proxy)
カリスマ経営者の有名なITスクールに入ると、カリスマ経営者に教えてもらえてプログラムが書けるような気分になります。
実際にはカリスマ経営者は、社員やバイトの先生に何らかの方針を伝えるのみで、教育行為の大半は社員やバイトの先生が行うことになると思います。
カリスマ経営者は、カリスマとして活動したり経営者として活動したりするのが忙しいので全ての生徒の相手ができないため、代理で教える先生が必要となります。
オフショア窓口(A remote proxy)
単価の安い海外にIT業務の委託を行いたいですが、英語が分かりませんし、海外に行って仕事を発注するのも大変です。
一方、単価の安い国側でも単価の高い日本の仕事がほしいですが、日本に移住するのも大変ですし生活コストが上がってしまうため安い単価で仕事が受けられなくなります。
日本語と現地語が話せるPMを日本事務所に配置することで現地からの業務提供を円滑に行うことができます。
美容院(A protective proxy)
多くの人は、髪の毛を自分で切ることができますが、それをしようとは思いません。イケてない髪型になったら取り返しがつかないからです。
かっこよく(かわいく)切ってくれる人に頼むのが安全です。
メルカリ(A smart proxy)
いらなくなった古着を売るとして、友達に声をかけるなど色々な手段があると思いますが、メルカリに出品される方も多いのではないでしょうか。
家の前に置いておいても誰かの目に止まり購入されることはほぼないと思いますが、メルカリに出品しておくとあなたに代わって古着を売り飛ばしてくれます。
(参考)
What is the exact difference between Adapter and Proxy patterns?
13. Chain of Responsibility(責任の鎖)
大勢のお客で賑わっている居酒屋での出来事です。炭酸麦茶を頼んで30分以上経ちますがまだ届きません。いい加減遅いなと思いテーブルの呼び出しベルを押し店員を呼び出し「オーダー入ってます?」とすまなそうに確認して炭酸麦茶を提供してもらうことができました。
一方隣の席からは「お前じゃ話にならん、店長を呼んでこい」という怒鳴り声が聞こえてきました。興味津々でそちらの席を横目で観察したところ、バイトリーダーらしき人物が出てきて謝罪をするも結局店長は出てこないという様子を目撃することになりました。
これが、責任の鎖です。
この居酒屋のスタッフは、バイト→バイトリーダー→雇われ店長→本部マネージャー→本部社長と言った責任の鎖でつながれており、顧客の怒り度に対して誰がどう対処するかを規定しています。
public abstract class Responsible {
private Responsible next = null;
private String name = null;
public Responsible(String name){
this.name = name;
}
public Responsible setNext(Responsible next){
this.next = next;
return next;
}
public final void handle(Claim claim){
if(canHandle(claim)) {
doHandle(claim);
} else if(next != null) {
next.handle(claim);
}else{
//FIXME: Something to do.
}
}
protected abstract boolean canHandle(Claim claim);
protected abstract void doHandle(Claim claim);
}
// バイト
public class PartTimer extends Responsible {
public HeadOfPartTimer(String name){
super(name);
}
protected boolean canHandle(Claim claim){
if(claim.level.lessThan(Claim.ANGRY)){
return true;
}
return false;
}
protected void doHandle(Claim claim){
// 平謝り
// オーダーされた商品を持ってくる
}
}
// バイトリーダー
public class HeadOfPartTimer extends Responsible {
public HeadOfPartTimer(String name){
super(name);
}
protected boolean canHandle(Claim claim){
if(claim.level.lessThan(Claim.SUPER_ANGRY)){
return true;
}
return false;
}
protected void doHandle(Claim claim){
// 平謝り
// 次回使える5%引きチケットを渡す
}
}
// 雇われ店長
public class StoreManager extends Responsible {
public StoreManager(String name){
super(name);
}
protected boolean canHandle(Claim claim){
if(claim.level.lessThan(Claim.SUPER_SUPER_ANGRY)){
return true;
}
return false;
}
protected void doHandle(Claim claim){
throw new UnsupportedOperationException();
}
}
14. Command(手順書通りやって)
作業の手順を書いた手順書があります。コピーすれば複数の手順書にできますし、いろいろな人に渡して作業をお願いすることができます。
Command パターンは、「手順書」と「依頼」を表すパターンです。手順の内容は実際の手順書によって異なります。
「銀座のコーラを買ってくる」依頼であれば「銀座への行き方」などが手順書に定義されます。「銀座に行く」手順書と「コーラを買う」手順書を分ければ、買い物コマンドを作るだけで銀座での買い物はし放題かもしれません。
コマンドを実行した結果としてコーラを受け取りたいですし、コーラ以外にも焼きそばやパンも食べたいとなると、戻り値が void
では受け取れず、また、コーラ買ってくるまでの間他の事をしていたいとなると、非同期での実行が必要になってきます。
適用自体は簡単ですが、要件をよく確認して設計しないといたずらに複雑なコードになるだけなので注意が必要です。
15. Interpreter(RPGゲームのスクリプト言語)
スクリプト言語がインタプリタによって実行されているかは一旦横においておくことにします。
デザインパターンとして学ぶべきことは「業務上の課題を解決するためになんらかの文法の言語を作りそれを解析して動かす」パターンということです。
RPGゲームを作る場合を例に考察してみましょう。
RPGゲームは、街の入り口に立っている町人Aに「ようこそラダトームのまちへ」と喋らせたり、主人公やライバルキャラクターに小芝居させながら大量の会話をさせるなどのイベントシーンが必要です。これらのイベントシーンをプログラマがアセンブラやC言語などのプログラム言語で書くのは非常に大変です。
ゲームの企画やプランナーの人が「talk(charaA, ようこそラダトームのまちへ)」と書いたら町人Aがセリフをしゃべってくれる仕組みがあったらどうでしょうか。いちいちプログラマにお願いして嫌な顔をされながらイベントを作るよりは遥かに簡単そうです。一方のプログラマはバトルプログラムやエフェクトプログラムに集中できそうで win win です。
この時に作る talk()
等を解析して動かすプログラムを巨大な if 文で作ると大変なので、インタプリタを作って Composite パターンなどに当てはまるように解析して実行していくのがインタプリタパターンです。
昔は毎回スクリプトエンジンを手作りしていましたが最近はLua言語が人気があるようですね。
16. Iterator(古い設計。関数型プログラムをしよう)
iterator パターンは、配列やリストに含まれる要素に対して何らかの処理を行うパターンです。
hasNext()
, getNext()
などのメソッドを見かけたら iterator パターンでなんらかの集合の要素を取得しているんだなと見てもらえば良いと思いますが、近代言語ではよりよい方法が提供されているので敢えて実装しない方がよいでしょう。
配列操作用のメソッドに関数を渡す事で配列の全ての要素に対して任意の処理を行えます。
例)
find(f) : f関数の戻り値が true の要素を返す(1つ)
filter(f) : f関数の戻り値が true の要素を返す(複数)
map(f) : f関数の戻り値の配列に map する
reduce(f) : 配列の要素に対してfで与えられる関数を順次実行した結果を返す(1つ)
some(f) : f関数の戻り値が true の要素があるか?
every(f) : f関数の戻り値がすべて true か?
flatMap(f) : f関数の戻り値(配列)を全て連結した配列を返す
...
余計なことは覚えずに、js では Array クラスのメソッド、Java では StreamApi を利用するとよいでしょう。
参考)MDN Array
17. Mediator(SESと指揮命令)
Mediator(仲介役)というとあまり見かけない表現かと思いますが、われわれの仕事現場に目を向けると分かりやすい例があります。
ITプロジェクトで協力会社さんに参画頂いた際、参画者の性質を見てAさんは開発、Bさんは試験などおおよその方針を指揮命令者が出していると思います。Mediator パターンで実現したいのはこの指揮命令による秩序です。
ただ、SESで参画している協力会社さん同士が直接業務に関する指示を行ってはいけないにも関わらず、指揮命令者の指示を待っていると仕事が進まないので、直接会話して仕事を進めているケースもあるかもしれません。
現実の世界でも必ずしもうまく機能しないケース(指揮命令系統が複雑になるだけで仕事が進まない)もある通り、Mediatorパターンを適用する際には、本当にMediatorが必要なのかをよく考えて適用する必要があるでしょう。
18. Memento(記念品よりセーブデータ)
令和の世の中で、Memento(記念品、形見)と言われて日常で馴染み深い人は少数派の様に思います。
コンピューターゲームを遊んで、次の日その続きから遊ぶために、その時の状態を保存したセーブデータを作ったことがある人はそれなりにいるのではないでしょうか。
Mementoパターンは、ある時点での処理の続きを再開できるように必要なデータを保存しておくパターンです。
セーブデータは、次回からゲームを再開するために、その時点での所持金や経験値やレベルなどのプレイヤーの状態を表す変数が保存されますが、その時点での画面の映像などは保存されません。
Mementoパターンでも同様に、プログラムの動作に必要な最小限のデータのみを保存します。
19. Observer(新刊お知らせ機能)
Observer(観測者)パターンは、言葉の響きからすると観測者が Polling などして何かを観測するように思えますが、実際は、「新刊お知らせ機能」の様に、「読んでる漫画の新刊がでたら通知してくれ」と登録しておくと、新刊が出た際に Observer(観測者)に通知が来るパターンです。
タイマーなどで定期監視をしなくてよいためコンピュータに対する負荷も少なく、「新刊が出た」などのイベントに対して処理を実行できるイベントドリブンの考え方も人間に優しいです。
Angular や Vue や React などに実装されているデータバインディングがあるとほとんど無用になるのですがちょっとうまく動かない時に利用する watch がObserver の考え方と近いと思います。
20. State(キャラクタ選択)
執筆時点の流行に乗ると、ELDEN RING での放浪剣士や素寒貧といった素性選択かと思いますが、戦士や魔法使いなどの職業の例えの方が普遍的で分かりやすそうなのでそちらで話を進めます。
プレイヤーの選択により、プレイヤーの操作するキャラクターが「戦士」や「魔法使い」になるゲームがあったとします。「戦士」は剣や盾などを装備しての近接攻撃が得意で、魔法使いは杖やローブなどが装備可能で魔法による遠距離攻撃が得意です。
AボタンやBボタンを押下時のイベント関数に「もし戦士だったら」とか、「もし魔法使いだったら」といったコードを書くのは一見直感的です。
最初に戦士しかいないところに魔法使いを追加したらその様なコードを書くことも往々にしてあるでしょう。
ただこれが、複雑なコマンド入力でキャラクタ固有スキル発動もしたい、職業によって固有のスキルツリーも持ちたいとなってくると「もし戦士だったら」という分岐をどこに書いたらいいかがよく分からなくなってきます。拡張が辛くなってきた時がリファクタリング適正期かと思います。
仕様書も書かずにノリで拡張したコードであれば、新たにプロジェクトに追加されたメンバーに新ジョブ「僧侶」の実装を依頼することも難しいでしょうし、ゲームをやらないメンバーはスキルツリーのコードが職業によって分岐があることに気がつかないかもしれません。
そこで、状態(職業)毎にクラスを定義して各クラスに対して定められたアクションの実装を行えば、自然かつ拡張性の高いクラス定義を行えるという考え方が State パターンです。
例)
- 攻撃行動
- 「戦士という状態」のプレイヤーが攻撃をすれば剣を振る
- 「魔法使いという状態」のプレイヤーが攻撃をすれば魔法を詠唱
- 防御行動
- 「戦士という状態」のプレイヤーが防御をすれば盾を構える
- 「魔法使いという状態」のプレイヤーが防御をしても何もできない
- etc...
jobState.attacAction()
でそれぞれの職業に応じた攻撃を行えるといった実装になるため、ここに「騎士」や「僧侶」を追加しても Player
クラスやその他のコードの変更は不要で、KnightState
や ClericState
といった新規追加クラスに対してコードを書けばよいというのも精神衛生上よいです。
職業を状態と捉えるかどうかは議論がありそうですが、デザインパターンの本質としてはパターンを適用することでコードが分かりやすくなるかどうかというところだと思います。
21. Strategy(戦略か戦術か、それが問題だ)
戦略を定義したクラスを用意してそれを入れ替えれば、コードの修正範囲は小さいという考え方のパターンですが、ストラテジーパターンで作られているJavaのソートは、皆さんご存知の通り大変冗長なコードになっています。
戦術レベルの内容を戦略で解決するというデザインが適切ではないというポピュラーな例と言えるでしょう。
Strategy は適用すべきところに適用することにして、ソートは Lambda や Comparator を利用して書くのがよいでしょう。
// 【冗長】昇順ソート戦略
Comparator<String> ascSortStoratagy = new Comparator<String>() {
@Override
public int compare(String s1, String s2) {
return s1.compareTo(s2);
}
};
// 【冗長】降順ソート戦略
Comparator<String> descSortStoratagy = new Comparator<String>() {
@Override
public int compare(String s1, String s2) {
return s2.compareTo(s1);
}
};
// 【冗長】昇順ソート戦略でソート
List<String> sortedItems = items.stream()
.sorted(ascSortStoratagy) // 別途定義した昇順ストラテジーを指定。インラインでも書けるが冗長
.collect(Collectors.toList());
// Lambda でソート
List<String> sortedItems = items.stream()
.sorted((s1, s2) -> s1.compareTo(s2)) // Lambda 式で直接定義
.collect(Collectors.toList());
// Comparator でソート
// クラスのフィールドを比較する場合は、Comparator.comparing を利用する
List<String> sortedItems = items.stream()
.sorted(Comparator.naturalOrder()) // 昇順
.collect(Collectors.toList())
(参考)
Comparatorを使ってListをソートする方法
22. Template Method(学習指導要綱)
日本には様々な学校がありますが、そこで提供される教育はおよそ同じような内容でしょう。
年間にどれぐらい授業があって、どんなふうに教育しなさい、というのが学習指導要綱として定められているからです。
その学習指導要綱に沿って各教員が教育を行う、というのが Template Method の考え方に近いでしょう。
ところで、複雑すぎる Template Method はシステムの品質を上げません。実装が困難で、実装者による個人差が大きく出てしまうからです。
Template Method を実装することが教員に要求されていますが、実装次第で東大合格者が出たり学級崩壊が起きたり、過労で離任が発生したりもしていることからも分かるでしょう。
本稿を書くにあたって【総則編】小学校学習指導要領(平成29年告示)解説を一読しましたが、これは大変だなと思いました。
// 学習指導要綱
public abstract class CurriculumGuideline {
// 授業の準備をする
public abstract void prepareClassroom();
// 授業をする
public abstract void doClassroom();
// 宿題をつくる
public abstract void makeHomework();
// 宿題を採点する
public abstract void checkHomework();
// and so on...
}
// 怠け者先生
public class LazyTeacher extends CurriculumGuideline {
public abstract void prepareClassroom() {
// TODO Auto-generated method stub
}
public abstract void doClassroom() {
// TODO Auto-generated method stub
}
public abstract void makeHomework() {
// TODO Auto-generated method stub
}
public abstract void checkHomework() {
// TODO Auto-generated method stub
}
}
一方で、コンビニやファミレスのアルバイト作業は作業のテンプレート化がうまくできていて結果に差異が少ない様に思えます。たまに例外が発生して twitter などで炎上していますが、作業要項の問題とは考えにくいです。
Template Method はシステムを捉えるのに必須な考え方ですが、適用が難しいという印象です。
23. Visitor(ウーバーとマックデリバリーとヤマトと佐川と…)
家に来そうなVisitor(訪問者)を列挙してみました。
ウーバーが顧客の家を訪問したら『お金をもらって商品を渡す』という行為をします。
訪問者側に、訪問時の処理を実装するのが Visitor パターンです。
この様な実装にしておくと、顧客に断りなく値上げをしたりサービスの質を落とすこともやり放題…ではなく、アルゴリズムをオブジェクトの構造から分離ができますが、少々大掛かりなパターンであまり利用することはない印象です。
おわりに
ちょいちょい時間を見つけて更新していたら1年ぐらいかかりましたが、全部書けてよかったです。
なんとなく意識高く難しく感じる『デザインパターン』を少しでも身近に感じて頂けたら幸いです。
デザインパターンは偉大な先人達が開発する時の 『ベストプラクティス』 をパターンとして抽出してくれたものなので学びは多いです。
近代開発からすると、冗長なパターンが多い様な気もしなくはないですが、設計の考え方をうまく学んで、よりよいコードを書くのに役立てて頂けたらいいのかなと思いました。
(参考サイト)
TECHSCORE デザインパターン
DESIGN PATTERNS
-
実際は大量のドキュメントを書いたり試験したりもする仕事。 ↩