2008-11-24 23 views
17

Dưới đây là mã tôi đã thử, có cách nào tốt hơn để thực hiện việc này không?Làm cách nào để triển khai và mở rộng mẫu xây dựng của Joshua trong .net?

public class NutritionFacts 
    { 
     public static NutritionFacts.Builder Build(string name, int servingSize, int servingsPerContainer) 
     { 
      return new NutritionFacts.Builder(name, servingSize, servingsPerContainer); 
     } 

     public sealed class Builder 
     { 
      public Builder(String name, int servingSize, 
      int servingsPerContainer) 
      { 
      } 
      public Builder totalFat(int val) { } 
      public Builder saturatedFat(int val) { } 
      public Builder transFat(int val) { } 
      public Builder cholesterol(int val) { } 
      //... 15 more setters 
      public NutritionFacts build() 
      { 
       return new NutritionFacts(this); 
      } 
     } 
     private NutritionFacts(Builder builder) { } 
     protected NutritionFacts() { } 
    } 
  • Làm thế nào để chúng tôi mở rộng một lớp học như vậy? Do , chúng ta cần viết các trình tạo riêng biệt cho mỗi lớp học bắt buộc ?

    public class MoreNutritionFacts : NutritionFacts 
    { 
        public new static MoreNutritionFacts.Builder Build(string name, int servingSize, int servingsPerContainer) 
        { 
         return new MoreNutritionFacts.Builder(name, servingSize, servingsPerContainer); 
        } 
        public new sealed class Builder 
        { 
         public Builder(String name, int servingSize, 
         int servingsPerContainer) {} 
         public Builder totalFat(int val) { } 
         public Builder saturatedFat(int val) { } 
         public Builder transFat(int val) { } 
         public Builder cholesterol(int val) { } 
         //... 15 more setters 
         public Builder newProperty(int val) { } 
         public MoreNutritionFacts build() 
         { 
          return new MoreNutritionFacts(this); 
         } 
        } 
        private MoreNutritionFacts(MoreNutritionFacts.Builder builder) { } 
    } 
    

Trả lời

21

Trong Nghị định thư Buffers, chúng tôi thực hiện các mô hình xây dựng như thế này (bao la đơn giản):

public sealed class SomeMessage 
{ 
    public string Name { get; private set; } 
    public int Age { get; private set; } 

    // Can only be called in this class and nested types 
    private SomeMessage() {} 

    public sealed class Builder 
    { 
    private SomeMessage message = new SomeMessage(); 

    public string Name 
    { 
     get { return message.Name; } 
     set { message.Name = value; } 
    } 

    public int Age 
    { 
     get { return message.Age; } 
     set { message.Age = value; } 
    } 

    public SomeMessage Build() 
    { 
     // Check for optional fields etc here 
     SomeMessage ret = message; 
     message = null; // Builder is invalid after this 
     return ret; 
    } 
    } 
} 

Đây là không hoàn toàn giống như các mô hình trong EJ2, nhưng:

  • Không yêu cầu sao chép dữ liệu vào thời gian xây dựng. Nói cách khác, trong khi bạn đang thiết lập các thuộc tính, bạn đang làm như vậy trên đối tượng thực - bạn vẫn chưa thể nhìn thấy nó. Điều này tương tự như những gì StringBuilder thực hiện.
  • Trình tạo không hợp lệ sau khi gọi Build() để đảm bảo tính bất biến. Điều này không may có nghĩa là nó không thể được sử dụng như một loại "nguyên mẫu" theo cách mà phiên bản EJ2 có thể.
  • Chúng tôi sử dụng các thuộc tính thay vì getters và setters, phần lớn - phù hợp với bộ khởi tạo đối tượng của C# 3.
  • Chúng tôi cũng cung cấp các người định cư trả lại this vì lợi ích của người dùng trước C# 3.

Tôi chưa thực sự xem xét kế thừa với mẫu trình tạo - không được hỗ trợ trong Protocol Buffers. Tôi nghi ngờ nó khá phức tạp.

+0

>> "phù hợp với trình khởi tạo đối tượng của C# 3." Trong trường hợp này sẽ được tạo ra như SomeMessage.Build mới (Tên =" GK ", Age = 0).Nhưng nó tốt như viết SomeMessage mới (Tên =" GK ", Age = 0) phải không? Trong trường hợp này làm Chúng ta cần phương pháp xây dựng như thế nào? Nó sẽ giúp chúng ta tránh các nhà xây dựng lồng ghép như thế nào? –

