【Design Pattern】建造者模式

原本以為建造者模式應該很快就可以寫完一篇,但實際在理解的過程中,看了不少篇文章的介紹,反而越看越迷惑,這篇阿猩要介紹建造者模式,除了使用方式之外,也包含一些阿猩自己對建造者模式的想法,如有不對的想法,歡迎各位提出。

情境說明


為何要使用建造者模式
看到許多文章的介紹,部分文章提到建造者模式,要解決的問題是建構子的多樣性。

舉個例子來看,阿猩本身會彈吉他,假設要寫一個紀錄吉他型號的功能,吉他的Model物件可能會長得像是

class Guitar
{
    public string Brand { get; set; }        //品牌
    public string Model { get; set; }        //款式
    public string BodyStyle { get; set; }    //Body
    public string Year { get; set; }         //年份
    public string Color { get; set; }        //顏色
    public string NeckPickUp { get; set; }   //NeckPickUp
    public string MiddlePickUp { get; set; } //MiddlePickUp 
    public string BridgePickUp { get; set; } //BridgePickUp 

    public void DisplayInfo() 
    {
        Console.WriteLine($"品牌: {Brand}");
        Console.WriteLine($"型號: {Model}");
        Console.WriteLine($"款式: {BodyStyle}");
        Console.WriteLine($"年份: {Year}");
        Console.WriteLine($"顏色: {Color}");
        Console.WriteLine($"Neck拾音器: {NeckPickUp}");
        Console.WriteLine($"Middle拾音器: {MiddlePickUp}");
        Console.WriteLine($"Bridge拾音器: {BridgePickUp}");

        Console.ReadLine();
    }
}

 

當吉他越多有著越多的Property,可製造吉他的種類變多,就會衍伸出各式各樣的建構子,例如有些吉他擁有兩顆拾音器,有些則是3顆拾音器。

Amazon.com: Fender Player Stratocaster 電吉他楓木指板黑色: 樂器
Gibson|Les Paul Classic電吉他|弦宏樂器買電吉他就送你五年保固| 弦宏樂器

 

建構子會越來越多種,例如

class Guitar
{
    public string _brand;
    public string _model;
    public string _bodyStyle;
    public string _neckPickUp;
    public string _middlePickUp;
    public string _bridgePickUp;

    public Guitar(string brand, string model, string bodyStyle, string neckPickUp, string middlePickUp  string bridgePickUp)
    {
        _brand = brand;
        _model = model;
        _bodyStyle = bodyStyle;
        _neckPickUp = neckPickUp;
        _middlePickUp = middlePickUp; 
        _bridgePickUp = bridgePickUp;
    }

    public Guitar(string brand, string model, string bodyStyle, string NeckPickUp, string BridgePickUp)
    {
        _brand = brand;
        _model = model;
        _bodyStyle = bodyStyle;
        _neckPickUp = neckPickUp;
        _bridgePickUp = bridgePickUp;
    }
}

 

主程式 新增一把吉他
使用建構子的呼叫方式可能會像是

class Program
{
    static void Main(string[] args)
    {
        var fender = new Guitar("Fender", "PLAYER PLUS", "Straocaster", "Black", "Texas Special", "Texas Special", "Texas Special");
        fender.DisplayInfo();
        
        var gibson = new Guitar("Gibson", "R9", "Les Paul", "Red", "SH-1", "SH-4");
        gibson.DisplayInfo();
    }
}

 

 

使用Builder Pattern


新增Interface,定義建立吉他時,可以做到的事情
例如可設定model名稱、設定neck、middle、bridge的拾音器等。
 

// 定義吉他builder可Set的Property
interface IGuitarBuilder
{
    void SetModel(string model);
    void SetBodyStyle(string bodyStyle);
    void SetYear(string year);
    void SetColor(string color);
    void SetNeckPickUp(string pickUp);
    void SetMiddlePickUp(string pickUp);
    void SetBridgePickUp(string pickUp);
    Guitar GetGuitar();
}

 

實作Builder 

class FenderBuilder : IGuitarBuilder
{
    private Guitar guitar;

    public FenderBuilder()
    {
        guitar = new Guitar() { Brand = "Fender" };
    }

    public Guitar GetGuitar()
    {
        return guitar;
    }

    public void SetBodyStyle(string bodyStyle)
    {
       guitar.BodyStyle = bodyStyle;
    }
     
    public void SetYear(string year)
    {
       guitar.Year = year;
    }

    public void SetColor(string color)
    {
       guitar.Color = color;
    }

    public void SetModel(string model)
    {
       guitar.Model = model;
    }

    public void SetNeckPickUp(string pickUp)
    {
       guitar.NeckPickUp = pickUp;
    }

    public void SetMiddlePickUp(string pickUp)
    {
       guitar.MiddlePickUp = pickUp;
    }

    public void SetBridgePickUp(string pickUp)
    {
       guitar.BridgePickUp = pickUp;
    }
}

 

 

