Có một mẫu để khởi tạo các đối tượng được tạo thông qua một thùng chứa DI


147

Tôi đang cố gắng để Unity quản lý việc tạo các đối tượng của mình và tôi muốn có một số tham số khởi tạo không được biết cho đến thời gian chạy:

Hiện tại, cách duy nhất tôi có thể nghĩ ra cách thực hiện là có một phương thức init trên giao diện.

interface IMyIntf {
  void Initialize(string runTimeParam);
  string RunTimeParam { get; }
}

Sau đó, để sử dụng nó (trong Unity) tôi sẽ làm điều này:

var IMyIntf = unityContainer.Resolve<IMyIntf>();
IMyIntf.Initialize("somevalue");

Trong kịch bản này, runTimeParamparam được xác định tại thời điểm chạy dựa trên đầu vào của người dùng. Trường hợp tầm thường ở đây chỉ đơn giản trả về giá trị củarunTimeParam nhưng trong thực tế, tham số sẽ giống như tên tệp và phương thức khởi tạo sẽ làm một cái gì đó với tệp.

Điều này tạo ra một số vấn đề, cụ thể là Initializephương thức này có sẵn trên giao diện và có thể được gọi nhiều lần. Đặt cờ trong quá trình thực hiện và ném ngoại lệ vào cuộc gọi lặp lạiInitialize có vẻ khó hiểu.

Tại thời điểm tôi giải quyết giao diện của mình, tôi không muốn biết gì về việc triển khai IMyIntf. Tuy nhiên, điều tôi muốn là kiến ​​thức rằng giao diện này cần một số tham số khởi tạo nhất định. Có cách nào để chú thích (thuộc tính?) Giao diện với thông tin này và chuyển chúng vào khung khi đối tượng được tạo không?

Chỉnh sửa: Mô tả giao diện nhiều hơn một chút.


9
Bạn đang thiếu điểm sử dụng DI container. Sự phụ thuộc được cho là sẽ được giải quyết cho bạn.
Pierreten

Bạn lấy thông số cần thiết từ đâu? (tệp cấu hình, db, ??)
Jaime

runTimeParamlà một phụ thuộc được xác định tại thời điểm chạy dựa trên đầu vào của người dùng. Có nên thay thế cho việc này để chia nó thành hai giao diện - một cho khởi tạo và một để lưu trữ các giá trị?
Igor Zevaka

sự phụ thuộc trong IoC, thường đề cập đến sự phụ thuộc vào các lớp hoặc đối tượng loại ref khác có thể được xác định trong giai đoạn khởi tạo IoC. Nếu lớp của bạn chỉ cần một số giá trị để hoạt động, đó là nơi phương thức Khởi tạo () trong lớp của bạn trở nên tiện dụng.
Ánh sáng

Ý tôi là hãy tưởng tượng có 100 lớp trong ứng dụng của bạn mà cách tiếp cận này có thể được áp dụng; sau đó bạn sẽ phải tạo thêm 100 lớp xuất xưởng + 100 giao diện cho các lớp của mình và bạn có thể thoát khỏi nếu bạn chỉ sử dụng phương thức Khởi tạo ().
Ánh sáng

Câu trả lời:


276

Bất cứ nơi nào bạn cần một giá trị thời gian chạy để xây dựng một phụ thuộc cụ thể, Tóm tắt Factory là giải pháp.

Có các phương thức Khởi tạo trên các giao diện có mùi của Trừu tượng Leaky .

Trong trường hợp của bạn, tôi muốn nói rằng bạn nên mô hình hóa IMyIntfgiao diện về cách bạn cần sử dụng nó - chứ không phải cách bạn định tạo ra các triển khai. Đó là một chi tiết thực hiện.

Do đó, giao diện nên đơn giản là:

public interface IMyIntf
{
    string RunTimeParam { get; }
}

Bây giờ hãy xác định Nhà máy Trừu tượng:

public interface IMyIntfFactory
{
    IMyIntf Create(string runTimeParam);
}

Bây giờ bạn có thể tạo một triển khai cụ thể IMyIntfFactoryđể tạo các trường hợp cụ thể IMyIntfnhư thế này:

public class MyIntf : IMyIntf
{
    private readonly string runTimeParam;

    public MyIntf(string runTimeParam)
    {
        if(runTimeParam == null)
        {
            throw new ArgumentNullException("runTimeParam");
        }

        this.runTimeParam = runTimeParam;
    }

    public string RunTimeParam
    {
        get { return this.runTimeParam; }
    }
}

Lưu ý cách điều này cho phép chúng tôi bảo vệ các bất biến của lớp bằng cách sử dụng readonlytừ khóa. Không có phương pháp Khởi tạo có mùi là cần thiết.

Việc IMyIntfFactorythực hiện có thể đơn giản như thế này:

public class MyIntfFactory : IMyIntfFactory
{
    public IMyIntf Create(string runTimeParam)
    {
        return new MyIntf(runTimeParam);
    }
}

Trong tất cả các khách hàng của bạn, nơi bạn cần một IMyIntfcá thể, bạn chỉ cần phụ thuộc vào IMyIntfFactorybằng cách yêu cầu nó thông qua Con Contortor tiêm .

Bất kỳ DI Container nào có giá trị muối của nó sẽ có thể tự động nối dây một IMyIntfFactoryví dụ cho bạn nếu bạn đăng ký chính xác.


13
Vấn đề là một phương thức (như Khởi tạo) là một phần của API của bạn, trong khi đó hàm tạo thì không. blog.ploeh.dk/2011/02/11/InterfacesAreAccessModifier.aspx
Mark Seemann

12
Hơn nữa, một phương thức Khởi tạo chỉ ra Khớp nối tạm thời: blog.ploeh.dk/2011/05/24/DesignSmellTemporalCoupling.aspx
Mark Seemann

2
@Darlene Bạn có thể sử dụng Công cụ trang trí khởi tạo một cách lười biếng, như được mô tả trong phần 8.3.6 của cuốn sách của tôi . Tôi cũng cung cấp một ví dụ về một cái gì đó tương tự trong bài trình bày của tôi Big Object Graphs Up Front .
Mark Seemann

2
@Mark Nếu việc tạo ra MyIntftriển khai của nhà máy đòi hỏi nhiều hơn runTimeParam(đọc: các dịch vụ khác mà một người muốn giải quyết bằng IoC), thì bạn vẫn phải đối mặt với việc giải quyết các phụ thuộc đó trong nhà máy của mình. Tôi thích câu trả lời của @PhilSandler về việc chuyển những phụ thuộc đó vào nhà xây dựng của nhà máy để giải quyết vấn đề này - đó có phải là vấn đề của bạn không?
Jeff

2
Cũng là công cụ tuyệt vời, nhưng câu trả lời của bạn cho câu hỏi khác này thực sự đã đến điểm của tôi.
Jeff

15

Thông thường khi bạn gặp phải tình huống này, bạn cần xem lại thiết kế của mình và xác định xem bạn có đang trộn các đối tượng trạng thái / dữ liệu với các dịch vụ thuần túy của mình không. Trong hầu hết các trường hợp (không phải tất cả), bạn sẽ muốn tách hai loại đối tượng này.

Nếu bạn cần một tham số cụ thể theo ngữ cảnh được truyền trong hàm tạo, một tùy chọn là tạo một nhà máy giải quyết các phụ thuộc dịch vụ của bạn thông qua hàm tạo và lấy tham số thời gian chạy của bạn làm tham số của phương thức Tạo () (hoặc Tạo ( ), Build () hoặc bất cứ điều gì bạn đặt tên cho phương thức xuất xưởng của mình).

Có setters hoặc phương thức Khởi tạo () thường được cho là thiết kế tồi, vì bạn cần "nhớ" để gọi cho họ và đảm bảo rằng họ không mở quá nhiều trạng thái triển khai của bạn (nghĩa là điều gì sẽ ngăn ai đó tái -calling khởi tạo hoặc setter?).


5

Tôi cũng đã gặp tình huống này một vài lần trong các môi trường nơi tôi đang tự động tạo các đối tượng ViewModel dựa trên các đối tượng Mô hình (được phác thảo thực sự tốt bởi bài đăng Stackoverflow khác này ).

Tôi thích cách tiện ích mở rộng Ninject cho phép bạn tự động tạo các nhà máy dựa trên giao diện:

Bind<IMyFactory>().ToFactory();

Tôi không thể tìm thấy bất kỳ chức năng tương tự trực tiếp trong Unity ; Vì vậy, tôi đã viết phần mở rộng của riêng mình cho IUnityContainer , cho phép bạn đăng ký các nhà máy sẽ tạo các đối tượng mới dựa trên dữ liệu từ các đối tượng hiện có về cơ bản ánh xạ từ hệ thống phân cấp một loại sang hệ thống phân cấp loại khác: UnityMappingFactory @ GitHub

Với mục tiêu đơn giản và dễ đọc, tôi đã kết thúc với một tiện ích mở rộng cho phép bạn trực tiếp chỉ định ánh xạ mà không cần khai báo các lớp hoặc giao diện nhà máy riêng lẻ (trình tiết kiệm thời gian thực). Bạn chỉ cần thêm ánh xạ vào đúng nơi bạn đăng ký các lớp trong quá trình bootstrapping thông thường ...

//make sure to register the output...
container.RegisterType<IImageWidgetViewModel, ImageWidgetViewModel>();
container.RegisterType<ITextWidgetViewModel, TextWidgetViewModel>();

