Skip to main content

GoFデザインパターン - 振る舞いパターン Part 2

振る舞いパターンの後半5つを紹介します。これらは特によく使用される重要なパターンを含んでいます。

7. Observer(オブザーバー)

概要

あるオブジェクトの状態が変化したときに、依存するすべてのオブジェクトに自動的に通知されるようにします。発行/購読(Pub/Sub)モデルとも呼ばれます。

使用場面

  • イベント駆動型プログラミング
  • モデルとビューの同期(MVCパターン)
  • ニュース配信、株価更新通知

サンプルコード

// .NETではイベントとデリゲート、またはIObservable<T>/IObserver<T>が標準ですが、
// 基本的な実装パターンを示します。

// Subject(監視される側)
public interface ISubject
{
void Attach(IObserver observer);
void Detach(IObserver observer);
void Notify();
}

// Observer(監視する側)
public interface IObserver
{
void Update(ISubject subject);
}

// Concrete Subject
public class WeatherStation : ISubject
{
private List<IObserver> _observers = new List<IObserver>();
private float _temperature;

public float Temperature
{
get => _temperature;
set
{
_temperature = value;
Notify();
}
}

public void Attach(IObserver observer)
{
_observers.Add(observer);
}

public void Detach(IObserver observer)
{
_observers.Remove(observer);
}

public void Notify()
{
foreach (var observer in _observers)
{
observer.Update(this);
}
}
}

// Concrete Observers
public class PhoneDisplay : IObserver
{
public void Update(ISubject subject)
{
if (subject is WeatherStation weatherStation)
{
Console.WriteLine($"Phone Display: Temperature updated to {weatherStation.Temperature}°C");
}
}
}

public class WindowDisplay : IObserver
{
public void Update(ISubject subject)
{
if (subject is WeatherStation weatherStation)
{
Console.WriteLine($"Window Display: Current temp is {weatherStation.Temperature}°C");
}
}
}

// .NETのイベントを使用したモダンな実装
public class StockMarket
{
// イベント定義
public event EventHandler<StockPriceChangedEventArgs> PriceChanged;

public void UpdatePrice(string symbol, decimal price)
{
// イベント発火
PriceChanged?.Invoke(this, new StockPriceChangedEventArgs(symbol, price));
}
}

public class StockPriceChangedEventArgs : EventArgs
{
public string Symbol { get; }
public decimal Price { get; }

public StockPriceChangedEventArgs(string symbol, decimal price)
{
Symbol = symbol;
Price = price;
}
}

public class Trader
{
private string _name;

public Trader(string name)
{
_name = name;
}

public void OnPriceChanged(object sender, StockPriceChangedEventArgs e)
{
Console.WriteLine($"Trader {_name} notified: {e.Symbol} is now ${e.Price}");
}
}

// 使用例
class Program
{
static void Main()
{
// 基本的なObserverパターン
Console.WriteLine("=== Weather Station ===\n");

var weatherStation = new WeatherStation();
var phone = new PhoneDisplay();
var window = new WindowDisplay();

weatherStation.Attach(phone);
weatherStation.Attach(window);

weatherStation.Temperature = 25.5f;
weatherStation.Temperature = 28.0f;

// .NETイベント
Console.WriteLine("\n=== Stock Market (C# Events) ===\n");

var market = new StockMarket();
var trader1 = new Trader("Alice");
var trader2 = new Trader("Bob");

// イベント購読
market.PriceChanged += trader1.OnPriceChanged;
market.PriceChanged += trader2.OnPriceChanged;

market.UpdatePrice("MSFT", 350.00m);

// 購読解除
market.PriceChanged -= trader2.OnPriceChanged;

market.UpdatePrice("GOOGL", 2800.00m);
}
}

8. State(ステート)

概要

オブジェクトの内部状態が変化したときに、その振る舞いを変更できるようにします。オブジェクトがクラスを変更したかのように見えます。

使用場面

  • オブジェクトの振る舞いが状態に依存する場合
  • 多数の条件分岐(if-else, switch)を排除したい場合
  • 注文ステータス(新規、支払い済み、発送済み、完了)
  • TCP接続状態(Listen, Established, Closed)

サンプルコード

// State Interface
public interface IOrderState
{
void Proceed(Order order);
void Cancel(Order order);
}

// Context
public class Order
{
public IOrderState State { get; set; }

public Order()
{
// 初期状態
State = new NewOrderState();
}

public void Proceed()
{
State.Proceed(this);
}

public void Cancel()
{
State.Cancel(this);
}
}