主程式 使用建造者模式新增一把吉他
使用Builder Pattern時,主程式會像是

class Program
{
    static void Main(string[] args)
    {
        var fenderBuilder = new FenderBuilder();
        fenderBuilder.SetBodyStyle("Stratocaster");
        fenderBuilder.SetModel("Custom Shop 1960 Stratocaster");
        fenderBuilder.SetYear("2023");
        fenderBuilder.SetColor("3-Colour Sunburst");
        fenderBuilder.SetNeckPickUp("HandWound Strat TEX");
        fenderBuilder.SetMiddlePickUp("HandWound Strat TEX");
        fenderBuilder.SetBridgePickUp("HandWound Strat TEX");
        
        var fender_CustomShop1960 = fenderBuilder.GetGuitar();
        fender_CustomShop1960.DisplayInfo();
    }
}

 

使用建造者模式後,建立吉他物件,不用建立許多混亂的建構子,
使用者可自行調整需要的欄位,不受順序的影響最後再呼叫GetGuitar()
 

使用Direcotr
阿猩看到很多篇文章都有提到Director,Director是把執行邏輯包裝起來,像是,

class Director
{

    public static Guitar Fender_Thinline72_Director 
    (
        IGuitarBuilder builder, 
        string BodyStyle, 
        string Model, 
        string Year, 
        string Color, 
        string NeckPickUp, 
        string MiddlePickUp, 
        string BridgePickUp
    )
    {
        builder.SetBodyStyle(BodyStyle);
        builder.SetModel(Model);
        builder.SetYear(Year);
        builder.SetColor(Color);
        builder.SetNeckPickUp(NeckPickUp);
        builder.SetBridgePickUp(BridgePickUp);
            
        return builder.GetGuitar();
    }
}  

 

阿猩的困惑
阿猩在這個階段花了很多時間釐清自己的思緒,一開始越看越困惑。
花了很多時間自問自答,然後試著釐清思緒。

使用建構子   => Ex fender = new Guitar("Tele", "Thinline 72", "3-Colour Sunburst");
好處              => 可限制建立此物件必填欄位
混亂的原因   => 建構子應該只能放「無論任何情況都會存在的欄位」,如果把非必要的欄位放到建構子,數量一多就會產生混亂。

Builder Patthen => 許多網路的範例,共用Method都只有SetProperty,但阿猩的想法是,使用建構子的時機,比較像是不讓外部對Object做Set,只能透過建構子建立物件,同時又可以限制必填欄位,不然等於換地方寫Code。

// Builder Pattern
var fenderBuilder = new FenderBuilder();
fenderBuilder.SetBodyStyle("Stratocaster");
fenderBuilder.SetModel("Custom Shop 1960 Stratocaster");
…   
var fender_CustomShop1960 = fenderBuilder.GetGuitar();
fender_CustomShop1960.DisplayInfo();

// 建構子寫法
var fender = new Guitar();
fender.BodyStyle = "Stratocaster";
fender.Model = "Custom Shop 1960 Stratocaster";
…
fender.DisplayInfo();

建立IBuilder => 已確定會有多種Builder,且抽出共用Method,沒有多種Builder的話,IBuilder就沒意義。
實作Builder  => 實作每個Builder的共用Method。
建立Director => 將主程式的包起來,直接後續使用者不須知道建立Custom Shop 1960 Stratocaster需要設定哪些Property。

 

 

嘗試使用建造者模式,撰寫.NET Program.cs


新增WebAPI專案,.NET 的 Startup.cs 或 Program.cs,內容會像是
 

 

上圖的程式碼做到了  

  1. Createbuilder
  2. 註冊服務,跟順序無關,讓使用者自行加入專案需要的服務
  3. 最後執行builder.Build(); 
     

.NET使用的概念就是建造者模式,所以阿猩試著參考Program.cs內容,模仿並做出一個Program.cs

建立Builder
建造者模式要先抽出IBuilder,定義出抽象可做到的事情。開發過.NET一陣子的話,不難發現,WebApplication.CreateBuilder回傳的型別是WebApplicationBuilder,可get取得各式的Web應用程式及服務,像是Environment、Services、Configuration、Host、Logging等等。

#region 組件 Microsoft.AspNetCore, Version=6.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60
// Microsoft.AspNetCore.dll
#endregion

#nullable enable


