Belum lama ini saya berada di twitter dengan Igor Vidler Dan Matthias Norback Mengenai dependensi yang disuntikkan penyetel, dependensi tersebut dapat berubah.
Saat menggunakan objek sebagai layanan dalam aplikasi, kami tidak ingin objek tersebut memiliki status. Ini karena kita dapat memanggilnya berkali-kali tanpa mengubah hasilnya. Misalnya, jika kita memiliki layanan email yang mengirimkan email untuk kita, kita ingin layanan tersebut berperilaku sama setiap kali kita memanggilnya di aplikasi kita. Kami tidak ingin layanan berperilaku berbeda karena kami telah menyebutnya di tempat lain dalam permintaan. Kami dapat memastikan bahwa layanan yang kami tulis secara internal tidak mengubah statusnya dengan cara ini. Namun, saat kami menggunakan injeksi penyetel, kami mengambil kendali dari layanan dan memberikannya ke kode yang menggunakan layanan tersebut.
Dua cara utama memasukkan dependensi adalah injeksi konstruktor dan injeksi penyetel. Salah satu keuntungan utama injeksi konstruktor adalah memastikan bahwa kita memasukkan dependensi. Artinya, Anda tidak akan terkejut nantinya jika hal itu tidak terjadi. Keuntungan lainnya adalah pemilihan dependensi tidak dapat diubah. Layanan build memperbaikinya sebagai ketergantungan itu (dengan asumsi kami tidak menyediakan cara lain untuk mengubahnya).
Melalui injeksi penyetel, kita dapat memanggil metode penyetel beberapa kali. Oleh karena itu, pilihan dependensi tidak statis untuk injeksi penyetel. Kami tidak dapat menggunakan layanan ini karena kami tahu bahwa menggunakan parameter yang sama akan memberikan hasil yang sama. Kode lain yang menggunakan layanan ini dapat mengubah ketergantungan antar panggilan. Sekarang, di mana pun layanan digunakan, dependensi berbeda dapat diatur, yang akan mengubah perilakunya.
Misalnya, kita dapat menyetel filter pada layanan email kita. Karena ini opsional, kami memilih untuk menyuntikkannya menggunakan metode penyetel. Jika kami tidak memerlukan filter, maka kami tidak memanggil metode ini. Jika kode lain dapat mengubah ketergantungan filter, kita tidak dapat mengharapkan hasil yang konsisten.
Kita dapat mengubah penyetel agar hanya memperbolehkan ketergantungan disetel satu kali. Kita perlu menulis kode tambahan untuk ini. Masalah utamanya adalah gagal karena sifat opsional dari dependensi yang disediakan oleh injeksi penyetel. Masih belum ada jaminan bahwa perilaku tersebut akan tetap tidak berubah sepanjang umur layanan. Kita bisa menggunakan layanan tersebut sebelum mengatur dependensinya, atau kita bisa menggunakan layanan itu lagi setelah mengatur dependensinya, namun kita akan mendapatkan hasil yang berbeda.
Kita dapat menghindari hal ini dengan memasukkan dependensi ke dalam konstruktor dan menjadikannya opsional. Sekarang ketergantungannya masih opsional, tetapi kami sedang memperbaiki apakah itu disetel pada waktu konstruksi.
BTW, lebih baik melakukan refactor untuk menghindari ketergantungan opsional. Sebaliknya, kita dapat beralih ke penerapan kelas antarmuka layanan yang berbeda. Yang satu tidak bergantung dan yang lainnya wajib.
Jadi apakah ada gunanya injeksi setter? Kegunaan lainnya adalah sebagai metode “penambah”, menambahkan dependensi ke koleksi. Memanggilnya lagi di sini tidak menggantikan ketergantungan namun menambahkannya ke koleksi. Masih dimungkinkan untuk mengubah perilaku di sini dengan menambahkan dependensi tambahan nanti.
Apa yang dapat kita lakukan jika ingin memperbaiki kumpulan ketergantungan? Kita dapat memperbaikinya pada waktu konstruksi dengan meneruskan koleksi lengkap sebagai argumen konstruktor. Lalu, apakah hal ini selalu memungkinkan? Symfony2 menggunakan metode adder untuk menambahkan dependensi di luar definisi layanan asli. Ketergantungan ini biasanya ditambahkan selama proses kompiler. Program kompiler mencari layanan yang diberi tag dan menambahkannya sebagai dependensi ke layanan lain. Hal ini dilakukan dengan menambahkan panggilan ke metode penambah layanan untuk setiap ketergantungan dalam definisi layanan. Berikut ini contoh dari dokumentasi Symfony:
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Reference;
class TransportCompilerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
if (!$container->hasDefinition('acme_mailer.transport_chain')) {
return;
}
$definition = $container->getDefinition(
'acme_mailer.transport_chain'
);
$taggedServices = $container->findTaggedServiceIds(
'acme_mailer.transport'
);
foreach ($taggedServices as $id => $attributes) {
$definition->addMethodCall(
'addTransport',
array(new Reference($id))
);
}
}
}
Masalahnya di sini adalah kode yang menggunakan layanan tersebut mungkin masih menambah ketergantungan lagi nantinya. Jadi bisakah kita mencegah hal ini terjadi? Dalam percakapan Twitter, kami membahas beberapa opsi. Salah satunya adalah memiliki cara untuk menandai bahwa kita sudah selesai menambahkan dependensi. Jika metode ini dipanggil setelah menyetel bendera, pengecualian dapat diberikan. Kita dapat melakukan ini dengan menambahkan metode yang menyetel tanda dan memanggilnya di akhir definisi layanan. Sayangnya, ini terasa seperti solusi yang agak canggung dan bergantung pada memastikan bahwa panggilan tersebut ditambahkan ke metode penguncian.
Pendekatan lain adalah dengan memiliki objek pembangun yang mengumpulkan dependensi dan kemudian menggunakannya untuk mengonfigurasi layanan. Hal ini memerlukan penambahan kompleksitas ekstra, yang tidak menarik. Faktanya, kami menyadari bahwa kontainer layanan sudah melakukan pekerjaan ini. Mengubah cara kerja kompiler saja sudah cukup. Parameter konstruktor pertama dari kelas TransportChain sekarang menjadi kumpulan transport. Pass kompiler sekarang mungkin terlihat seperti ini:
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Reference;
class TransportCompilerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
if (!$container->hasDefinition('acme_mailer.transport_chain')) {
return;
}
$definition = $container->getDefinition(
'acme_mailer.transport_chain'
);
$transports = $definition->getArgument(0);
$taggedServices = $container->findTaggedServiceIds(
'acme_mailer.transport'
);
foreach ($taggedServices as $id => $attributes) {
$transports[] = new Reference($id);
}
$definition->replaceArgument(0, $transports);
}
}
Di sini kita mendapatkan parameter pertama dalam definisi layanan rantai transportasi. Dalam hal ini kita berasumsi bahwa itu telah didefinisikan sebagai suatu himpunan. Kami kemudian melampirkan referensi ke layanan tag ke koleksi yang menyimpan layanan yang ada. Parameter dalam definisi kemudian diganti dengan koleksi yang terisi. Sekarang kita dapat menghapus metode addTransport dari kelas. Hal ini mencegah perubahan lebih lanjut dilakukan pada kode menggunakan rantai transport.
Jadi sepertinya kita dapat menghindari sebagian besar situasi injeksi penyetel tanpa banyak usaha. Kami kemudian memberi diri kami keamanan yang lebih baik dari injeksi konstruktor.