原本以為建造者模式應該很快就可以寫完一篇,但實際在理解的過程中,看了不少篇文章的介紹,反而越看越迷惑,這篇阿猩要介紹建造者模式,除了使用方式之外,也包含一些阿猩自己對建造者模式的想法,如有不對的想法,歡迎各位提出。
情境說明
為何要使用建造者模式
看到許多文章的介紹,部分文章提到建造者模式,要解決的問題是建構子的多樣性。
舉個例子來看,阿猩本身會彈吉他,假設要寫一個紀錄吉他型號的功能,吉他的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顆拾音器。
建構子會越來越多種,例如
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,內容會像是
上圖的程式碼做到了
- Createbuilder
- 註冊服務,跟順序無關,讓使用者自行加入專案需要的服務
- 最後執行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_執行結果