Làm thế nào để sử dụng tiêm phụ thuộc và tránh khớp nối thời gian?


11

Giả sử tôi có các Servicephụ thuộc nhận thông qua hàm tạo nhưng cũng cần được khởi tạo với dữ liệu tùy chỉnh (ngữ cảnh) trước khi có thể sử dụng:

public interface IService
{
    void Initialize(Context context);
    void DoSomething();
    void DoOtherThing();
}

public class Service : IService
{
    private readonly object dependency1;
    private readonly object dependency2;
    private readonly object dependency3;

    public Service(
        object dependency1,
        object dependency2,
        object dependency3)
    {
        this.dependency1 = dependency1 ?? throw new ArgumentNullException(nameof(dependency1));
        this.dependency2 = dependency2 ?? throw new ArgumentNullException(nameof(dependency2));
        this.dependency3 = dependency3 ?? throw new ArgumentNullException(nameof(dependency3));
    }

    public void Initialize(Context context)
    {
        // Initialize state based on context
        // Heavy, long running operation
    }

    public void DoSomething()
    {
        // ...
    }

    public void DoOtherThing()
    {
        // ...
    }
}

public class Context
{
    public int Value1;
    public string Value2;
    public string Value3;
}

Bây giờ - dữ liệu ngữ cảnh không được biết trước nên tôi không thể đăng ký dưới dạng phụ thuộc và sử dụng DI để đưa dữ liệu vào dịch vụ

Đây là ví dụ khách hàng trông như thế nào:

public class Client
{
    private readonly IService service;

    public Client(IService service)
    {
        this.service = service ?? throw new ArgumentNullException(nameof(service));
    }

    public void OnStartup()
    {
        service.Initialize(new Context
        {
            Value1 = 123,
            Value2 = "my data",
            Value3 = "abcd"
        });
    }

    public void Execute()
    {
        service.DoSomething();
        service.DoOtherThing();
    }
}

Như bạn có thể thấy - có khớp nối tạm thời và khởi tạo mã phương thức có liên quan, bởi vì trước tiên tôi cần gọi service.Initializeđể có thể gọi service.DoSomethingservice.DoOtherThingsau đó.

Các phương pháp khác mà tôi có thể loại bỏ những vấn đề này là gì?

Làm rõ thêm về hành vi:

Mỗi phiên bản của máy khách cần có phiên bản riêng của dịch vụ được khởi tạo với dữ liệu ngữ cảnh cụ thể của máy khách. Vì vậy, dữ liệu ngữ cảnh đó không phải là tĩnh hoặc được biết trước vì vậy nó không thể được DI đưa vào trong hàm tạo.

Câu trả lời:


17

Có một số cách để giải quyết vấn đề khởi tạo:

  • Như đã trả lời trong /software//a/334994/301401 , các phương thức init () là một mùi mã. Khởi tạo một đối tượng là trách nhiệm của nhà xây dựng - đó là lý do tại sao chúng ta có các nhà xây dựng.
  • Thêm Dịch vụ đã cho phải được khởi tạo vào nhận xét doc của hàm Clienttạo và để cho hàm tạo ném nếu dịch vụ không được khởi tạo. Điều này chuyển trách nhiệm cho người cung cấp cho bạn IServiceđối tượng.

Tuy nhiên, trong ví dụ của bạn, Clientlà người duy nhất biết các giá trị được truyền vào Initialize(). Nếu bạn muốn giữ nó theo cách đó, tôi đề nghị như sau:

  • Thêm một IServiceFactoryvà chuyển nó cho các nhà Clientxây dựng. Sau đó, bạn có thể gọi serviceFactory.createService(new Context(...))cho bạn một khởi tạo IServicecó thể được sử dụng bởi khách hàng của bạn.

Các nhà máy có thể rất đơn giản và cũng cho phép bạn tránh các phương thức init () và sử dụng các hàm tạo thay thế:

public interface IServiceFactory
{
    IService createService(Context context);
}

public class ServiceFactory : IServiceFactory
{
    public Service createService(Context context)
    {
        return new Service(context);
    }
}

Trong máy khách, OnStartup()cũng là một phương thức khởi tạo (nó chỉ sử dụng một tên khác). Vì vậy, nếu có thể (nếu bạn biết Contextdữ liệu), nhà máy nên được gọi trực tiếp trong hàm Clienttạo. Nếu điều đó là không thể, bạn cần lưu trữ IServiceFactoryvà gọi nó vào OnStartup().

Khi Servicecó phụ thuộc không được cung cấp bởi Clienthọ sẽ được DI cung cấp thông qua ServiceFactory:

public interface IServiceFactory
{
    IService createService(Context context);
}    

public class ServiceFactory : IServiceFactory
{        
    private readonly object dependency1;
    private readonly object dependency2;
    private readonly object dependency3;

    public ServiceFactory(object dependency1, object dependency2, object dependency3)
    {
        this.dependency1 = dependency1;
        this.dependency2 = dependency2;
        this.dependency3 = dependency3;
    }

    public Service createService(Context context)
    {
        return new Service(context, dependency1, dependency2, dependency3);
    }
}

1
Cảm ơn bạn, giống như tôi nghĩ, ở điểm cuối cùng ... Và trong ServiceFactory, bạn sẽ sử dụng hàm tạo DI trong chính nhà máy vì các phụ thuộc cần thiết cho nhà xây dựng dịch vụ hoặc trình định vị dịch vụ sẽ phù hợp hơn?
Dusan

1
@Dusan không sử dụng Trình định vị dịch vụ. Nếu Servicecó các phụ thuộc khác ngoài Context, sẽ không được cung cấp bởi Client, chúng có thể được cung cấp qua DI ServiceFactoryđể được chuyển đến Servicekhi createServiceđược gọi.
Mr.Mindor

@Dusan Nếu bạn cần cung cấp các phụ thuộc khác nhau cho các Dịch vụ khác nhau (ví dụ: dịch vụ này cần phụ thuộc1_1 nhưng kế tiếp cần phụ thuộc1_2), nhưng nếu mẫu này hoạt động theo cách khác, thì bạn có thể sử dụng mẫu tương tự thường được gọi là mẫu Builder. Builder cho phép bạn thiết lập một đối tượng từng phần theo thời gian nếu cần thiết. Sau đó, bạn có thể làm điều này ... ServiceBuilder partial = new ServiceBuilder().dependency1(dependency1_1).dependency2(dependency2_1).dependency3(dependency3_1);và được để lại với dịch vụ được thiết lập một phần của bạn, sau đó thực hiệnService s = partial.context(context).build()
Aaron

1

Các Initializephương pháp nên được loại bỏ khỏi IServicegiao diện, vì đây là một chi tiết thực hiện. Thay vào đó, hãy định nghĩa một lớp khác lấy ví dụ cụ thể của Dịch vụ và gọi phương thức khởi tạo trên nó. Sau đó, lớp mới này thực hiện giao diện IService:

public class ContextDependentService : IService
{
    public ContextDependentService(Context context, Service service)
    {
        this.service = service;

        service.Initialize(context);
    }

    // Methods in the IService interface
}

Điều này giữ cho mã máy khách không biết gì về thủ tục khởi tạo, trừ trường hợp ContextDependentServicelớp được khởi tạo. Bạn ít nhất giới hạn các phần trong ứng dụng của bạn cần biết về thủ tục khởi tạo mạnh mẽ này.


1

Dường như với tôi rằng bạn có hai lựa chọn ở đây

  1. Di chuyển mã Khởi tạo vào Ngữ cảnh và đưa vào Bối cảnh khởi tạo

ví dụ.

public InitialisedContext Initialise()
  1. Có cuộc gọi đầu tiên để Thực hiện cuộc gọi Khởi tạo nếu cuộc gọi chưa hoàn tất

