Haruskah saya menggunakan Dependency Injection atau pabrik statis?

81

Saat mendesain sistem saya sering dihadapkan dengan masalah memiliki banyak modul (logging, akses database, dll) yang digunakan oleh modul lain. Pertanyaannya adalah, bagaimana cara saya menyediakan komponen-komponen ini ke komponen lain. Tampaknya ada dua jawaban ketergantungan injeksi atau menggunakan pola pabrik. Namun keduanya tampak salah:

  • Pabrik membuat pengujian menjadi menyakitkan dan tidak memungkinkan pertukaran implementasi yang mudah. Mereka juga tidak membuat dependensi terlihat (misalnya Anda memeriksa suatu metode, tidak menyadari fakta bahwa ia memanggil metode yang memanggil metode yang memanggil metode yang menggunakan database).
  • Dependecy injection secara besar-besaran membengkak daftar argumen konstruktor dan itu mengolesi beberapa aspek di seluruh kode Anda. Situasi umum adalah di mana konstruktor lebih dari setengah kelas terlihat seperti ini(....., LoggingProvider l, DbSessionProvider db, ExceptionFactory d, UserSession sess, Descriptions d)

Berikut adalah situasi umum yang saya punya masalah dengan: Saya memiliki kelas pengecualian, yang menggunakan deskripsi kesalahan yang dimuat dari database, menggunakan kueri yang memiliki parameter pengaturan bahasa pengguna, yang ada di objek sesi pengguna. Jadi untuk membuat Pengecualian baru saya perlu deskripsi, yang membutuhkan sesi basis data dan sesi pengguna. Jadi saya ditakdirkan untuk menyeret semua objek ini di semua metode saya kalau-kalau saya mungkin perlu melemparkan pengecualian.

Bagaimana saya mengatasi masalah seperti itu ??

RokL
sumber
2
Jika sebuah pabrik dapat menyelesaikan semua masalah Anda, mungkin Anda bisa menyuntikkan pabrik ke objek Anda dan mendapatkan LoggingProvider, DbSessionProvider, ExceptionFactory, UserSession dari sana.
Giorgio
1
Terlalu banyak "Input" untuk suatu metode, baik yang disahkan atau disuntikkan, lebih merupakan masalah dari desain metode itu sendiri. Kemanapun Anda pergi, Anda mungkin ingin mengurangi ukuran metode Anda sedikit (yang lebih mudah dilakukan setelah Anda mendapatkan suntikan di tempat)
Bill K
Solusinya di sini seharusnya tidak mengurangi argumen. Alih-alih membangun abstraksi yang membangun objek tingkat lebih tinggi yang melakukan semua pekerjaan di objek dan memberi Anda manfaat.
Alex

Jawaban:

74

Gunakan injeksi ketergantungan, tetapi setiap kali argumen konstruktor Anda menjadi terlalu besar, refactor dengan menggunakan Layanan Fasad . Idenya adalah untuk mengelompokkan beberapa argumen konstruktor bersama, memperkenalkan abstraksi baru.

Misalnya, Anda bisa memperkenalkan jenis baru SessionEnvironmentmengenkapsulasi DBSessionProvider, yang UserSessiondan dimuat Descriptions. Namun, untuk mengetahui abstraksi mana yang paling masuk akal, Anda harus mengetahui detail program Anda.

Pertanyaan serupa sudah diajukan di sini pada SO .

Doc Brown
sumber
9
+1: Saya pikir mengelompokkan argumen konstruktor ke dalam kelas adalah ide yang sangat bagus. Ini juga memaksa Anda untuk mengatur argumen ini menjadi struktur yang lebih bermakna.
Giorgio
5
Tetapi jika hasilnya bukan struktur yang berarti maka Anda hanya menyembunyikan kompleksitas yang melanggar SRP. Dalam hal ini refactoring kelas harus dilakukan.
danidacar
1
@Giorgio Saya tidak setuju dengan pernyataan umum bahwa "mengelompokkan argumen konstruktor ke dalam kelas adalah ide yang sangat bagus.". Jika Anda memenuhi syarat ini dengan "dalam skenario ini" maka itu berbeda.
tymtam
19

