今回はBuilderパターンを実装をしながら理解していく。
Builderパターンって何?というところからスタートします。
毎度のこと実装例を考えるのが非常に難しい・・・
Builder Pattern
ここは手っ取り早くchatGPTに一度聞いてみる。
→ Builderパターンは複雑なオブジェクトの生成を簡素化し、柔軟性を持たせるための手法です。
Product(製品): ビルダーパターンによって生成されるオブジェクトのインターフェースを定義します。ビルダーによって作成されるオブジェクトはこのインターフェースを実装します。
Builder(ビルダー): Productの具体的な実装を行うインターフェースを定義します。ビルダーはオブジェクトの各部分を生成するためのメソッドを提供します。また、ビルダーはProductを返すbuildメソッドを持っています。
Concrete Builder(具体ビルダー): Builderインターフェースを実装し、具体的なオブジェクトの生成手順を提供します。具体ビルダーはProductのインスタンスを保持し、ビルドプロセスの進行状況を追跡します。
Director(ディレクター): ビルダーの使い方を知っているオブジェクトで、ビルドプロセスを制御します。ディレクターは、複数のビルダーを使用して異なるオブジェクトを構築するために利用されることがあります。
Builderで骨組みを用意してDirectorではbuilderで用意したものを使って、構築していくものでしょうか。
実装
今回は三角形や四角形の面積を求めるところをBuilderパターンを使って実装します。
あまりいい実装例が思い浮かびませんでしたね・・・
設計はこんな感じ
今回は、Builderで面積の計算、形の判定、面積の出力を持ち、Director.Constractでそれらを用いて面積を出力します。
TriagleArea(Concrete Builder) の実装は以下。
#region privateなメンバ
/// <summary>
/// 辺の長さのリスト
/// </summary>
private List<int> m_Sides = new List<int>();
/// <summary>
/// 総面積
/// </summary>
private double m_Area;
#endregion
#region 構築
/// <summary>
/// 三角形を受け取って、辺の長さを格納する
/// </summary>
/// <param name="triangle"></param>
public TriangleAreaBuilder(Triangle triangle)
{
m_Sides.Add(triangle.SideA);
m_Sides.Add(triangle.SideB);
m_Sides.Add(triangle.SideC);
}
#endregion
/// <summary>
/// 三角形の面積を求める
/// </summary>
/// <param name="func"></param>
public void CalcArea(Func<List<int>, double> func)
{
m_Area = func(m_Sides);
}
/// <summary>
/// 三角形の形を判定する
/// </summary>
/// <returns>三角形の面積を求めるメソッドをfuncで返す</returns>
public Func<List<int>, double> JudgeShape()
{
// 三角形かどうかを判定する
if (m_Sides.Max() > m_Sides.Sum() - m_Sides.Max())
{
return null;
}
// 辺の長さがいくつ同じものがあるかを判定する
var sideTotals = m_Sides.GroupBy(x => x).
ToDictionary(side => side.Key, side => side.Count());
// 同じ辺の長さでまとめたDictionaryがの要素数が1つなら正三角形、それ以外は不等辺三角形
switch (sideTotals.Count)
{
case 1:
return EquilateralTriangleCalcArea;
default:
return ScaleneTriangleCalcArea;
}
}
/// <summary>
/// 三角形の総面積を出力する
/// </summary>
/// <returns></returns>
public double OutputArea()
{
return m_Area;
}
ちなみに、EquilateralTriangleCalcAreaは正三角形をScaleneTriangleCalcAreaはそれ以外の三角形の面積を求めるメソッドです。
後はこれをDirectorで使って面積を出力します。
public class Director
{
/// <summary>
/// 骨組みとなるBuilder
/// </summary>
private IBuilder m_builder;
/// <summary>
/// コンストラクタ
/// </summary>
/// <param name="builder"></param>
public Director(IBuilder builder)
{
m_builder = builder;
}
/// <summary>
/// 骨組みとなるBuilderを使って、三角形の面積を求める
/// </summary>
public void Construct()
{
var shapeResult = m_builder.JudgeShape();
if (shapeResult == null)
{
Console.WriteLine("三角形ではありません");
return;
}
m_builder.CalcArea(shapeResult);
var area = m_builder.OutputArea();
Console.WriteLine("面積は{0}です", area);
}
}
こんな感じで、builderで用意したメソッドを使って面積の出力をしました。
メリット
以下がメリットでしょうか。しかし今回の実装例からではあまり見受けられないものもありますね
-
オブジェクトの構築プロセスが分かりやすくなり、クライアントは具体的な生成手順を知らなくてよい
-
複雑なオブジェクトの構築をシンプルにできる。コンストラクタで多くの引数を受け取ったりなどが必要なくなる
chatGPTに聞いたらこんな例を出してくれたので一応載せておきます。
- 柔軟性と拡張性: 新しいオブジェクトの種類を追加する場合や、構築手順を変更する場合に、既存のコードを大幅に変更する必要がなくなります。Builderクラスを適宜拡張・調整するだけで新しいオブジェクトの生成をサポートできます。
具体的な例を挙げると、例えばあるゲームでモンスターを表現するクラスをBuilderパターンで作成しているとします。このゲームに新しいタイプのモンスターを追加したい場合、以下のようなステップが考えられます。
既存のBuilderクラスを拡張する: 既存のBuilderクラスに新しいタイプのモンスターを生成するメソッドを追加します。これにより、既存のモンスター生成コードは影響を受けずに新しいモンスターをサポートできます。
新しいBuilderクラスを作成する: もし既存のBuilderクラスに変更を加えると他の部分に影響を及ぼす場合、新しいBuilderクラスを作成して新しいモンスターの生成を行うメソッドを実装します。既存のコードに影響を与えることなく新しいモンスタータイプをサポートできます。
また、構築手順を変更する場合も同様に対応できます。既存のBuilderクラスのメソッドを調整するか、新しいBuilderクラスを作成することで、異なる構築手順を持つオブジェクトをサポートできます。
終わり
今回はデザインパターンのBuilderパターンを勉強しました。
引き続き別のパターンも勉強していきます。