// Concrete States
public class NewOrderState : IOrderState
{
public void Proceed(Order order)
{
Console.WriteLine("Payment processed. Moving to Paid state.");
order.State = new PaidOrderState();
}

public void Cancel(Order order)
{
Console.WriteLine("Order cancelled.");
order.State = new CancelledOrderState();
}
}

public class PaidOrderState : IOrderState
{
public void Proceed(Order order)
{
Console.WriteLine("Order shipped. Moving to Shipped state.");
order.State = new ShippedOrderState();
}

public void Cancel(Order order)
{
Console.WriteLine("Refunding payment and cancelling order.");
order.State = new CancelledOrderState();
}
}

public class ShippedOrderState : IOrderState
{
public void Proceed(Order order)
{
Console.WriteLine("Order delivered. Moving to Delivered state.");
order.State = new DeliveredOrderState();
}

public void Cancel(Order order)
{
Console.WriteLine("Cannot cancel shipped order. Initiate return process.");
}
}

public class DeliveredOrderState : IOrderState
{
public void Proceed(Order order)
{
Console.WriteLine("Order is already delivered.");
}

public void Cancel(Order order)
{
Console.WriteLine("Cannot cancel delivered order. Initiate return process.");
}
}

public class CancelledOrderState : IOrderState
{
public void Proceed(Order order)
{
Console.WriteLine("Cannot proceed with cancelled order.");
}

public void Cancel(Order order)
{
Console.WriteLine("Order is already cancelled.");
}
}

// 使用例
class Program
{
static void Main()
{
var order = new Order();

Console.WriteLine("--- Normal Flow ---");
order.Proceed(); // New -> Paid
order.Proceed(); // Paid -> Shipped
order.Proceed(); // Shipped -> Delivered

Console.WriteLine("\n--- Cancellation Flow ---");
var order2 = new Order();
order2.Proceed(); // New -> Paid
order2.Cancel(); // Paid -> Cancelled (Refund)
order2.Proceed(); // Cannot proceed
}
}

9. Strategy(ストラテジー)

概要

アルゴリズムのファミリを定義し、それぞれをカプセル化して交換可能にします。アルゴリズムを使用するクライアントから独立してアルゴリズムを変更できます。

使用場面

  • 複数のアルゴリズムを動的に切り替えたい場合
  • ソートアルゴリズムの選択
  • 圧縮アルゴリズムの選択
  • 割引計算ロジックの切り替え

サンプルコード

// Strategy Interface
public interface ISortStrategy
{
void Sort(List<string> list);
}

// Concrete Strategies
public class QuickSortStrategy : ISortStrategy
{
public void Sort(List<string> list)
{
list.Sort(); // List<T>.Sort uses QuickSort internally
Console.WriteLine("Sorted using QuickSort");
}
}

public class BubbleSortStrategy : ISortStrategy
{
public void Sort(List<string> list)
{
// バブルソートの実装(簡略化)
Console.WriteLine("Sorted using BubbleSort");
}
}

// Context
public class SortedList
{
private List<string> _list = new List<string>();
private ISortStrategy _sortStrategy;

public void SetSortStrategy(ISortStrategy sortStrategy)
{
_sortStrategy = sortStrategy;
}

public void Add(string name)
{
_list.Add(name);
}

public void Sort()
{
_sortStrategy.Sort(_list);
foreach (var name in _list)
{
Console.WriteLine($" {name}");
}
}
}

// より実践的な例:割引計算
public interface IDiscountStrategy
{
decimal ApplyDiscount(decimal totalAmount);
}

public class NoDiscountStrategy : IDiscountStrategy
{
public decimal ApplyDiscount(decimal totalAmount) => totalAmount;
}

public class PercentageDiscountStrategy : IDiscountStrategy
{
private decimal _percentage;
public PercentageDiscountStrategy(decimal percentage) => _percentage = percentage;

public decimal ApplyDiscount(decimal totalAmount) => totalAmount * (1 - _percentage);
}

public class FlatRateDiscountStrategy : IDiscountStrategy
{
private decimal _amount;
public FlatRateDiscountStrategy(decimal amount) => _amount = amount;

public decimal ApplyDiscount(decimal totalAmount) => totalAmount - _amount;
}

public class ShoppingCart
{
private IDiscountStrategy _discountStrategy;
private decimal _totalAmount;

public ShoppingCart(IDiscountStrategy discountStrategy)
{
_discountStrategy = discountStrategy;
}

public void SetAmount(decimal amount) => _totalAmount = amount;

public decimal CalculateTotal()
{
return _discountStrategy.ApplyDiscount(_totalAmount);
}
}

