Apakah ada pola untuk menginisialisasi objek yang dibuat melalui wadah DI

147

Saya mencoba untuk mendapatkan Unity untuk mengelola pembuatan objek saya dan saya ingin memiliki beberapa parameter inisialisasi yang tidak diketahui hingga run-time:

Saat ini satu-satunya cara saya bisa memikirkan cara untuk melakukannya adalah dengan memiliki metode Init pada antarmuka.

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

Kemudian untuk menggunakannya (dalam Unity) saya akan melakukan ini:

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

Dalam skenario ini runTimeParamparam ditentukan pada saat run-time berdasarkan input pengguna. Kasus sepele di sini hanya mengembalikan nilai runTimeParamtetapi dalam kenyataannya parameter akan menjadi sesuatu seperti nama file dan metode inisialisasi akan melakukan sesuatu dengan file tersebut.

Ini menciptakan sejumlah masalah, yaitu bahwa Initializemetode ini tersedia di antarmuka dan dapat dipanggil beberapa kali. Mengatur bendera dalam implementasi dan melempar pengecualian pada panggilan berulang untuk Initializetampaknya cara kikuk.

Pada titik di mana saya menyelesaikan antarmuka saya, saya tidak ingin tahu apa-apa tentang implementasi IMyIntf. Apa yang saya inginkan, bagaimanapun, adalah pengetahuan bahwa antarmuka ini membutuhkan parameter inisialisasi satu kali tertentu. Apakah ada cara entah bagaimana menjelaskan (atribut?) Antarmuka dengan informasi ini dan meneruskannya ke kerangka kerja ketika objek dibuat?

Sunting: Menjelaskan antarmuka sedikit lebih.

Igor Zevaka
sumber
9
Anda kehilangan titik menggunakan wadah DI. Ketergantungan seharusnya diselesaikan untuk Anda.
Pierreten
Dari mana Anda mendapatkan parameter yang dibutuhkan? (file konfigurasi, db, ??)
Jaime
runTimeParamadalah ketergantungan yang ditentukan pada saat dijalankan berdasarkan input pengguna. Haruskah alternatif untuk ini membaginya menjadi dua antarmuka - satu untuk inisialisasi dan satu lagi untuk menyimpan nilai?
Igor Zevaka
dependensi dalam IoC, biasanya merujuk pada ketergantungan pada kelas atau objek tipe ref lainnya yang dapat ditentukan pada fase inisialisasi IoC. Jika kelas Anda hanya membutuhkan beberapa nilai untuk bekerja, di situlah metode Initialize () di kelas Anda menjadi berguna.
The Light
Maksud saya bayangkan ada 100 kelas di aplikasi Anda di mana pendekatan ini dapat diterapkan; maka Anda harus membuat tambahan 100 kelas pabrik + 100 antarmuka untuk kelas Anda dan Anda bisa lolos jika Anda hanya menggunakan metode Initialize ().
The Light

Jawaban:

276

Setiap tempat di mana Anda memerlukan nilai run-time untuk membangun ketergantungan tertentu, Abstract Factory adalah solusinya.

Memiliki metode Inisialisasi pada antarmuka bau Abstraksi Leaky .

Dalam kasus Anda, saya akan mengatakan bahwa Anda harus memodelkan IMyIntfantarmuka tentang bagaimana Anda perlu menggunakannya - bukan bagaimana Anda bermaksud untuk membuat implementasi darinya. Itu detail implementasi.

Dengan demikian, antarmuka seharusnya:

public interface IMyIntf
{
    string RunTimeParam { get; }
}

Sekarang tentukan Pabrik Abstrak:

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

Anda sekarang dapat membuat implementasi konkret IMyIntfFactoryyang menciptakan contoh konkret IMyIntfseperti ini:

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

Perhatikan bagaimana ini memungkinkan kami untuk melindungi invarian kelas dengan menggunakan readonlykata kunci. Tidak diperlukan metode inisialisasi bau.

Sebuah IMyIntfFactoryimplementasi mungkin sesederhana ini:

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

Di semua konsumen Anda di mana Anda membutuhkan sebuah IMyIntfinstance, Anda cukup mengandalkan IMyIntfFactorydengan memintanya melalui Constructor Injection .

Setiap Kontainer DI yang bernilai garamnya akan dapat secara otomatis mengirimkan IMyIntfFactorycontoh kepada Anda jika Anda mendaftarkannya dengan benar.

Mark Seemann
sumber
13
Masalahnya adalah bahwa suatu metode (seperti Inisialisasi) adalah bagian dari API Anda, sedangkan konstruktornya tidak. blog.ploeh.dk/2011/02/28/InterfacesAreAccessModifiers.aspx
Mark Seemann
13
Selain itu, metode Initialize menunjukkan Temporal Coupling: blog.ploeh.dk/2011/05/24/DesignSmellTemporalCoupling.aspx
Mark Seemann
2
@ Darlene Anda mungkin dapat menggunakan Dekorator malas diinisialisasi, seperti dijelaskan dalam bagian 8.3.6 buku saya . Saya juga memberikan contoh yang serupa dalam presentasi saya Big Object Graphs Up Front .
Mark Seemann
2
@ Mark Jika kreasi MyIntfimplementasi pabrik membutuhkan lebih dari runTimeParam(baca: layanan lain yang ingin diselesaikan oleh IoC), maka Anda masih dihadapkan pada penyelesaian dependensi di pabrik Anda. Saya suka jawaban @ PhilSandler untuk meneruskan dependensi tersebut ke konstruktor pabrik untuk menyelesaikan masalah ini - apakah Anda juga mengambilnya?
Jeff
2
Juga hal-hal hebat, tetapi jawaban Anda untuk pertanyaan lain ini benar-benar sampai ke titik saya.
Jeff
15

