デザインパターンをPlantUMLで確認(振舞いパターン編)

デザインパターンを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
601-desgin-pattern-with-uml-template.png

Template Method を利用することで ConcreteClassA ConcreteClassB 間で処理の手順(アルゴリズム)を共通化することができます。つまり、Template Method をみることで全体の処理の流れを俯瞰することができます。

もし、ConcreteClassC が追加され、 ConcreteClassAConcreteClassC で共通の処理ができてしまったケースを考えます。その場合、「以下のようにフックメソッド( needsMethod )を利用した実装にする」または「Strategyパターン を利用する」といった対応を検討します。

601-desgin-pattern-with-uml-template-hook.png

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
601-desgin-pattern-with-uml-strategy.png

以下のように各クラスがメソッドを実行するケースを考えます。

クラス 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
601-desgin-pattern-with-uml-observer.png

Observerパターンでは、状態に変化があったことを通知する側(Subject)通知される側(Observer) に対して通知を受け取る方法を提供しています。Observerが通知を受け取るまでの処理の流れを例として示します。

  • 登録
    • concreteSubjectA->registerObserver(concreteObserverA)
    • concreteSubjectA->registerObserver(concreteObserverB)
  • concreteSubjectAのstateが更新された
    • concreteSubjectA->notifyObserversが呼び出される
    • notifyObservers内で 登録済みobservers のupdateメソッドを呼び出す

SubjectObserverupdate というメソッドを持っていることだけを知っています。以下のようのことは関知せず、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
601-desgin-pattern-with-uml-command.png

処理の流れの例を示します。

  • 振る舞いを登録
    • ClientAInvokersetCommandメソッド を呼び出し、 CommandA CommandB を登録
    • ClientBInvokersetCommandメソッド を呼び出し、 CommandC を登録
  • 振る舞いを実行
    • 何かしらのタイミングによって、Invokerexecuteメソッド が実行される
      • 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
601-desgin-pattern-with-uml-iterator.png

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
601-desgin-pattern-with-uml-state-1.png

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

各状態クラスに遷移処理が記述されるので、後から見たとき、全体的な状態遷移の流れが把握しづらくなります。状態遷移図 を別途作成しておくことをおすすめします。

また、イベント数が増えるほど、各状態クラスでの実装が面倒な部分があります。以下のケースを考えます。

601-desgin-pattern-with-uml-state-2.png

例えばStateCではeventDしか発生しません。

601-desgin-pattern-with-uml-state-3.png

各状態クラスで全てのイベントを実装するのは面倒なので、デフォルトをエラー処理にして、必要なeventのみをオーバーライドするという対応も考えられます。