namespace Microsoft.AspNetCore.Builder
{
    //
    // 摘要:
    //     A builder for web applications and services.
    public sealed class WebApplicationBuilder
    {
        //
        // 摘要:
        //     Provides information about the web hosting environment an application is running.
        public IWebHostEnvironment Environment { get; }
        //
        // 摘要:
        //     A collection of services for the application to compose. This is useful for adding
        //     user provided or framework provided services.
        public IServiceCollection Services { get; }
        //
        // 摘要:
        //     A collection of configuration providers for the application to compose. This
        //     is useful for adding new configuration sources and providers.
        public ConfigurationManager Configuration { get; }
        //
        // 摘要:
        //     A collection of logging providers for the application to compose. This is useful
        //     for adding new logging providers.
        public ILoggingBuilder Logging { get; }
        //
        // 摘要:
        //     An Microsoft.AspNetCore.Hosting.IWebHostBuilder for configuring server specific
        //     properties, but not building. To build after configuration, call Microsoft.AspNetCore.Builder.WebApplicationBuilder.Build.
        public ConfigureWebHostBuilder WebHost { get; }
        //
        // 摘要:
        //     An Microsoft.Extensions.Hosting.IHostBuilder for configuring host specific properties,
        //     but not building. To build after configuration, call Microsoft.AspNetCore.Builder.WebApplicationBuilder.Build.
        public ConfigureHostBuilder Host { get; }

        //
        // 摘要:
        //     Builds the Microsoft.AspNetCore.Builder.WebApplication.
        //
        // 傳回:
        //     A configured Microsoft.AspNetCore.Builder.WebApplication.
        public WebApplication Build();
    }
}

 

1_新增WebApplication
WebApplicationBuilder可讓MVC、WebAPI等框架使用,也可讓使用者自行註冊需要的服務,最後再使用public WebApplication Build(); 建立整個WebApplication所需的服務。阿猩也建立一個WebApplication,並回傳Builder,並仿照.NET 設為static。

class WebApplication
{
    public static BaseBuilder CreateBuilder()
    {
        return new BaseBuilder();
    }
}



2_新增BaseBuilder
BaseBuilder包含上述的Service、Logging等,為了儲存開發者註冊的服務,阿猩建立一個webApplicationCollection。

namespace BuilderPattern.Builder
{
    public class BaseBuilder
    {
       private List<string> webApplicationCollection;
        public List<string> Environment { get; } = new List<string>();
        public List<string> Services { get; } = new List<string>();
        public List<string> Configuration { get; } = new List<string>();
        public List<string> Logging { get; } = new List<string>();
        public List<string> WebHost { get; } = new List<string>();
        public List<string> Host { get; } = new List<string>();

        public BaseBuilder()
        {
           webApplicationCollection = new List<string>();
        }
    }
}

 

3_BaseBuilder.cs 加入 public void Build()
.NET原本執行Build()後,要完成一堆建置服務的工作,阿猩這裡只是想親身體驗,用建造者模式揣摩.NET開發者的想法,所以Build()中只是簡單印出註冊了哪些服務。

public void Build()
{
    webApplicationCollection.AddRange(Environment);
    webApplicationCollection.AddRange(Services);
    webApplicationCollection.AddRange(Configuration);
    webApplicationCollection.AddRange(Logging);
    webApplicationCollection.AddRange(WebHost);
    webApplicationCollection.AddRange(Host);

    foreach (var item in webApplicationCollection)
    {
        Console.WriteLine(item);
        Console.WriteLine("==============================");
    }

    Console.WriteLine("\nbuild finish");
    Console.ReadLine();
}

 

5_撰寫註冊服務Extension
.NET 對WebApplication寫了許多服務的擴充,阿猩只寫了幾個有代表性的服務,包含AddDbContext、AddScoped、AddAuthentication、AddAuthorization、AddSwaggerGen

public static class BuilderExtension
{
    public static void AddDbContext<TContext>(this List<string> serviceCollection, string connStr) where TContext : class
    {
        string msg = $"{typeof(TContext).ToString()} 連線字串設定為 {connStr}";
        serviceCollection.Add(msg);
    }

    public static void AddScoped<ITService, TService>(this List<string> serviceCollection) 
        where ITService : class
        where TService : class
    {
        string msg = $"{typeof(ITService).ToString()} 服務 已註冊,實作為 {typeof(TService).ToString()}";
        serviceCollection.Add(msg);
    }
 
    public static void AddAuthentication(this List<string> serviceCollection)
    {
        string msg = $"已註冊 認證 服務";
        serviceCollection.Add(msg);
    }

    public static void AddAuthorization(this List<string> serviceCollection)
    {
        string msg = $"已註冊 授權 服務";
        serviceCollection.Add(msg);
    }

    public static void AddSwaggerGen(this List<string> serviceCollection)
    {
        string msg = $"已註冊 Swagger 服務";
        serviceCollection.Add(msg);
    }
}

 

6_Program.cs
按照原本.NET的使用模式,依序執行CreateBuilder()、註冊服務、Build()。

class Program
{
    static void Main(string[] args)
    {
        var builder = WebApplication.CreateBuilder();

        string connStr = "Data Source=資料庫來源;Initial Catalog=資料庫名稱;User ID=帳號;Password=密碼; providerName = System.Data.SqlClient";

        builder.Services.AddDbContext<NorthwindContext>(connStr);
        builder.Services.AddScoped<DbContext, NorthwindContext>();

        builder.Services.AddAuthentication();
        builder.Services.AddAuthorization();

        builder.Services.AddSwaggerGen();

        builder.Build();
    }
}

 

7_執行結果