// 使用例
class Program
{
static void Main()
{
// ソート戦略の例
Console.WriteLine("=== Sorting Strategy ===\n");

var studentRecords = new SortedList();
studentRecords.Add("Samual");
studentRecords.Add("Jimmy");
studentRecords.Add("Sandra");

studentRecords.SetSortStrategy(new QuickSortStrategy());
studentRecords.Sort();

studentRecords.SetSortStrategy(new BubbleSortStrategy());
studentRecords.Sort();

// 割引戦略の例
Console.WriteLine("\n=== Discount Strategy ===\n");

var cart = new ShoppingCart(new NoDiscountStrategy());
cart.SetAmount(10000m);
Console.WriteLine($"Regular price: {cart.CalculateTotal()}");

cart = new ShoppingCart(new PercentageDiscountStrategy(0.1m)); // 10% off
cart.SetAmount(10000m);
Console.WriteLine($"10% off price: {cart.CalculateTotal()}");

cart = new ShoppingCart(new FlatRateDiscountStrategy(500m)); // 500 yen off
cart.SetAmount(10000m);
Console.WriteLine($"500 yen off price: {cart.CalculateTotal()}");
}
}

10. Template Method(テンプレートメソッド)

概要

操作におけるアルゴリズムの骨格を定義し、いくつかのステップをサブクラスに延期します。アルゴリズムの構造を変えずに、特定のステップを再定義できます。

使用場面

  • アルゴリズムの共通部分を親クラスにまとめたい場合
  • フレームワークの作成(ライフサイクルメソッドなど)
  • データ処理パイプライン(読み込み→処理→保存)

サンプルコード

// Abstract Class
public abstract class DataMiner
{
// Template Method(アルゴリズムの骨格)
public void Mine(string path)
{
var file = OpenFile(path);
var rawData = ExtractData(file);
var data = ParseData(rawData);
var analysis = AnalyzeData(data);
SendReport(analysis);
CloseFile(file);
}

// 共通の実装
protected virtual object OpenFile(string path)
{
Console.WriteLine($"Opening file: {path}");
return new object();
}

protected virtual void CloseFile(object file)
{
Console.WriteLine("Closing file");
}

protected virtual void SendReport(string analysis)
{
Console.WriteLine($"Sending report: {analysis}");
}

// サブクラスで実装すべき抽象メソッド
protected abstract string ExtractData(object file);
protected abstract string ParseData(string rawData);

// フックメソッド(オーバーライド可能だがデフォルト実装あり)
protected virtual string AnalyzeData(string data)
{
Console.WriteLine("Analyzing data (default algorithm)");
return $"Result for {data}";
}
}

// Concrete Classes
public class PDFDataMiner : DataMiner
{
protected override string ExtractData(object file)
{
Console.WriteLine("Extracting data from PDF");
return "PDF Raw Data";
}

protected override string ParseData(string rawData)
{
Console.WriteLine("Parsing PDF data");
return "Parsed PDF Content";
}
}

public class CSVDataMiner : DataMiner
{
protected override string ExtractData(object file)
{
Console.WriteLine("Extracting data from CSV");
return "CSV Raw Data";
}

protected override string ParseData(string rawData)
{
Console.WriteLine("Parsing CSV data");
return "Parsed CSV Content";
}

// フックをオーバーライド
protected override string AnalyzeData(string data)
{
Console.WriteLine("Analyzing CSV data (specialized algorithm)");
return $"Detailed CSV Analysis: {data}";
}
}

// 使用例
class Program
{
static void Main()
{
Console.WriteLine("=== PDF Mining ===\n");
DataMiner pdfMiner = new PDFDataMiner();
pdfMiner.Mine("document.pdf");

Console.WriteLine("\n=== CSV Mining ===\n");
DataMiner csvMiner = new CSVDataMiner();
csvMiner.Mine("data.csv");
}
}

11. Visitor(ビジター)

概要

オブジェクト構造上の要素に対して実行される操作を表現します。要素のクラスを変更せずに、新しい操作を定義できます。

使用場面

  • 複雑なオブジェクト構造(構文木など)に対して、様々な操作を行いたい場合
  • オブジェクト構造は安定しているが、操作が頻繁に追加される場合
  • コンパイラ(構文解析木に対して、型チェック、コード生成、最適化などを行う)

サンプルコード

// Element Interface
public interface IElement
{
void Accept(IVisitor visitor);
}

// Concrete Elements
public class ConcreteElementA : IElement
{
public string Name { get; } = "Element A";

public void Accept(IVisitor visitor)
{
visitor.VisitConcreteElementA(this);
}

public void OperationA()
{
Console.WriteLine("Operation A logic");
}
}

