引子:炒麵沒放鹽#

大鳥與小菜去大排檔吃宵夜。第一盤蛋炒飯味道不夠、雞蛋少;第二盤炒麵竟沒放鹽。同樣是麥當勞、肯德基的漢堡,不論在哪家店、什麼時間吃,味道幾乎一致。

「魚香肉絲」幾乎所有中餐館都有,卻能吃出上萬種口味——因為廚師不同、心情不同、火候不同。

麥當勞、肯德基的成功在於規範的工作流程:原料放多少克、加熱幾分鐘都有嚴格規定。

對應軟體:

  • 我們吃到的菜依賴於廚師(細節)→ 依賴倒轉原則被違反
  • 麥當勞的菜依賴於工作流程(抽象)→ 流程穩定,配料可換
  • 工作流程是抽象的,具體放什麼配料、烤多長時間等細節依賴於這個抽象

場景:畫小人#

要求:用畫筆在 PictureBox 上畫一個小人——必須有頭、身體、兩手、兩腳

第一版:直接寫在 Form 裡#

Pen p = new Pen(Color.Yellow);
Graphics gThin = pictureBox1.CreateGraphics();
gThin.DrawEllipse(p, 50, 20, 30, 30);   // 頭
gThin.DrawRectangle(p, 60, 50, 10, 50); // 身體
gThin.DrawLine(p, 60, 50, 40, 100);     // 左手
gThin.DrawLine(p, 70, 50, 90, 100);     // 右手
gThin.DrawLine(p, 60, 100, 45, 150);    // 左腳
gThin.DrawLine(p, 70, 100, 85, 150);    // 右腳

這就像「炒麵忘記放鹽」:要再畫一個胖小人時,很容易就少畫一條腿

第二版:抽出 Builder 類#

class PersonThinBuilder
{
    private Graphics g;
    private Pen p;
    public PersonThinBuilder(Graphics g, Pen p) { this.g = g; this.p = p; }
    public void Build()
    {
        g.DrawEllipse(p, 50, 20, 30, 30);
        g.DrawRectangle(p, 60, 50, 10, 50);
        g.DrawLine(p, 60, 50, 40, 100);
        g.DrawLine(p, 70, 50, 90, 100);
        g.DrawLine(p, 60, 100, 45, 150);
        g.DrawLine(p, 70, 100, 85, 150);
    }
}

達到了複用,但炒麵忘記放鹽的問題還在:新增一個高個子小人時,仍可能漏畫某個部位。

建造者模式#

建造者模式(Builder Pattern),又稱生成器模式:將一個複雜物件的構建與它的表示分離,使得同樣的構建過程可以創建不同的表示。[DP]

關鍵思想:

  • 建造的「過程」是穩定的——必有頭、身體、兩手、兩腳
  • 建造的「細節」是不同的——胖瘦、高矮各異
  • 用戶只需告訴你他要什麼樣的小人,過程細節不必知道

結構#

  • Builder(抽象建造者):為建立 Product 物件的各部件指定的抽象介面
  • ConcreteBuilder(具體建造者):實作 Builder 介面,構造和裝配各部件
  • Product(產品):被構建的複雜物件
  • Director(指揮者):使用 Builder 介面構建一個物件,將「過程」與用戶隔離
classDiagram
    class PersonDirector {
        -PersonBuilder pb
        +CreatePerson()
    }
    class PersonBuilder {
        <<abstract>>
        +BuildHead()*
        +BuildBody()*
        +BuildArmLeft()*
        +BuildArmRight()*
        +BuildLegLeft()*
        +BuildLegRight()*
    }
    class PersonThinBuilder
    class PersonFatBuilder
    PersonDirector o--> PersonBuilder
    PersonBuilder <|-- PersonThinBuilder
    PersonBuilder <|-- PersonFatBuilder
abstract class PersonBuilder
{
    protected Graphics g;
    protected Pen p;
    public PersonBuilder(Graphics g, Pen p) { this.g = g; this.p = p; }
    public abstract void BuildHead();
    public abstract void BuildBody();
    public abstract void BuildArmLeft();
    public abstract void BuildArmRight();
    public abstract void BuildLegLeft();
    public abstract void BuildLegRight();
}

子類必須覆寫所有抽象方法——少了一個編譯就過不了,就像建造者模式版的「鹽必須放」。

具體建造者:

class PersonThinBuilder : PersonBuilder
{
    public PersonThinBuilder(Graphics g, Pen p) : base(g, p) { }
    public override void BuildHead()     { g.DrawEllipse(p, 50, 20, 30, 30); }
    public override void BuildBody()     { g.DrawRectangle(p, 60, 50, 10, 50); }
    public override void BuildArmLeft()  { g.DrawLine(p, 60, 50, 40, 100); }
    public override void BuildArmRight() { g.DrawLine(p, 70, 50, 90, 100); }
    public override void BuildLegLeft()  { g.DrawLine(p, 60, 100, 45, 150); }
    public override void BuildLegRight() { g.DrawLine(p, 70, 100, 85, 150); }
}

指揮者:

class PersonDirector
{
    private PersonBuilder pb;
    public PersonDirector(PersonBuilder pb) { this.pb = pb; }

    public void CreatePerson()
    {
        pb.BuildHead();
        pb.BuildBody();
        pb.BuildArmLeft();
        pb.BuildArmRight();
        pb.BuildLegLeft();
        pb.BuildLegRight();
    }
}

客戶端:

Pen p = new Pen(Color.Yellow);

PersonThinBuilder ptb = new PersonThinBuilder(pictureBox1.CreateGraphics(), p);
PersonDirector pdThin = new PersonDirector(ptb);
pdThin.CreatePerson();

PersonFatBuilder pfb = new PersonFatBuilder(pictureBox2.CreateGraphics(), p);
PersonDirector pdFat = new PersonDirector(pfb);
pdFat.CreatePerson();

Director 的目的是依用戶選擇來建造小人。建造過程一步一步走,缺一不可——少畫一條腿的問題從根本上消失。

何時使用?#

主要用於創建複雜的物件,這些物件內部構建之間的建造順序通常是穩定的,但內部構建本身面臨複雜的變化

優點:

  • 將建造程式碼與表示程式碼分離
  • 建造者隱藏了產品如何組裝,改變產品內部表示只需新定義一個具體建造者

設計時要把握粒度:Builder 類裡的建造方法必須足夠普遍,能為各種具體建造者所用,不要把每種類型獨有的細節都塞進抽象類。

通用結構#

class Product
{
    IList<string> parts = new List<string>();
    public void Add(string part) => parts.Add(part);
    public void Show() { foreach (var p in parts) Console.WriteLine(p); }
}

abstract class Builder
{
    public abstract void BuildPartA();
    public abstract void BuildPartB();
    public abstract Product GetResult();
}

class Director
{
    public void Construct(Builder builder)
    {
        builder.BuildPartA();
        builder.BuildPartB();
    }
}

客戶端:

Director director = new Director();
Builder b1 = new ConcreteBuilder1();
Builder b2 = new ConcreteBuilder2();

director.Construct(b1);
Product p1 = b1.GetResult();
p1.Show();

director.Construct(b2);
Product p2 = b2.GetResult();
p2.Show();

建造者模式適用於:當創建複雜物件的演算法應該獨立於該物件的組成部分以及它們的裝配方式時。