//define the mapping between different class hierarchies...
container.RegisterFactory<IWidget, IWidgetViewModel>()
.AddMap<IImageWidget, IImageWidgetViewModel>()
.AddMap<ITextWidget, ITextWidgetViewModel>();

Sau đó, bạn chỉ cần khai báo giao diện nhà máy ánh xạ trong hàm tạo cho CI và sử dụng phương thức Tạo () của nó ...

public ImageWidgetViewModel(IImageWidget widget, IAnotherDependency d) { }

public TextWidgetViewModel(ITextWidget widget) { }

public ContainerViewModel(object data, IFactory<IWidget, IWidgetViewModel> factory)
{
    IList<IWidgetViewModel> children = new List<IWidgetViewModel>();
    foreach (IWidget w in data.Widgets)
        children.Add(factory.Create(w));
}

Là một phần thưởng bổ sung, mọi phụ thuộc bổ sung trong hàm tạo của các lớp được ánh xạ cũng sẽ được giải quyết trong quá trình tạo đối tượng.

Rõ ràng, điều này sẽ không giải quyết mọi vấn đề nhưng nó đã phục vụ tôi khá tốt cho đến nay vì vậy tôi nghĩ tôi nên chia sẻ nó. Có nhiều tài liệu hơn trên trang web của dự án trên GitHub.


1

Tôi không thể trả lời bằng thuật ngữ Unity cụ thể nhưng có vẻ như bạn chỉ đang tìm hiểu về tiêm phụ thuộc. Nếu vậy, tôi khuyên bạn nên đọc hướng dẫn sử dụng đóng gói ngắn gọn, rõ ràng và thông tin cho Ninject .

Điều này sẽ đưa bạn qua các tùy chọn khác nhau mà bạn có khi sử dụng DI và cách tính toán các vấn đề cụ thể mà bạn sẽ gặp phải trên đường đi. Trong trường hợp của bạn, rất có thể bạn muốn sử dụng bộ chứa DI để khởi tạo các đối tượng của mình và để đối tượng đó có được một tham chiếu đến từng phụ thuộc của nó thông qua hàm tạo.

Hướng dẫn cũng chi tiết cách chú thích các phương thức, thuộc tính và thậm chí các tham số bằng cách sử dụng các thuộc tính để phân biệt chúng khi chạy.

Ngay cả khi bạn không sử dụng Ninject, hướng dẫn sẽ cung cấp cho bạn các khái niệm và thuật ngữ về chức năng phù hợp với mục đích của bạn và bạn có thể ánh xạ kiến ​​thức đó đến Unity hoặc các khung DI khác (hoặc thuyết phục bạn thử dùng Ninject) .


Cảm ơn vì điều đó. Tôi thực sự đang đánh giá các khung DI và NInject sẽ là khung tiếp theo của tôi.
Igor Zevaka

@johann: nhà cung cấp? github.com/ninject/ninject/wiki/ từ
anthony

1

Tôi nghĩ rằng tôi đã giải quyết nó và nó cảm thấy khá lành mạnh, vì vậy nó phải đúng một nửa :))

Tôi chia IMyIntfthành một giao diện "getter" và "setter". Vì thế:

interface IMyIntf {
  string RunTimeParam { get; }
}


interface IMyIntfSetter {
  void Initialize(string runTimeParam);
  IMyIntf MyIntf {get; }
}

Sau đó, việc thực hiện:

class MyIntfImpl : IMyIntf, IMyIntfSetter {
  string _runTimeParam;

  void Initialize(string runTimeParam) {
    _runTimeParam = runTimeParam;
  }

  string RunTimeParam { get; }

  IMyIntf MyIntf {get {return this;} }
}

//Unity configuration:
//Only the setter is mapped to the implementation.
container.RegisterType<IMyIntfSetter, MyIntfImpl>();
//To retrieve an instance of IMyIntf:
//1. create the setter
IMyIntfSetter setter = container.Resolve<IMyIntfSetter>();
//2. Init it
setter.Initialize("someparam");
//3. Use the IMyIntf accessor
IMyIntf intf = setter.MyIntf;

IMyIntfSetter.Initialize()vẫn có thể được gọi nhiều lần nhưng sử dụng các bit của mô hình Trình định vị dịch vụ, chúng ta có thể gói nó khá độc đáo để IMyIntfSettergần như là một giao diện bên trong khác biệt IMyIntf.


13
Đây không phải là một giải pháp đặc biệt tốt vì nó dựa trên một phương pháp Khởi tạo, đó là một Trừu tượng Leaky. Btw, cái này không giống như Bộ định vị dịch vụ, mà giống như Giao diện tiêm. Trong mọi trường hợp, xem câu trả lời của tôi cho một giải pháp tốt hơn.
Mark Seemann
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.