Edited at

C#で学ぶデザインパターン入門 ⑫Decorator Pattern

More than 1 year has passed since last update.


概要

@hyuki 先生著の『Javaで学ぶデザインパターン入門』(2004年、SB Creative)の1章ずつをベースに、サンプルコードをC#で置き換えながら勉強していく記事です。

※著者の @hyuki 先生には適切に書籍への参照を入れれば問題ない旨ご確認いただいています。


本題

Decoratorパターン

第12回はDecoratorパターンです。Decoratorパターンは「外枠(飾り、Decoration)を再帰的に追加することで核となる部分の機能を追加していく」デザインパターンです。


サンプルコード

早速具体的な事例を見てみましょう。『Javaで学ぶデザインパターン入門』(2004年、SB Creative)に掲載されているコードをC#で(大体)書き換えます。


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace DecoratorPattern
{
class Program
{
static void Main(string[] args)
{
Display b1 = new StringDisplay("Hello, world.");
Display b2 = new SideBorder(b1, '#');
Display b3 = new FullBorder(b2);
b1.Show();
       // => Hello, world.
b2.Show();
// => #Hello, world.#
b3.Show();
// =>
// +---------------+
// |#Hello, world.#|
// +---------------+
Display b4 =
new SideBorder(
new FullBorder(
new SideBorder(
new FullBorder(
new StringDisplay("こんにちは。")
),
'*')
),
'/'
);
b4.Show();
// =>
// /+----------------+/
// /|*+------------+*|/
// /|*|こんにちは。|*|/
// /|*+------------+*|/
// /+----------------+/

// 実行が一瞬で終わって確認できないので、キーの入力を待ちます
Console.ReadLine();
}
}

// Component
// ・機能を追加するときの核
public abstract class Display
{
public abstract int Columns { get; }
public abstract int Rows { get; }
public abstract string GetRowText(int row);
public void Show()
{
for(int i = 0; i < Rows; i++)
{
Console.WriteLine(GetRowText(i));
}
}
}

// ConcreteComponent
// ・Componentを実装
public class StringDisplay : Display
{
private string str;
public StringDisplay(string str)
{
this.str = str;
}
public override int Columns
{
get
{
Encoding sjisEnc = Encoding.GetEncoding("shift_jis");
return sjisEnc.GetByteCount(str);
}
}

public override int Rows
{
get { return 1; }
}

public override string GetRowText(int row)
{
if (row == 0)
{
return str;
}
else
{
return null;
}
}
}

// Decorator
// ・Componentと同じインターフェース(API)を持つ
// ・飾る対象となるComponentを持つ
public abstract class Border : Display
{
protected Display display;
protected Border(Display display)
{
this.display = display;
}
}

// ConcreteDecorator
// ・Decoratorを実装
public class SideBorder : Border
{
private char borderChar;
public SideBorder(Display display, char ch) : base(display)
{
this.borderChar = ch;

}
public override int Columns
{
get
{
return 1 + display.Columns + 1;
}
}

public override int Rows
{
get
{
return display.Rows;
}
}

public override string GetRowText(int row)
{
return borderChar + display.GetRowText(row) + borderChar;
}
}

// ConcreteDecorator
// ・Decoratorを実装
public class FullBorder : Border
{
public FullBorder(Display display) : base(display) { }
public override int Columns
{
get
{
return 1 + display.Columns + 1;
}
}

public override int Rows
{
get
{
return 1 + display.Rows + 1;
}
}

public override string GetRowText(int row)
{
if (row == 0)
{
return "+" + MakeLine('-', display.Columns) + "+";
}
else if (row == display.Rows + 1)
{
return "+" + MakeLine('-', display.Columns) + "+";
}
else
{
return "|" + display.GetRowText(row - 1) + "|";
}
}

private string MakeLine(char ch, int count)
{
StringBuilder sb = new StringBuilder();
for (int i = 0; i < count; i++)
{
sb.Append(ch);
}
return sb.ToString();
}
}
}


効能


  • 包まれるものを変更することなく機能を追加することができる

  • 委譲によりクラス間がゆるく結合しているため、フレームワークのソースを変更することなくオブジェクト間の関係を変えた新しいオブジェクトを作ることができる

  • ConcreteDecoratorを増やせば多様な機能を追加できる


使用上の注意


  • よく似ている小さなクラスができてしまう


関連しているパターン


感想、疑問、メモ


  • 飾り枠を使って中身を隠してもインターフェース(サンプルではColumns、Rows、GetRowText())は隠されていない。この状態を「インターフェース(API)が透過的である」という。

  • javaの標準クラスだとjava.ioというパッケージにDecoratorパターンが採用されている。

  • 機能拡張 = 継承、の前に検討するとよいかもしれません。


C#で学ぶデザインパターン入門

①Iterator

②Adapter

③Template Method

④Factory Method

⑤Singleton

⑥Prototype

⑦Builder

⑧AbstractFactory

⑨Bridge

⑩Strategy Pattern

⑪Composite Pattern

⑫Decorator Pattern