ví dụ.

public async Task Execute()
{
     //lock context
     //check context is not initialised
     // init if required
     //execute code...
}
  1. Chỉ cần ném ngoại lệ nếu Ngữ cảnh không được khởi chạy khi bạn gọi Execute. Giống như SqlConnection.

Tiêm một nhà máy là tốt nếu bạn chỉ muốn tránh bối cảnh như một tham số. Chỉ nói việc triển khai cụ thể này cần một ngữ cảnh và bạn không muốn thêm nó vào Giao diện

Nhưng về cơ bản, bạn có cùng một vấn đề, nếu nhà máy chưa có bối cảnh khởi tạo.


0

Bạn không nên phụ thuộc giao diện của mình vào bất kỳ bối cảnh db và khởi tạo phương thức nào. Bạn có thể làm điều đó trong lớp xây dựng cụ thể.

public interface IService
{
    void DoSomething();
    void DoOtherThing();
}

public class Service : IService
{
    private readonly object dependency1;
    private readonly object dependency2;
    private readonly object dependency3;
    private readonly object context;

    public Service(
        object dependency1,
        object dependency2,
        object dependency3,
        object context )
    {
        this.dependency1 = dependency1 ?? throw new ArgumentNullException(nameof(dependency1));
        this.dependency2 = dependency2 ?? throw new ArgumentNullException(nameof(dependency2));
        this.dependency3 = dependency3 ?? throw new ArgumentNullException(nameof(dependency3));

        // context is concrete class details not interfaces.
        this.context = context;

        // call init here constructor.
        this.Initialize(context);
    }

    protected void Initialize(Context context)
    {
        // Initialize state based on context
        // Heavy, long running operation
    }

    public void DoSomething()
    {
        // ...
    }

    public void DoOtherThing()
    {
        // ...
    }
}

Và, một câu trả lời cho câu hỏi chính của bạn sẽ là Thuộc tính tiêm .

public class Service
    {
        public Service(Context context)
        {
            this.context = context;
        }

        private Dependency1 _dependency1;
        public Dependency1 Dependency1
        {
            get
            {
                if (_dependency1 == null)
                    _dependency1 = Container.Resolve<Dependency1>();

                return _dependency1;
            }
        }

        //...
    }

Bằng cách này, bạn có thể gọi tất cả các phụ thuộc bằng cách tiêm bất động sản . Nhưng nó có thể là con số khổng lồ. Nếu vậy, bạn có thể sử dụng Con Contortor tiêm cho chúng, nhưng bạn có thể đặt bối cảnh của mình theo thuộc tính bằng cách kiểm tra xem nó có rỗng không.


OK, tuyệt, nhưng ... mỗi phiên bản của máy khách cần phải có phiên bản riêng của dịch vụ được khởi tạo với dữ liệu ngữ cảnh khác nhau. Dữ liệu ngữ cảnh đó không phải là tĩnh hoặc được biết trước vì vậy nó không thể được DI đưa vào trong hàm tạo. Sau đó, làm cách nào để có / tạo phiên bản dịch vụ cùng với các phụ thuộc khác trong các máy khách của tôi?
Dusan

hmm, liệu hàm tạo tĩnh có chạy trước khi bạn đặt bối cảnh không? và khởi tạo trong các nhà xây dựng có nguy cơ ngoại lệ
Ewan

Tôi đang nghiêng về nhà máy tiêm có thể tạo và khởi tạo dịch vụ với dữ liệu ngữ cảnh cụ thể (chứ không phải chính dịch vụ tiêm), nhưng tôi không chắc có giải pháp nào tốt hơn không.
Dusan

@Ewan Bạn nói đúng. Tôi sẽ cố gắng tìm một giải pháp cho nó. Nhưng trước đó, tôi sẽ loại bỏ nó ngay bây giờ.
Engineert

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.