public class ConcreteElementB : IElement
{
public string Name { get; } = "Element B";
public int Value { get; } = 42;

public void Accept(IVisitor visitor)
{
visitor.VisitConcreteElementB(this);
}

public void OperationB()
{
Console.WriteLine("Operation B logic");
}
}

// Visitor Interface
public interface IVisitor
{
void VisitConcreteElementA(ConcreteElementA element);
void VisitConcreteElementB(ConcreteElementB element);
}

// Concrete Visitors
public class ConcreteVisitor1 : IVisitor
{
public void VisitConcreteElementA(ConcreteElementA element)
{
Console.WriteLine($"{element.Name} visited by Visitor 1");
element.OperationA();
}

public void VisitConcreteElementB(ConcreteElementB element)
{
Console.WriteLine($"{element.Name} visited by Visitor 1. Value: {element.Value}");
element.OperationB();
}
}

public class ConcreteVisitor2 : IVisitor
{
public void VisitConcreteElementA(ConcreteElementA element)
{
Console.WriteLine($"{element.Name} visited by Visitor 2 (Special handling)");
}

public void VisitConcreteElementB(ConcreteElementB element)
{
Console.WriteLine($"{element.Name} visited by Visitor 2 (Special handling)");
}
}

// Object Structure
public class ObjectStructure
{
private List<IElement> _elements = new List<IElement>();

public void Attach(IElement element)
{
_elements.Add(element);
}

public void Detach(IElement element)
{
_elements.Remove(element);
}

public void Accept(IVisitor visitor)
{
foreach (var element in _elements)
{
element.Accept(visitor);
}
}
}

// より実践的な例:ドキュメントのエクスポート
public interface IDocumentElement
{
void Accept(IDocumentVisitor visitor);
}

public class Paragraph : IDocumentElement
{
public string Text { get; set; }
public Paragraph(string text) => Text = text;
public void Accept(IDocumentVisitor visitor) => visitor.VisitParagraph(this);
}

public class Image : IDocumentElement
{
public string Url { get; set; }
public Image(string url) => Url = url;
public void Accept(IDocumentVisitor visitor) => visitor.VisitImage(this);
}

public interface IDocumentVisitor
{
void VisitParagraph(Paragraph paragraph);
void VisitImage(Image image);
}

public class HtmlExportVisitor : IDocumentVisitor
{
public void VisitParagraph(Paragraph paragraph)
{
Console.WriteLine($"<p>{paragraph.Text}</p>");
}

public void VisitImage(Image image)
{
Console.WriteLine($"<img src='{image.Url}' />");
}
}

public class MarkdownExportVisitor : IDocumentVisitor
{
public void VisitParagraph(Paragraph paragraph)
{
Console.WriteLine($"{paragraph.Text}\n");
}

public void VisitImage(Image image)
{
Console.WriteLine($"![]({image.Url})");
}
}

// 使用例
class Program
{
static void Main()
{
// 基本的なVisitor
Console.WriteLine("=== Basic Visitor ===\n");

var structure = new ObjectStructure();
structure.Attach(new ConcreteElementA());
structure.Attach(new ConcreteElementB());

structure.Accept(new ConcreteVisitor1());
Console.WriteLine();
structure.Accept(new ConcreteVisitor2());

// ドキュメントエクスポート
Console.WriteLine("\n=== Document Export ===\n");

var document = new List<IDocumentElement>
{
new Paragraph("Hello World"),
new Image("logo.png"),
new Paragraph("End of document")
};

Console.WriteLine("--- HTML Export ---");
var htmlVisitor = new HtmlExportVisitor();
foreach (var element in document) element.Accept(htmlVisitor);

Console.WriteLine("\n--- Markdown Export ---");
var mdVisitor = new MarkdownExportVisitor();
foreach (var element in document) element.Accept(mdVisitor);
}
}

まとめ

振る舞いパターン(Part 2)の比較:

パターン主な目的使用場面
Observer状態変化の通知イベント処理、MVC
State状態による振る舞いの変更注文フロー、接続状態
Strategyアルゴリズムの切り替えソート、圧縮、割引計算
Template Methodアルゴリズムの骨格定義フレームワーク、データ処理
Visitor構造に対する操作の追加構文木処理、エクスポート

選択のガイドライン

  • 状態変化を他に通知したい → Observer
  • 状態によってクラスを変えたい → State
  • アルゴリズムを交換可能にしたい → Strategy
  • 処理の流れを固定し、一部をカスタマイズしたい → Template Method
  • データ構造と処理を分離したい → Visitor