Bagaimana cara menggunakan injeksi ketergantungan dan menghindari pemasangan temporal?

11

Misalkan saya memiliki Serviceyang menerima dependensi melalui konstruktor tetapi juga perlu diinisialisasi dengan data kustom (konteks) sebelum dapat digunakan:

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;
}

Sekarang - data konteks tidak diketahui sebelumnya jadi saya tidak bisa mendaftarkannya sebagai ketergantungan dan menggunakan DI untuk menyuntikkannya ke dalam layanan

Beginilah contoh klien terlihat seperti:

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();
    }
}

Seperti yang Anda lihat - ada kopling temporal dan inisialisasi kode metode bau yang terlibat, karena saya pertama-tama harus memanggil service.Initializeuntuk dapat memanggil service.DoSomethingdan service.DoOtherThingsesudahnya.

Apa pendekatan lain di mana saya bisa menghilangkan masalah ini?

Klarifikasi tambahan dari perilaku:

Setiap instance klien perlu memiliki instance layanan itu sendiri yang diinisialisasi dengan data konteks spesifik klien. Jadi, data konteks itu tidak statis atau diketahui sebelumnya sehingga tidak dapat disuntikkan oleh DI dalam konstruktor.

Dusan
sumber

Jawaban:

18

Ada beberapa cara untuk mengatasi masalah inisialisasi:

  • Seperti yang dijawab dalam /software//a/334994/301401 , metode init () adalah bau kode. Inisialisasi objek adalah tanggung jawab konstruktor - itu sebabnya kami memiliki konstruktor.
  • Tambah Layanan yang diberikan harus diinisialisasi ke komentar dokumen Clientkonstruktor dan biarkan konstruktor membuang jika layanan tidak diinisialisasi. Ini memindahkan tanggung jawab kepada orang yang memberi Anda IServiceobjek.

Namun, dalam contoh Anda, Clientini adalah satu-satunya yang mengetahui nilai yang diteruskan Initialize(). Jika Anda ingin tetap seperti itu, saya sarankan yang berikut:

  • Tambahkan IServiceFactorydan berikan ke Clientkonstruktor. Kemudian Anda dapat menelepon serviceFactory.createService(new Context(...))yang memberi Anda inisialisasi IServiceyang dapat digunakan oleh klien Anda.

Pabrik-pabrik bisa sangat sederhana dan juga memungkinkan Anda untuk menghindari metode init () dan menggunakan konstruktor sebagai gantinya:

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

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

Di klien, OnStartup()juga merupakan metode inisialisasi (hanya menggunakan nama yang berbeda). Jadi jika memungkinkan (jika Anda tahu Contextdatanya), pabrik harus langsung dipanggil dalam Clientkonstruktor. Jika itu tidak memungkinkan, Anda perlu menyimpan IServiceFactorydan memanggilnya OnStartup().

Ketika Servicedependensi tidak disediakan oleh Clientmereka akan disediakan oleh DI melalui 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);
    }
}
pschill
sumber
1
Terima kasih, seperti yang saya kira, pada poin terakhir ... Dan di ServiceFactory, apakah Anda akan menggunakan konstruktor DI di pabrik itu sendiri untuk dependensi yang diperlukan untuk konstruktor layanan atau pelacak layanan akan lebih cocok?
Dusan
1
@Usan tidak menggunakan Service Locator. Jika Servicememiliki dependensi selain Context, yang tidak disediakan oleh Client, mereka dapat diberikan melalui DI untuk ServiceFactoryditeruskan ke Servicesaat createServicedipanggil.
Mr.Mindor
@Dusan Jika Anda perlu menyediakan dependensi berbeda ke Layanan yang berbeda (yaitu: ini membutuhkan dependensi1_1 tetapi yang berikutnya membutuhkan dependency1_2), tetapi jika pola ini berlaku untuk Anda, maka Anda dapat menggunakan pola serupa yang sering disebut pola Builder. Builder memungkinkan Anda mengatur bagian sedikit demi sedikit dari waktu ke waktu jika perlu. Maka Anda dapat melakukan ini ... ServiceBuilder partial = new ServiceBuilder().dependency1(dependency1_1).dependency2(dependency2_1).dependency3(dependency3_1);dan dibiarkan dengan layanan pengaturan sebagian Anda, kemudian nantiService s = partial.context(context).build()
Aaron
1

