デザインパターンをPlantUMLで確認します。ここでは、振舞いパターンとして利用頻度の高いパターンについて取り上げます。( Template Method
Strategy
Observer
Command
Iterator
State
)
振舞いパターンでは、機能拡張する中で、同じ処理を重複して書かないようにするためのアイデアなどを与えてくれます。
デザインパターンをPlantUMLで確認(作成パターン編)
デザインパターンをPlantUMLで確認(構造パターン編)
デザインパターンをPlantUMLで確認(振舞いパターン編) ← 今回
Template Method
( 処理手順を共通化 )
@startuml
title Template Method
abstract class AbstractClass {
--
templateMethod()
- commonMethod()
# {abstract} method1()
# {abstract} method2()
}
class ConcreteClassA {
--
# method1()
# method2()
}
class ConcreteClassB {
--
# method1()
# method2()
}
AbstractClass <|-- ConcreteClassA
AbstractClass <|-- ConcreteClassB
note right of AbstractClass
templateMethod内で
「commonMethod」「method1」「method2」を呼び出します。
end note
@enduml
Template Method
を利用することで ConcreteClassA
ConcreteClassB
間で処理の手順(アルゴリズム)を共通化することができます。つまり、Template Method
をみることで全体の処理の流れを俯瞰することができます。
もし、ConcreteClassC
が追加され、 ConcreteClassA
と ConcreteClassC
で共通の処理ができてしまったケースを考えます。その場合、「以下のようにフックメソッド( needsMethod
)を利用した実装にする」または「Strategyパターン
を利用する」といった対応を検討します。
Strategy
( 交換可能な処理を定義 )
@startuml
title Strategy
class Context {
- behaviorA
- behaviorB
--
}
class ContextA
class ContextB
class ContextC
interface BehaviorA {
--
+ method()
}
class BehaviorA1 {
--
+ method()
}
class BehaviorA2 {
--
+ method()
}
interface BehaviorB {
--
+ method()
}
class BehaviorB1 {
--
+ method()
}
class BehaviorB2 {
--
+ method()
}
BehaviorA <|.. BehaviorA1
BehaviorA <|.. BehaviorA2
BehaviorB <|.. BehaviorB1
BehaviorB <|.. BehaviorB2
Context *--> BehaviorA
Context *--> BehaviorB
ContextA -|> Context
ContextB --|> Context
Context <|- ContextC
@enduml
以下のように各クラスがメソッドを実行するケースを考えます。
クラス | BehaviorA | BehaviorB |
---|---|---|
ContextA | A1のメソッドを実行 | B1のメソッドを実行 |
ContextB | A1のメソッドを実行 | B2のメソッドを実行 |
ContextC | A2のメソッドを実行 | B1のメソッドを実行 |
上記ケースの場合、 Template Methodパターン
より Strategyパターン
の利用を優先して検討します。継承( Template Method
)で実装すると、A1のメソッド
B1のメソッド
の処理が重複して書かれることになります。これは、DRYの原則に反します。
また、Strategyパターン
は has-aの関係
で振る舞いを持つので、実行時に動的に振る舞いを変更することも可能です。
Observer
( 通知する側 : 通知される側 = 1 : 多 )
@startuml
title Observer Pattern
interface Subject <<通知する側>> {
--
+ registerObserver(Observer)
+ removeObserver(Observer)
+ notifyObservers()
}
interface Observer <<通知される側>> {
--
+ update()
}
class ConcreteSubjectA {
- observers
- state
--
+ registerObserver(Observer)
+ removeObserver(Observer)
+ notifyObservers()
+ setState()
}
class ConcreteSubjectB {
- observers
- state
--
+ registerObserver(Observer)
+ removeObserver(Observer)
+ notifyObservers()
+ setState()
}
class ConcreteObserverA {
--
* update()
}
class ConcreteObserverB {
--
* update()
}
Subject "1" o- "*" Observer
Subject <|.. ConcreteSubjectA
Subject <|.. ConcreteSubjectB
Observer <|.. ConcreteObserverA
Observer <|.. ConcreteObserverB
@enduml
Observerパターンでは、状態に変化があったことを通知する側(Subject)
が 通知される側(Observer)
に対して通知を受け取る方法を提供しています。Observerが通知を受け取るまでの処理の流れを例として示します。
- 登録
- concreteSubjectA->registerObserver(concreteObserverA)
- concreteSubjectA->registerObserver(concreteObserverB)
- concreteSubjectAのstateが更新された
- concreteSubjectA->notifyObserversが呼び出される
- notifyObservers内で
登録済みobservers
のupdateメソッドを呼び出す
Subject
は Observer
が update
というメソッドを持っていることだけを知っています。以下のようのことは関知せず、Observer
の変更に対する影響を受けません(疎結合)。
Subject
で関知しないこと- 具体的にどういった
Observer
が存在するのか- つまり、
Observer
が増えてもSubject
の処理は変更する必要はありません
- つまり、
update
ではどういった処理が実装されるのか
- 具体的にどういった
Observerパターンは、 言語
や フレームワーク
によってがサポートされているケースがあります。
e.g. https://readouble.com/laravel/5.7/ja/eloquent.html#observers
Command
( 処理の呼び出しを抽象化 )
@startuml
title: Command
class ClientA
class ClientB
class Invoker <<アクションの要求者>> {
- commands
---
+ setCommand()
+ execute(Int id)
}
class ReceiverA <<実際に実行するクラス>> {
--
+ actionXxx()
}
class ReceiverB <<実際に実行するクラス>> {
--
+ actionYyy()
}
class ReceiverC <<実際に実行するクラス>> {
--
+ actionZzz()
}
interface Command {
--
+ execute()
}
class CommandA {
- receiver
---
+ execute()
}
class CommandB {
- receiver
---
+ execute()
}
class CommandC {
- receiver
---
+ execute()
}
CommandA o--> ReceiverA
CommandB o--> ReceiverB
CommandC o--> ReceiverC
Invoker <-- ClientA
Invoker <-- ClientB
Invoker o-> Command
Command <|.. CommandA
Command <|.. CommandB
Command <|.. CommandC
CommandA <--- ClientA
CommandB <--- ClientB
CommandC <--- ClientB
@enduml
処理の流れの例を示します。
- 振る舞いを登録
ClientA
がInvoker
のsetCommandメソッド
を呼び出し、CommandA
CommandB
を登録ClientB
がInvoker
のsetCommandメソッド
を呼び出し、CommandC
を登録
- 振る舞いを実行
- 何かしらのタイミングによって、
Invoker
のexecuteメソッド
が実行されるInvoker
に登録されている各Commandのexecuteメソッド
が実行される- Commandに紐づくReceiverの処理が実行される
- 何かしらのタイミングによって、
Invoker
は実際に実行する処理( actionXxx
actionYyy
actionZzz
)のことを関知せず、execute
という抽象メソッドで処理を起動しています。Commandパターンを利用することで アクションの要求者
と 実際に実行するクラス
が分離され、お互いの処理を関知しなくても良くなります。
Iterator
( 共通した操作でコレクションを操作 )
@startuml
title Iterator
class Client
interface Iterator {
--
+ hasNext()
+ next()
+ remove()
}
class IteratorA {
--
+ hasNext()
+ next()
+ remove()
}
class IteratorB {
--
+ hasNext()
+ next()
+ remove()
}
interface Aggregate {
--
+ createIterator()
}
class AggregateA {
- Array items
--
+ createIterator()
}
class AggregateB {
- List items
--
+ createIterator()
}
Iterator <|... IteratorA
Iterator <|.. IteratorB
Aggregate <|... AggregateA
Aggregate <|.. AggregateB
Aggregate <- Client
Client -> Iterator
AggregateA -> IteratorA
AggregateB -> IteratorB
@enduml
Aggregateは以下の役割を持ちます。
- AggregateAはコレクション(例として
配列
)とIteratorインスタンスを生成するメソッドを保持。 - AggregateBはコレクション(例として
リスト
)とIteratorインスタンスを生成するメソッドを保持。
Clientの処理の流れは以下のようになります。
- AggregateからIteratorを取得。
- Iteratorインタフェースのメソッドを利用して、コレクションに対する反復処理を実行。
Clientは、コレクションの格納方式( 配列
リスト
など)を関知せずに、Iteratorインタフェースで提供されている共通した操作でコレクションを操作できるようになります。
State
( 条件文を無くして状態ごとにクラスを作成 )
@startuml
title State
class Context {
- State state
--
+ setState()
+ eventA()
+ eventB()
+ eventC()
+ eventD()
}
interface State {
--
+ eventA()
+ eventB()
+ eventC()
+ eventD()
}
class StateA {
--
+ eventA()
+ eventB()
+ eventC()
+ eventD()
}
class StateB {
--
+ eventA()
+ eventB()
+ eventC()
+ eventD()
}
class StateC {
--
+ eventA()
+ eventB()
+ eventC()
+ eventD()
}
Context -> State
State <|.. StateA
State <|.. StateB
State <|.. StateC
@enduml
Stateパターンでは状態ごとにクラスを作成します。下記のように条件文が多くなるのを防ぎます。
public function eventA() {
if (状態Aである) {
// 処理A
} else if (状態Bである) {
// 処理B
} else if (状態Cである) {
// 処理C
}
}
public function eventB() {
if (状態Aである) {
// 処理A
} else if (状態Bである) {
// 処理B
} else if (状態Cである) {
// 処理C
}
}
各状態クラスに遷移処理が記述されるので、後から見たとき、全体的な状態遷移の流れが把握しづらくなります。状態遷移図
を別途作成しておくことをおすすめします。
また、イベント数が増えるほど、各状態クラスでの実装が面倒な部分があります。以下のケースを考えます。
例えばStateC
ではeventD
しか発生しません。
各状態クラスで全てのイベントを実装するのは面倒なので、デフォルトをエラー処理にして、必要なeventのみをオーバーライドするという対応も考えられます。