Biasanya ketika Anda menghadapi situasi ini, Anda perlu mengunjungi kembali desain Anda dan menentukan apakah Anda mencampur objek stateful / data Anda dengan layanan murni Anda. Dalam sebagian besar (tidak semua) kasus, Anda ingin memisahkan kedua jenis objek ini.

Jika Anda memerlukan parameter khusus konteks yang diteruskan dalam konstruktor, salah satu opsi adalah membuat pabrik yang menyelesaikan dependensi layanan Anda melalui konstruktor, dan menggunakan parameter run-time sebagai parameter dari metode Create () (atau Menghasilkan ( ), Build () atau apa pun yang Anda sebutkan metode pabrik Anda).

Memiliki setter atau metode Initialize () umumnya dianggap sebagai desain yang buruk, karena Anda perlu "ingat" untuk memanggil mereka dan pastikan mereka tidak membuka terlalu banyak kondisi implementasi Anda (yaitu apa yang menghentikan seseorang untuk -memanggil inisialisasi atau penyetel?).

Phil Sandler
sumber
5

Saya juga telah menemukan situasi ini beberapa kali di lingkungan di mana saya secara dinamis membuat objek ViewModel berdasarkan objek Model (diuraikan dengan sangat baik oleh posting Stackoverflow ini ).

Saya menyukai bagaimana ekstensi Ninject yang memungkinkan Anda membuat pabrik secara dinamis berdasarkan antarmuka:

Bind<IMyFactory>().ToFactory();

Saya tidak dapat menemukan fungsi serupa secara langsung di Unity ; jadi saya menulis ekstensi saya sendiri ke IUnityContainer yang memungkinkan Anda untuk mendaftarkan pabrik yang akan membuat objek baru berdasarkan data dari objek yang ada pada dasarnya memetakan dari satu hierarki jenis ke hierarki jenis yang berbeda: UnityMappingFactory @ GitHub

Dengan tujuan kesederhanaan dan keterbacaan, saya berakhir dengan ekstensi yang memungkinkan Anda untuk secara langsung menentukan pemetaan tanpa mendeklarasikan masing-masing kelas pabrik atau antarmuka (penghemat waktu nyata). Anda cukup menambahkan pemetaan di mana Anda mendaftarkan kelas selama proses bootstrap normal ...

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

Kemudian Anda cukup mendeklarasikan antarmuka pabrik pemetaan di konstruktor untuk CI dan menggunakan metode Buat () ...

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

Sebagai bonus tambahan, setiap dependensi tambahan dalam konstruktor dari kelas yang dipetakan juga akan diselesaikan selama pembuatan objek.

Jelas, ini tidak akan menyelesaikan setiap masalah tetapi sejauh ini telah membantu saya, jadi saya pikir saya harus membaginya. Ada lebih banyak dokumentasi di situs proyek di GitHub.

jigamiller
sumber
1

Saya tidak bisa menjawab dengan terminologi Unity tertentu tetapi sepertinya Anda baru belajar tentang injeksi ketergantungan. Jika demikian, saya mendorong Anda untuk membaca panduan pengguna singkat, jelas, dan informasi untuk Ninject .

Ini akan memandu Anda melalui berbagai opsi yang Anda miliki saat menggunakan DI, dan bagaimana menjelaskan masalah spesifik yang akan Anda hadapi di sepanjang jalan. Dalam kasus Anda, kemungkinan besar Anda ingin menggunakan wadah DI untuk membuat instance objek Anda, dan meminta objek tersebut mendapatkan referensi ke masing-masing dependensinya melalui konstruktor.

Panduan ini juga merinci cara membuat anotasi metode, properti, dan bahkan parameter menggunakan atribut untuk membedakannya saat runtime.

Bahkan jika Anda tidak menggunakan Ninject, panduan akan memberikan Anda konsep dan terminologi fungsi yang sesuai dengan tujuan Anda, dan Anda harus dapat memetakan pengetahuan itu ke Unity atau kerangka kerja DI lainnya (atau meyakinkan Anda untuk mencoba Ninject) .

anthony
sumber
Terima kasih untuk itu. Saya sebenarnya mengevaluasi kerangka kerja DI dan NInject akan menjadi yang saya berikutnya.
Igor Zevaka
@ Johann: penyedia? github.com/ninject/ninject/wiki/…
anthony
1

Saya pikir saya menyelesaikannya dan rasanya agak sehat, jadi pasti setengah benar :))

Saya terpecah IMyIntfmenjadi "getter" dan "setter" interface. Begitu:

interface IMyIntf {
  string RunTimeParam { get; }
}


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

Kemudian implementasinya:

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()masih bisa dipanggil beberapa kali tetapi menggunakan bit paradigma Service Locator kita dapat membungkusnya dengan cukup baik sehingga IMyIntfSetterhampir merupakan antarmuka internal yang berbeda IMyIntf.

Igor Zevaka
sumber
13
Ini bukan solusi yang sangat baik karena bergantung pada metode Inisialisasi, yang merupakan Abstraksi Leaky. Btw, ini tidak terlihat seperti Service Locator, tetapi lebih seperti Antarmuka Injeksi. Bagaimanapun, lihat jawaban saya untuk solusi yang lebih baik.
Mark Seemann