創建者模式 | Creator

2024-08-25
GRASP

創建物件的職責誰負責

設計酒駕眼中的創建物件的職責分配,圖片來源

情境

分配職責時絕對要面臨一個問題,那就是創建物件的職責怎麼分配? 依照資訊專家,建構子一定是物件 A 實現,但誰負責調用並創建該實例呢 ?

此時就有聰明的同學舉手了,很簡單呀 ! 創建物件不就交給工廠類別負責創建嗎 ? 在解決創建物件的同時,也不會提升系統的耦合了。 如果真的像這位聰明的同學說的一樣,那這篇文章到這邊就可以結束了, 但後面還有內容,因此繼續看吧 !

如果都用工廠模式,就會看到工廠多到滿出來,造成領域概念是工業區的錯覺, GRASP 的創建者就是解決創建物件這個職責而提出的通用性法則。


創建者是什麼 ?

“ 有一些通用的原則以用於創建職責的分配。如果分配的好,設計就能夠支持低耦合,提高清晰度、封裝性、可複用性。 ”

― Craig Larman, Applying UML and Patterns 3rd, ch17.10

面對分配創建職責這個問題,可以透過以下關係去分配
  1. B 包含 A
  2. B 紀錄 A
  3. B 直接使用 A
  4. B 具有 A 的初始化資訊,並且創建時會傳遞給 A

而如果有超過一個以上的候選人,通常以 B 包含 A 作為首選。


創建者案例

talk is cheap, show me the code,所以看看例子吧。

B 包含 A

實現新增書籍用例時,因為 Library 包含著 Book,所以可以讓創建 Book 的職責交付給 Library。

csharp
📝copy
                               
public class Library
{
    private List books = new List();

    public void AddBook(string title, string author)
    {
        Book newBook = new Book(title, author);  // Library 創建 Book
        books.Add(newBook);
    }
}

public class Book
{
    public string Title { get; private set;}
    public string Author { get; private set;}

    public Book(string title, string author)
    {
        Title = title;
        Author = author;
    }
}         
                                 
                            

B 紀錄 A

實現記錄書籍修改用例時,因為 BookLog 紀錄著每個 Book 的修改紀錄,所以 BookLog 可以創建 Book。

csharp
📝copy
                               
public class BookLog
{
    private List _logs = new List();

    public void Record(string title, string author, string action)
    {
        Book newBook = new Book(title, author);
        _logs.Add(newBook.GetBookIdentifier() + "_" + action);
    }
}

public class Book
{
    public string Title { get; private set; }
    public string Author { get; private set; }
    
    public Book(string title, string author)
    {
        Title = title;
        Author = author;
    }

    public string GetBookIdentifier()
    {
        return $"{this.Author}_{this.Title}";
    }
}         
                                 
                            

B 直接使用 A

實現呈現書本內容用例時,BookDisplay 在呈現書本內容時需要直接使用 Book 類別,所以 BookDisplay 可以創建 Book ,而 IRepository 負責從資料庫獲取到書本內容。

csharp
📝copy
                               
public interface IRepository 
{
    public string GetBookContent(string title, string author);
}

public class Book
{
    public string Title { get; private set; }
    public string Author { get; private set; }
    public string Content { get; private set; }

    public Book(string title, string author, string content)
    {
        Title = title;
        Author = author;
        Content = content;
    }
}

public class BookDisplay
{
    private ConsoleColor _foregroundColor;

    private IRepository _bookRepository;

    public BookDisplay(IRepository repository, ConsoleColor fontColor)
    {
        _bookRepository = repository;
        _foregroundColor = fontColor;
    }

    public void DisplayBook(string title, string author)
    {
        string content = _bookRepository.GetBookContent(title, author);
        Book book = new Book(title, author, content);  // BookDisplay 創建並直接使用 Book
        Console.ForegroundColor = _foregroundColor;
        Console.WriteLine(book.Content);
    }
}        
                                 
                            

B 具有 A 的初始化資訊,並且創建時會傳遞給 A

實現出版社出版某書用例時,BookPublisher 具有 Book 的初始化資訊,因此可以將 Book 的創建職責給 BookPublisher。

csharp
📝copy
                               
public class BookPublisher
{
    private string _publisher;

    public BookPublisher(string publisher)
    {
        this._publisher = publisher;
    }

    public Book CreateBook(string title, string author)
    {
        return new Book(title, author, _publisher);  // BookFactory 創建 Book 並傳遞初始化資訊
    }
}

public class Book
{
    public string Title { get; private set; }
    public string Author { get; private set; }
    public string Publisher { get; private set; }

    public Book(string title, string author, string publisher)
    {
        Title = title;
        Author = author;
        Publisher = publisher;
    }
}      
                                 
                            

創建者與工廠

我全都要

我全都要,圖片來源

經過以上的概念講解與案例解釋,不知道心中是否冒出一個疑問,工廠模式、抽象工廠模式難道都是小丑嗎? 都不必要嗎?

其實創建者與工廠模式都是面對哪個物件有職責創建物件 A 的解決方案,只是面對創建物件的複雜程度有不同的選擇, 基本上來講創建者負責處理簡單的創建工廠模式負責複雜的創建邏輯。 可以看看以下例子。

csharp
📝copy
                           
// 簡單創建者範例
public class Order
{
    public string ProductName { get; private set; }
    public int Quantity { get; private set; }

    // 創建者負責簡單的創建
    public Order(string productName, int quantity)
    {
        ProductName = productName;
        Quantity = quantity;
    }
}

public class Customer
{
    public string Name { get; private set; }

    public Customer(string name)
    {
        Name = name;
    }

    // Customer 創建 Order,簡單的創建邏輯由創建者負責
    public Order CreateOrder(string productName, int quantity)
    {
        return new Order(productName, quantity);
    }
}

// 複雜創建邏輯使用工廠模式
public class OrderFactory
{
    public static Order CreateOrder(string productName, int quantity, string discountCode, bool isPriority)
    {
        // 複雜的創建邏輯,例如根據折扣碼和優先級來決定不同的創建過程
        if (isPriority)
        {
            // 處理優先訂單邏輯
        }

        if (!string.IsNullOrEmpty(discountCode))
        {
            // 根據折扣碼應用折扣
        }

        return new Order(productName, quantity);
    }
}  
                             
                        
而複雜通常來自於
  1. 驗證邏輯
  2. 物件需要客製化
  3. 物件依賴外部資源
  4. 多步驟的創建邏輯
  5. 異常處理
創建者與工廠這兩個概念並非互斥的,而是用來解決在不同情境下的創建物件的職責。

作者心得

就像上一篇提到的剃刀原則,在很多時候,物件的創建不需要用到工廠模式, 而是可以利用類別間以存在的關係進行職責的分配,因為已經存在關係,所以不會增加類別間的耦合性的同時又完成了職責的分配。

在寫下這篇文章的時候,咒術回戰只剩下 4 話了,雖然從 236 話之後就抱著樂子人的心態看,但看到要完結了還是莫名感慨。


參考資料

也可以看看以下文章