Dependecy injection secara besar-besaran membengkak daftar argumen konstruktor dan itu mengolesi beberapa aspek di seluruh kode Anda.

Dari situ, sepertinya Anda tidak memahami DI yang sebenarnya - idenya adalah membalikkan pola instantiasi objek di dalam pabrik.

Masalah khusus Anda tampaknya menjadi masalah OOP yang lebih umum. Mengapa objek tidak bisa hanya melempar pengecualian normal, yang tidak bisa dibaca manusia selama runtime, dan kemudian memiliki sesuatu sebelum percobaan / tangkapan terakhir yang menangkap pengecualian itu, dan pada saat itu menggunakan informasi sesi untuk melempar pengecualian baru yang lebih cantik ?

Pendekatan lain adalah dengan memiliki pabrik pengecualian, yang diteruskan ke objek melalui konstruktor mereka. Alih-alih melempar pengecualian baru, kelas dapat menggunakan metode pabrik (mis throw PrettyExceptionFactory.createException(data).

Ingatlah bahwa objek Anda, selain dari objek pabrik Anda, tidak boleh menggunakan newoperator. Pengecualian pada umumnya merupakan satu kasus khusus, tetapi dalam kasus Anda, mereka mungkin pengecualian!

Jonathan Rich
sumber
1
Saya telah membaca di suatu tempat bahwa ketika daftar parameter Anda menjadi terlalu lama bukan karena Anda menggunakan injeksi dependensi tetapi karena Anda memerlukan injeksi dependensi yang lebih banyak.
Giorgio
Itu bisa menjadi salah satu alasan - umumnya, tergantung pada bahasanya, konstruktor Anda tidak boleh memiliki lebih dari 6-8 argumen, dan tidak lebih dari 3-4 dari itu harus menjadi objek itu sendiri, kecuali pola spesifik (seperti Builderpolanya) mendikte itu. Jika Anda memberikan parameter ke konstruktor karena objek Anda membuat instance objek lain, itu adalah kasus yang jelas untuk IoC.
Jonathan Rich
12

Anda telah mendaftarkan kerugian dari pola pabrik statis dengan cukup baik, tetapi saya tidak cukup setuju dengan kerugian dari pola injeksi ketergantungan:

Injeksi dependensi mengharuskan Anda untuk menulis kode untuk setiap dependensi bukan bug, tetapi fitur: Ini memaksa Anda untuk berpikir apakah Anda benar-benar membutuhkan dependensi ini, sehingga mempromosikan kopling longgar. Dalam contoh Anda:

Berikut adalah situasi umum yang saya punya masalah dengan: Saya memiliki kelas pengecualian, yang menggunakan deskripsi kesalahan yang dimuat dari database, menggunakan kueri yang memiliki parameter pengaturan bahasa pengguna, yang ada di objek sesi pengguna. Jadi untuk membuat Pengecualian baru saya perlu deskripsi, yang membutuhkan sesi basis data dan sesi pengguna. Jadi saya ditakdirkan untuk menyeret semua objek ini di semua metode saya kalau-kalau saya mungkin perlu melemparkan pengecualian.

Tidak, Anda tidak dikutuk. Mengapa logika bisnis bertanggung jawab untuk melokalkan pesan kesalahan Anda untuk sesi pengguna tertentu? Bagaimana jika, suatu saat nanti, Anda ingin menggunakan layanan bisnis itu dari program batch (yang tidak memiliki sesi pengguna ...)? Atau bagaimana jika pesan kesalahan tidak ditampilkan kepada pengguna yang sedang masuk, tetapi pengawasnya (yang mungkin lebih suka bahasa yang berbeda)? Atau bagaimana jika Anda ingin menggunakan kembali logika bisnis pada klien (yang tidak memiliki akses ke database ...)?

Jelas, pelokalan pesan tergantung pada siapa yang melihat pesan-pesan ini, yaitu itu adalah tanggung jawab dari lapisan presentasi. Oleh karena itu, saya akan membuang pengecualian biasa dari layanan bisnis, yang kebetulan membawa pengenal pesan yang kemudian dapat mencari penangan pengecualian lapisan presentasi di sumber pesan apa pun yang kebetulan digunakan.

Dengan begitu, Anda dapat menghapus 3 dependensi yang tidak perlu (UserSession, ExceptionFactory, dan mungkin deskripsi), sehingga membuat kode Anda lebih sederhana dan lebih fleksibel.

Secara umum, saya hanya akan menggunakan pabrik statis untuk hal-hal yang Anda perlukan aksesnya di mana-mana, dan yang dijamin akan tersedia di semua lingkungan yang kami inginkan untuk menjalankan kode (seperti Logging). Untuk yang lainnya, saya akan menggunakan injeksi ketergantungan lama biasa.

meriton - mogok
sumber
Ini. Saya tidak bisa melihat kebutuhan untuk menekan DB untuk melempar pengecualian.
Caleth
1

Gunakan injeksi ketergantungan. Menggunakan pabrik statis adalah pekerjaan Service Locatorantipattern. Lihat karya mani dari Martin Fowler di sini - http://martinfowler.com/articles/injection.html

Jika argumen konstruktor Anda menjadi terlalu besar dan Anda tidak menggunakan wadah DI dengan cara apa pun menulis pabrik Anda sendiri untuk instantiation, memungkinkannya untuk dapat dikonfigurasi, baik dengan XML atau mengikat implementasi ke antarmuka.

Sam
sumber
5
Service Locator bukan antipattern - Fowler sendiri merujuknya di URL yang Anda poskan. Sementara pola Layanan Locator dapat disalahgunakan (dengan cara yang sama bahwa Singletons disalahgunakan - untuk memisahkan negara global), ini adalah pola yang sangat berguna.
Jonathan Rich
1
Menarik untuk diketahui. Saya selalu mendengarnya disebut sebagai pola anti.
Sam
4
Ini hanya antipattern jika pelacak layanan digunakan untuk menyimpan keadaan global. Lokasi layanan harus menjadi objek stateless setelah instantiation, dan lebih disukai tidak berubah.
Jonathan Rich
XML bukan tipe aman. Saya akan menganggapnya sebagai rencana z setelah setiap pendekatan lain gagal
Richard Tingle
1

Saya akan pergi juga dengan Injeksi Ketergantungan. Ingatlah bahwa DI tidak hanya dilakukan melalui konstruktor, tetapi juga melalui setter Properti. Misalnya, logger bisa disuntikkan sebagai properti.

Juga, Anda mungkin ingin menggunakan wadah IoC yang dapat mengangkat beberapa beban untuk Anda, misalnya dengan menjaga parameter konstruktor untuk hal-hal yang diperlukan saat runtime oleh logika domain Anda (menjaga konstruktor dengan cara yang mengungkapkan maksud dari kelas dan dependensi domain sebenarnya) dan mungkin menyuntikkan kelas pembantu lainnya melalui properti.

Satu langkah lebih jauh yang mungkin ingin Anda tuju adalah Programmnig Berorientasi Aspek, yang diimplementasikan dalam banyak kerangka kerja utama. Ini dapat memungkinkan Anda untuk mencegat (atau "menyarankan" untuk menggunakan terminologi AspectJ) konstruktor kelas dan menyuntikkan properti yang relevan, mungkin diberikan atribut khusus.

Tallmaris
sumber
4
Saya menghindari DI melalui setter karena memperkenalkan jendela waktu di mana objek tidak dalam keadaan diinisialisasi penuh (antara panggilan constructor dan setter). Atau dengan kata lain itu memperkenalkan metode panggilan pesanan (harus memanggil X sebelum Y) yang saya hindari jika memungkinkan.
RokL
1
DI melalui seter properti sangat ideal untuk dependensi opsional. Logging adalah contoh yang bagus. Jika Anda perlu login, maka atur properti Logger, jika tidak, maka jangan atur.
Preston
1

Pabrik membuat pengujian menjadi menyakitkan dan tidak memungkinkan pertukaran implementasi yang mudah. Mereka juga tidak membuat dependensi terlihat (misalnya Anda memeriksa suatu metode, tidak menyadari fakta bahwa ia memanggil metode yang memanggil metode yang memanggil metode yang menggunakan database).

Saya tidak begitu setuju. Setidaknya tidak secara umum.

Pabrik sederhana:

public IFoo GetIFoo()
{
    return new Foo();
}

Injeksi sederhana:

myDependencyInjector.Bind<IFoo>().To<Foo>();

Kedua cuplikan memiliki tujuan yang sama, mereka mengatur tautan antara IFoodan Foo. Yang lainnya hanyalah sintaks.

Mengubah Foomenjadi ADifferentFoomembutuhkan banyak upaya tepat di kedua sampel kode.
Saya pernah mendengar orang berpendapat bahwa injeksi ketergantungan memungkinkan ikatan yang berbeda untuk digunakan, tetapi argumen yang sama dapat dibuat tentang pembuatan pabrik yang berbeda. Memilih ikatan yang tepat sama rumitnya dengan memilih pabrik yang tepat.

Metode pabrik memungkinkan Anda untuk menggunakan misalnya Foodi beberapa tempat dan ADifferentFoodi tempat lain. Beberapa mungkin menyebut ini baik (berguna jika Anda membutuhkannya), beberapa mungkin menyebut ini buruk (Anda bisa melakukan pekerjaan setengah-setengah untuk mengganti semuanya).
Namun, tidak terlalu sulit untuk menghindari ambiguitas ini jika Anda tetap menggunakan metode tunggal yang mengembalikan IFoosehingga Anda selalu memiliki sumber tunggal. Jika Anda tidak ingin menembak diri sendiri di kaki, jangan memegang senapan yang dimuat, atau pastikan untuk tidak mengarahkannya ke kaki Anda.


Dependecy injection secara besar-besaran membengkak daftar argumen konstruktor dan itu mengolesi beberapa aspek di seluruh kode Anda.

Inilah sebabnya mengapa beberapa orang lebih suka untuk secara eksplisit mengambil dependensi dalam konstruktor, seperti ini:

public MyService()
{
    _myFoo = DependencyFramework.Get<IFoo>();
}

Saya pernah mendengar argumen pro (tanpa konstruktor mengasapi), saya pernah mendengar argumen con (menggunakan konstruktor memungkinkan lebih banyak otomatisasi DI).

Secara pribadi, sementara saya telah menghasilkan untuk senior kami yang ingin menggunakan argumen konstruktor, saya telah melihat masalah dengan dropdownlist di VS (kanan atas, untuk menelusuri metode kelas saat ini) di mana nama metode tidak terlihat ketika satu metode tanda tangan lebih panjang dari layar saya (=> konstruktor kembung).

Pada tingkat teknis, saya juga tidak peduli. Pilihan mana pun membutuhkan usaha yang sama untuk mengetik. Dan karena Anda menggunakan DI, Anda biasanya tidak akan memanggil konstruktor secara manual. Tetapi bug Visual Studio UI membuat saya mendukung tidak membengkak argumen konstruktor.


Sebagai catatan tambahan, injeksi ketergantungan dan pabrik tidak saling eksklusif . Saya memiliki kasus-kasus di mana daripada memasukkan dependensi, saya telah memasukkan sebuah pabrik yang menghasilkan dependensi (NInject untungnya memungkinkan Anda untuk menggunakan Func<IFoo>sehingga Anda tidak perlu membuat kelas pabrik yang sebenarnya).

Kasus penggunaan untuk ini jarang terjadi tetapi memang ada.

Flater
sumber
OP bertanya tentang pabrik statis . stackoverflow.com/questions/929021/…
Basilevs
@ Basilevs "Pertanyaannya adalah, bagaimana cara saya menyediakan komponen-komponen ini ke komponen lain."
Anthony Rutledge
1
@Basilevs Semua yang saya katakan, selain catatan tambahan, berlaku untuk pabrik statis juga. Saya tidak yakin apa yang ingin Anda tunjukkan secara spesifik. Apa yang dimaksud dengan tautan?
Flater
Bisakah satu kasus penggunaan seperti menyuntikkan pabrik menjadi kasus di mana seseorang telah membuat kelas permintaan HTTP abstrak, dan menyusun strategi lima kelas anak polimorfik lainnya, satu untuk GET, POST, PUT, PATCH, dan DELETE? Anda tidak dapat mengetahui metode HTTP mana yang akan digunakan setiap kali ketika mencoba untuk mengintegrasikan hierarki kelas tersebut dengan router tipe MVC (yang mungkin sebagian bergantung pada kelas permintaan HTTP. Antarmuka Pesan HTTP PSR-7 jelek.
Anthony Rutledge
@AnthonyRutledge: pabrik DI dapat berarti dua hal (1) Kelas yang memaparkan beberapa metode. Saya pikir ini yang Anda bicarakan. Namun, ini tidak terlalu khusus untuk pabrik. Apakah itu kelas logika bisnis dengan beberapa metode publik, atau pabrik dengan beberapa metode publik, adalah masalah semantik dan tidak membuat perbedaan teknis. (2) Kasus penggunaan khusus-DI untuk pabrik adalah bahwa ketergantungan non-pabrik dipakai satu kali (selama injeksi), sedangkan versi pabrik dapat digunakan untuk instantiate ketergantungan aktual pada tahap selanjutnya (dan mungkin beberapa kali).
Flater
0

Dalam contoh tiruan ini, kelas pabrik digunakan saat runtime untuk menentukan jenis objek permintaan HTTP yang masuk untuk instantiate, berdasarkan metode permintaan HTTP. Pabrik itu sendiri disuntikkan dengan wadah injeksi ketergantungan. Ini memungkinkan pabrik untuk membuat penentuan runtime dan membiarkan wadah injeksi ketergantungan menangani dependensi. Setiap objek permintaan HTTP inbound memiliki setidaknya empat dependensi (superglobals dan objek lainnya).

<?php
namespace TFWD\Factories;

/**
 * A class responsible for instantiating
 * InboundHttpRequest objects (PHP 7.x)
 * 
 * @author Anthony E. Rutledge
 * @version 2.0
 */
class InboundHttpRequestFactory 
{
    private const GET = 'GET';
    private const POST = 'POST';
    private const PUT = 'PUT';
    private const PATCH = 'PATCH';
    private const DELETE = 'DELETE';

    private static $di;
    private static $method;

    // public function __construct(Injector $di, Validator $httpRequestValidator)
    // {
    //    $this->di = $di;
    //    $this->method = $httpRequestValidator->getMethod();
    // }

    public static function setInjector(Injector $di)
    {
        self::$di = $di;
    }    

    public static setMethod(string $method)
    {
        self::$method = $method;
    }

    public static function getRequest()
    {
        if (self::$method == self::GET) {
            return self::$di->get('InboundGetHttpRequest');
        } elseif ((self::$method == self::POST) && empty($_FILES)) {
            return self::$di->get('InboundPostHttpRequest');
        } elseif (self::$method == self::POST) {
            return self::$di->get('InboundFilePostHttpRequest');
        } elseif (self::$method == self::PUT) {
            return self::$di->get('InboundPutHttpRequest');
        } elseif (self::$method == self::PATCH) {
            return self::$di->get('InboundPatchHttpRequest');
        } elseif (self::$method == self::DELETE) {
            return self::$di->get('InboundDeleteHttpRequest');
        } else {
            throw new \RuntimeException("Unexpected HTTP request. Invalid request.");
        }
    }
}

Kode klien untuk pengaturan tipe MVC, dalam terpusat index.php, mungkin terlihat seperti berikut (validasi dihilangkan).

InboundHttpRequestFactory::setInjector($di);
InboundHttpRequestFactory::setMethod($httpRequestValidator->getMethod());
$di->set('InboundHttpRequest', InboundHttpRequestFactory::getRequest());
$router = $di->get('Router');  // The Router class depends on InboundHttpRequest objects.
$router->dispatch(); 

Atau, Anda dapat menghapus sifat statis (dan kata kunci) dari pabrik dan memungkinkan injector ketergantungan untuk mengelola semuanya (karenanya, konstruktor yang dikomentari). Namun, Anda harus mengubah beberapa (bukan konstanta) dari referensi anggota kelas ( self) menjadi anggota contoh ( $this).

Anthony Rutledge
sumber
Downvotes tanpa komentar tidak berguna.
Anthony Rutledge