+1

Không, nó sẽ được viết như mới SomeMessage.Builder {Name = "GK", Age = 0} Ghi chú sự khác biệt trong dấu ngoặc vs dấu ngoặc. Lưu ý rằng với C# 4, một hàm tạo duy nhất có rất nhiều tham số tùy chọn sẽ cung cấp một tùy chọn khác –

+0

Ok, bây giờ tôi đã nhận nó mới SomeMessage {Name = "GK", Age = 0 } không thể là các thuộc tính riêng tư, chỉ người xây dựng mới có thể truy cập nó –

4

This blog entry có thể quan tâm

Một biến thể gọn gàng trên mô hình trong C# là việc sử dụng một nhà điều hành diễn viên tiềm ẩn để thực hiện cuộc gọi cuối cùng để xây dựng() không cần thiết:

public class CustomerBuilder 
{ 

    ......  

    public static implicit operator Customer(CustomerBuilder builder) 
    { 
     return builder.Build(); 
    } 
} 
+0

Đó là một mẹo hay. –

+0

Tôi đã sử dụng mã (nhưng hơi sửa đổi) từ bài đăng trên blog trong một số hệ thống sản xuất và cho rằng nó hoạt động rất tốt – Kane

2

Chỉnh sửa: Tôi đã sử dụng lại điều này và đơn giản hóa nó để loại bỏ việc kiểm tra giá trị thừa trong các bộ cài đặt.

Gần đây tôi đã triển khai một phiên bản hoạt động tốt.

Nhà xây dựng là các nhà máy lưu trong bộ nhớ cache phiên bản gần đây nhất. Các nhà xây dựng có nguồn gốc tạo ra các cá thể và xóa bộ nhớ cache khi có bất cứ điều gì thay đổi.

Các lớp cơ sở là đơn giản:

public abstract class Builder<T> : IBuilder<T> 
{ 
    public static implicit operator T(Builder<T> builder) 
    { 
     return builder.Instance; 
    } 

    private T _instance; 

    public bool HasInstance { get; private set; } 

    public T Instance 
    { 
     get 
     { 
      if(!HasInstance) 
      { 
       _instance = CreateInstance(); 

       HasInstance = true; 
      } 

      return _instance; 
     } 
    } 

    protected abstract T CreateInstance(); 

    public void ClearInstance() 
    { 
     _instance = default(T); 

     HasInstance = false; 
    } 
} 

Vấn đề chúng tôi đang giải quyết là tinh tế hơn.Hãy nói rằng chúng ta có khái niệm về một Order:

public class Order 
{ 
    public string ReferenceNumber { get; private set; } 

    public DateTime? ApprovedDateTime { get; private set; } 

    public void Approve() 
    { 
     ApprovedDateTime = DateTime.Now; 
    } 
} 

ReferenceNumber không thay đổi sau khi tạo, vì vậy chúng tôi mô hình nó chỉ đọc qua các nhà xây dựng:

public Order(string referenceNumber) 
{ 
    // ... validate ... 

    ReferenceNumber = referenceNumber; 
} 

Làm thế nào để chúng ta reconstitute một khái niệm Order hiện từ dữ liệu cơ sở dữ liệu?

Đây là gốc của ngắt kết nối ORM: nó có xu hướng buộc người định cư công khai trên ReferenceNumberApprovedDateTime để thuận tiện kỹ thuật. Một sự thật rõ ràng là gì ẩn với người đọc trong tương lai; chúng tôi thậm chí có thể nói đó là một mô hình không chính xác. (Điều này cũng đúng cho các điểm mở rộng: buộc virtual loại bỏ khả năng cho các lớp cơ sở truyền đạt ý định của chúng.)

A Builder với kiến ​​thức đặc biệt là một mẫu hữu ích. Một thay thế cho các loại lồng nhau sẽ là quyền truy cập internal. Nó cho phép khả năng thay đổi, hành vi miền (POCO), và, như một phần thưởng, mẫu "nguyên mẫu" được Jon Skeet đề cập.

Đầu tiên, thêm một constructor internal-Order:

internal Order(string referenceNumber, DateTime? approvedDateTime) 
{ 
    ReferenceNumber = referenceNumber; 
    ApprovedDateTime = approvedDateTime; 
} 

Sau đó, thêm một Builder với đặc tính có thể thay đổi:

public class OrderBuilder : Builder<Order> 
{ 
    private string _referenceNumber; 
    private DateTime? _approvedDateTime; 

    public override Order Create() 
    { 
     return new Order(_referenceNumber, _approvedDateTime); 
    } 

    public string ReferenceNumber 
    { 
     get { return _referenceNumber; } 
     set { SetField(ref _referenceNumber, value); } 
    } 

    public DateTime? ApprovedDateTime 
    { 
     get { return _approvedDateTime; } 
     set { SetField(ref _approvedDateTime, value); } 
    } 
} 

Bit thú vị là SetField cuộc gọi. Xác định bởi Builder, nó đóng gói các mô hình của "thiết lập các lĩnh vực sao lưu nếu khác nhau, sau đó xóa dụ" mà nếu không sẽ là trong setters tài sản:

protected bool SetField<TField>(
     ref TField field, 
     TField newValue, 
     IEqualityComparer<T> equalityComparer = null) 
    { 
     equalityComparer = equalityComparer ?? EqualityComparer<TField>.Default; 

     var different = !equalityComparer.Equals(field, newValue); 

     if(different) 
     { 
      field = newValue; 

      ClearInstance(); 
     } 

     return different; 
    } 

Chúng tôi sử dụng ref cho phép chúng ta sửa đổi các lĩnh vực sao lưu. Chúng tôi cũng sử dụng trình so sánh bình đẳng mặc định nhưng cho phép người gọi ghi đè nó.

Cuối cùng, khi chúng ta cần để pha một Order, chúng tôi sử dụng OrderBuilder với diễn viên tiềm ẩn:

Order order = new OrderBuilder 
{ 
    ReferenceNumber = "ABC123", 
    ApprovedDateTime = new DateTime(2008, 11, 25) 
}; 

này đã thực sự dài. Hy vọng nó giúp!

+0

Cảm ơn bạn đã trả lời Bryan. Nó thực sự là một thực hiện rất sạch sẽ của mô hình. Jon, alasdairg và Jaime cũng có một số triển khai khác nhau/khác nhau. Cá nhân tôi thích có phương pháp xây dựng tĩnh trong loại, trả về lớp người xây dựng. –

+0

Tôi vẫn đang cố gắng tìm ra cách mở rộng lớp xây dựng như vậy. Nếu chúng ta có một SpecialOrder (tên xấu !!), thì chúng ta phải đi đến SpecialOrderBuilder nhưng trong trường hợp đó người dùng (một số người đang sử dụng mã của bạn) nên biết các trình tạo dựng của mỗi người. –

+0

Trong ví dụ trên tôi đang lặp lại mã trong MoreNutritionFacts.Builder, cũng không tốt. Có một implemenation sạch/chính xác hơn cho điều này? Như Jon đã nói, nó có vẻ phức tạp. –

0

Lý do để sử dụng mẫu xây dựng của Joshua Bloch là tạo ra một đối tượng phức tạp ngoài các bộ phận và cũng làm cho nó trở nên bất biến.

Trong trường hợp cụ thể này, việc sử dụng các tham số tùy chọn, được đặt tên trong C# 4.0 sẽ rõ ràng hơn. Bạn từ bỏ một số tính linh hoạt trong thiết kế (không đổi tên các tham số), nhưng bạn nhận được mã duy trì tốt hơn, dễ dàng hơn.

Nếu mã NutritionFacts là:

public class NutritionFacts 
    { 
    public int servingSize { get; private set; } 
    public int servings { get; private set; } 
    public int calories { get; private set; } 
    public int fat { get; private set; } 
    public int carbohydrate { get; private set; } 
    public int sodium { get; private set; } 

    public NutritionFacts(int servingSize, int servings, int calories = 0, int fat = 0, int carbohydrate = 0, int sodium = 0) 
    { 
     this.servingSize = servingSize; 
     this.servings = servings; 
     this.calories = calories; 
     this.fat = fat; 
     this.carbohydrate = carbohydrate; 
     this.sodium = sodium; 
    } 
    } 

Sau đó, một khách hàng sẽ sử dụng nó như

NutritionFacts nf2 = new NutritionFacts(240, 2, calories: 100, fat: 40); 

Nếu việc xây dựng phức tạp hơn này sẽ cần phải được tinh chỉnh; nếu "xây dựng" calo nhiều hơn là đặt trong một số nguyên, nó có thể hiểu rằng các đối tượng trợ giúp khác sẽ là cần thiết.

Các vấn đề liên quan