The InitializeMetode harus dihapus dari IServiceantarmuka, karena ini adalah detail pelaksanaan. Alih-alih, tentukan kelas lain yang mengambil contoh konkret Layanan dan memanggil metode inisialisasi di dalamnya. Kemudian kelas baru ini mengimplementasikan antarmuka IService:

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

        service.Initialize(context);
    }

    // Methods in the IService interface
}

Ini membuat kode klien tidak mengetahui prosedur inisialisasi, kecuali di mana ContextDependentServicekelas diinisialisasi. Anda setidaknya membatasi bagian-bagian dari aplikasi Anda yang perlu tahu tentang prosedur inisialisasi yang miring ini.

Greg Burghardt
sumber
1

Menurut saya, Anda memiliki dua opsi di sini

  1. Pindahkan kode Inisialisasi ke Konteks dan menyuntikkan Konteks Inisialisasi

misalnya.

public InitialisedContext Initialise()
  1. Mintalah panggilan pertama untuk Melaksanakan panggilan Menginisialisasi jika belum selesai

misalnya.

public async Task Execute()
{
     //lock context
     //check context is not initialised
     // init if required
     //execute code...
}
  1. Hanya melempar pengecualian jika Konteks tidak diinisialisasi ketika Anda memanggil Execute. Seperti SqlConnection.

Menyuntikkan pabrik tidak masalah jika Anda hanya ingin menghindari konteks lewat sebagai parameter. Katakan saja implementasi khusus ini membutuhkan konteks dan Anda ingin tidak menambahkannya ke Antarmuka

Tetapi Anda pada dasarnya memiliki masalah yang sama, bagaimana jika pabrik belum memiliki konteks yang sudah diinisialisasi.

Ewan
sumber
0

Anda seharusnya tidak bergantung pada antarmuka Anda ke konteks db dan menginisialisasi metode. Anda dapat melakukannya di konstruktor kelas beton.

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()
    {
        // ...
    }
}

Dan, jawaban dari pertanyaan utama Anda adalah Injeksi Properti .

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;
            }
        }

        //...
    }

Dengan cara ini Anda dapat memanggil semua dependensi dengan Injeksi Properti . Tapi bisa jadi jumlahnya sangat banyak. Jika demikian, Anda bisa menggunakan Injeksi Konstruktor untuk mereka, tetapi Anda dapat mengatur konteks Anda dengan properti dengan memeriksa apakah itu nol.

Insinyur
sumber
OK, bagus, tapi ... setiap instance klien perlu memiliki instance layanan itu sendiri yang diinisialisasi dengan data konteks yang berbeda. Data konteks itu tidak statis atau diketahui sebelumnya sehingga tidak bisa disuntikkan oleh DI di konstruktor. Lalu, bagaimana saya mendapatkan / membuat instance layanan bersama dengan dependensi lain di klien saya?
Dusan
hmm tidak akan berjalan konstruktor statis sebelum Anda mengatur konteksnya? dan menginisialisasi dalam pengecualian risiko konstruktor
Ewan
Saya condong ke pabrik injeksi yang dapat membuat dan menginisialisasi layanan dengan data konteks yang diberikan (daripada menyuntikkan layanan itu sendiri), tetapi saya tidak yakin apakah ada solusi yang lebih baik.
Dusan
@ Ewan Kamu benar. Saya akan mencoba mencari solusi untuk itu. Tapi sebelum itu, saya akan menghapusnya sekarang.
Insinyur
0

Misko Hevery memiliki posting blog yang sangat membantu tentang kasus yang Anda hadapi. Anda berdua membutuhkan yang baru dan dapat disuntikkan untuk Servicekelas Anda dan posting blog ini dapat membantu Anda.

Huruf tebal
sumber