Halo semuanya!
Kami akhirnya memiliki kontrak untuk memperbarui buku Mark Siman, "
Dependency Injection in .NET " - yang terpenting adalah ia menyelesaikannya sesegera mungkin. Kami juga memiliki
buku di editor Dinesh Rajput yang disegani tentang pola desain di Spring 5, di mana salah satu babnya juga dikhususkan untuk implementasi dependensi.
Kami telah lama mencari bahan yang menarik yang akan mengingat kekuatan paradigma DI dan memperjelas minat kami terhadapnya - dan sekarang telah ditemukan. Benar, penulis lebih suka memberi contoh di Go. Kami harap ini tidak mencegah Anda dari mengikuti pemikirannya dan membantu untuk memahami prinsip-prinsip umum inversi kontrol dan bekerja dengan antarmuka, jika topik ini dekat dengan Anda.
Pewarnaan emosional dari aslinya sedikit lebih tenang, jumlah tanda seru dalam terjemahan berkurang. Selamat membaca!
Menggunakan
antarmuka adalah teknik yang dapat dipahami yang memungkinkan Anda membuat kode yang mudah diuji dan mudah diperluas. Saya telah berulang kali diyakinkan bahwa ini adalah alat desain arsitektur paling kuat dari semuanya.
Tujuan artikel ini adalah untuk menjelaskan antarmuka apa, bagaimana mereka digunakan, dan bagaimana mereka memberikan ekstensibilitas dan testabilitas kode. Akhirnya, artikel tersebut harus menunjukkan bagaimana antarmuka dapat membantu mengoptimalkan manajemen pengiriman perangkat lunak dan menyederhanakan perencanaan!
AntarmukaAntarmuka menggambarkan kontrak. Bergantung pada bahasa atau kerangka kerjanya, penggunaan antarmuka dapat ditentukan secara eksplisit atau implisit. Jadi, dalam bahasa Go,
antarmuka didiktekan secara eksplisit . Jika Anda mencoba menggunakan entitas sebagai antarmuka, tetapi tidak sepenuhnya konsisten dengan aturan antarmuka ini, kesalahan waktu kompilasi akan terjadi. Misalnya, menjalankan contoh di atas, kami mendapatkan kesalahan berikut:
prog.go:22:85: cannot use BadPricer literal (type BadPricer) as type StockPricer in argument to isPricerHigherThan100: BadPricer does not implement StockPricer (missing CurrentPrice method) Program exited.
Antarmuka adalah alat untuk membantu melepaskan penelepon dari callee, ini dilakukan dengan menggunakan kontrak.
Mari kita konkretkan masalah ini menggunakan contoh program untuk perdagangan pertukaran otomatis. Program pedagang akan dipanggil dengan harga beli yang ditetapkan dan simbol ticker. Kemudian program akan pergi ke bursa untuk mencari tahu kutipan terbaru dari ticker ini. Selanjutnya, jika harga pembelian untuk ticker ini tidak melebihi harga yang ditentukan, program akan melakukan pembelian.

Dalam bentuk yang disederhanakan, arsitektur program ini dapat direpresentasikan sebagai berikut. Dari contoh di atas jelas bahwa operasi untuk memperoleh harga saat ini secara langsung tergantung pada protokol HTTP, di mana program menghubungi layanan pertukaran.
Kondisi
Action
juga secara langsung bergantung pada HTTP. Dengan demikian, kedua negara harus sepenuhnya memahami cara menggunakan HTTP untuk mengekstrak data pertukaran dan / atau menyelesaikan transaksi.
Begini tampilannya:
func analyze(ticker string, maxTradePrice float64) (bool, err) { resp, err := http.Get( "http://stock-service.com/currentprice/" + ticker ) if err != nil {
Di sini, penelepon (
analyze
) secara langsung bergantung pada HTTP. Dia perlu tahu bagaimana permintaan HTTP dirumuskan. Bagaimana penguraian mereka dilakukan. Cara menangani percobaan ulang, batas waktu, otentikasi, dll. Dia memiliki
pegangan yang
erat pada
http
.
Setiap kali kita memanggil analisis, kita juga harus memanggil perpustakaan http
.
Bagaimana antarmuka dapat membantu kami di sini? Dalam kontrak yang disediakan oleh antarmuka, Anda bisa mendeskripsikan
perilaku , bukan
implementasi spesifik.
type StockExchange interface { CurrentPrice(ticker string) float64 }
Di atas mendefinisikan konsep
StockExchange
. Di sini dikatakan bahwa
StockExchange
mendukung pemanggilan satu-satunya fungsi
CurrentPrice
. Ketiga garis ini bagi saya merupakan teknik arsitektur paling kuat dari semuanya. Mereka membantu kita mengontrol dependensi aplikasi jauh lebih percaya diri. Berikan pengujian. Berikan ekstensibilitas.
Injeksi KetergantunganUntuk memahami sepenuhnya nilai antarmuka, Anda harus menguasai teknik yang disebut "injeksi ketergantungan".
Ketergantungan injeksi berarti bahwa pemanggil menyediakan sesuatu yang dibutuhkan oleh pemanggil. Biasanya terlihat seperti ini: penelepon mengkonfigurasi objek, dan kemudian meneruskannya ke callee. Kemudian pihak yang disebut abstrak dari konfigurasi dan implementasi. Dalam hal ini, ada mediasi yang dikenal. Pertimbangkan permintaan ke layanan Istirahat HTTP. Untuk mengimplementasikan klien, kita perlu menggunakan perpustakaan HTTP yang dapat merumuskan, mengirim, dan menerima permintaan HTTP.
Jika kami menempatkan permintaan HTTP di belakang antarmuka, maka penelepon dapat dilepaskan, dan dia akan "tidak sadar" bahwa permintaan HTTP benar-benar terjadi.
Penelepon seharusnya hanya membuat panggilan fungsi generik. Ini bisa berupa panggilan lokal, panggilan jarak jauh, panggilan HTTP, panggilan RPC, dll. Penelepon tidak mengetahui apa yang terjadi, dan biasanya itu cocok untuknya, selama dia mendapatkan hasil yang diharapkan. Berikut ini menunjukkan seperti apa injeksi dependensi dalam metode
analyze
kami.
func analyze(se StockExchange, ticker string, maxTradePrice float64) (bool, error) { currentPrice := se.CurrentPrice(ticker) var hasTraded bool var err error if currentPrice <= maximumTradePrice { err = doTrade(ticker, currentPrice) if err == nil { hasTraded = true } } return hasTraded, err }
Saya tidak pernah berhenti kagum dengan apa yang terjadi di sini. Kami benar-benar membalik pohon ketergantungan kami dan mulai mengendalikan seluruh program dengan lebih baik. Terlebih lagi, bahkan secara visual seluruh implementasi telah menjadi lebih bersih dan lebih mudah dipahami. Kami melihat dengan jelas bahwa metode analisis harus memilih harga saat ini, memeriksa apakah harga ini cocok untuk kami, dan jika demikian, buat kesepakatan.
Yang terpenting, dalam hal ini kami melepaskan penelepon dari penelepon. Karena penelepon dan seluruh implementasi dipisahkan dari yang dipanggil menggunakan antarmuka, Anda dapat memperluas antarmuka dengan membuat banyak implementasi yang berbeda dari itu. Antarmuka memungkinkan Anda untuk membuat banyak implementasi spesifik yang berbeda tanpa perlu mengubah kode dari pihak yang dipanggil!

Status "dapatkan harga saat ini" dalam program ini hanya bergantung pada antarmuka
StockExchange
. Implementasi ini tidak tahu
apa -
apa tentang bagaimana menghubungi layanan pertukaran, bagaimana harga disimpan atau bagaimana permintaan dibuat. Ketidaktahuan yang sangat bahagia. Apalagi bilateral. Implementasi
HTTPStockExchange
juga tidak tahu apa-apa tentang analisis. Tentang konteks di mana analisis akan dilakukan, ketika itu dilakukan - karena tantangan terjadi secara tidak langsung.
Karena fragmen program (yang bergantung pada antarmuka) tidak perlu diubah ketika mengubah / menambah / menghapus implementasi spesifik,
desain seperti itu ternyata tahan lama . Misalkan kita menemukan bahwa
StockService
sangat sering tidak tersedia.
Bagaimana contoh di atas berbeda dari memanggil suatu fungsi? Saat menerapkan panggilan fungsi, implementasinya juga akan menjadi lebih bersih. Perbedaannya adalah bahwa ketika Anda memanggil fungsi, kita masih harus menggunakan HTTP. Metode
analyze
hanya akan mendelegasikan tugas fungsi, yang seharusnya memanggil
http
, daripada memanggil
http
itu sendiri secara langsung. Seluruh kekuatan teknik ini terletak pada "injeksi", yaitu, bahwa penelepon menyediakan antarmuka ke callee. Inilah tepatnya bagaimana inversi ketergantungan terjadi, di mana mendapatkan harga hanya bergantung pada antarmuka, dan bukan pada implementasinya.
Implementasi multipel dari kotakPada tahap ini, kami memiliki fungsi
analyze
dan antarmuka
StockExchange
, tetapi kami sebenarnya tidak dapat melakukan sesuatu yang berguna. Baru saja mengumumkan program kami. Saat ini, tidak mungkin untuk menyebutnya, karena kami masih belum memiliki implementasi spesifik tunggal yang akan memenuhi persyaratan antarmuka kami.
Penekanan utama dalam diagram berikut dibuat pada status "dapatkan harga saat ini" dan ketergantungannya pada antarmuka
StockExchange
. Berikut ini menunjukkan bagaimana dua implementasi yang sama sekali berbeda hidup berdampingan, dan mendapatkan harga saat ini tidak di ketahui. Selain itu, kedua implementasi tidak terkait satu sama lain, masing-masing hanya bergantung pada antarmuka
StockExchange
.

Produksi
Implementasi HTTP asli sudah ada dalam implementasi
analyze
utama; semua yang tersisa bagi kita adalah mengekstraknya dan merangkumnya di belakang implementasi konkret dari antarmuka.
type HTTPStockExchange struct {} func (se HTTPStockExchange) CurrentPrice(ticker string) float64 { resp, err := http.Get( "http://stock-service.com/currentprice/" + ticker ) if err != nil {
Kode yang sebelumnya kita tautkan ke fungsi analisis sekarang otonom dan memenuhi antarmuka
StockExchange
, yaitu, sekarang kita bisa meneruskannya untuk
analyze
. Seperti yang Anda ingat dari diagram di atas, analisis tidak lagi dikaitkan dengan ketergantungan HTTP. Menggunakan antarmuka,
analyze
tidak "membayangkan" apa yang terjadi di balik layar. Dia hanya tahu bahwa dia akan dijamin diberi objek yang bisa dia panggil
CurrentPrice
.
Juga di sini kita mengambil keuntungan dari sifat khas enkapsulasi. Sebelumnya, ketika http-permintaan terikat untuk dianalisis, satu-satunya cara untuk berkomunikasi dengan pertukaran melalui http tidak langsung - melalui metode
analyze
. Ya, kami bisa merangkum panggilan-panggilan ini ke dalam fungsi-fungsi dan menjalankan fungsi secara independen, tetapi antarmuka memaksa kami untuk melepaskan penelepon dari penelepon. Sekarang kita dapat menguji
HTTPStockExchange
terlepas dari penelepon. Ini secara fundamental mempengaruhi ruang lingkup pengujian kami dan bagaimana kami memahami dan menanggapi kegagalan pengujian.
PengujianDalam kode yang ada, kami memiliki struktur
HTTPStockService
, yang memungkinkan kami memastikan secara terpisah bahwa ia dapat berkomunikasi dengan layanan pertukaran dan mengurai respons yang diterima darinya. Tetapi sekarang mari kita pastikan bahwa analisis dapat menangani respons dari antarmuka
StockExchange
, apalagi, bahwa operasi ini dapat diandalkan dan dapat direproduksi.
currentPrice := se.CurrentPrice(ticker) if currentPrice <= maxTradePrice { err := doTrade(ticker, currentPrice) }
KITA BISA menggunakan implementasi dengan HTTP, tetapi itu akan memiliki banyak kelemahan. Melakukan panggilan jaringan dalam pengujian unit mungkin lambat, terutama untuk layanan eksternal. Karena keterlambatan dan koneksi jaringan yang tidak stabil, tes bisa berubah menjadi tidak dapat diandalkan. Selain itu, jika kami memerlukan pengujian dengan pernyataan bahwa kami dapat menyelesaikan transaksi, dan pengujian dengan pernyataan bahwa kami dapat menyaring kasus-kasus di mana transaksi TIDAK boleh disimpulkan, akan sulit untuk menemukan data produksi nyata yang dapat diandalkan untuk memenuhi kedua hal ini. kondisi. Seseorang dapat memilih
maxTradePrice
, meniru secara artifisial setiap kondisi dengan cara ini, misalnya, dengan
maxTradePrice := -100
transaksi tidak boleh diselesaikan, dan
maxTradePrice := 10000000
jelas harus diakhiri dengan transaksi.
Tetapi apa yang terjadi jika kuota tertentu dialokasikan kepada kami pada layanan pertukaran? Atau jika kita harus membayar akses? Akankah kita benar-benar (dan harus) membayar atau menghabiskan kuota kita ketika datang ke unit test? Idealnya, tes harus dijalankan sesering mungkin, sehingga harus cepat, murah, dan dapat diandalkan. Saya pikir dari paragraf ini jelas mengapa menggunakan versi dengan HTTP murni tidak rasional dalam hal pengujian!
Ada cara yang lebih baik, dan itu melibatkan penggunaan antarmuka!Memiliki antarmuka, Anda dapat dengan hati-hati membuat implementasi
StockExchange
, yang memungkinkan kami
analyze
dengan cepat, aman, dan andal.
type StubExchange struct { Price float64 } func (se StubExchange) CurrentPrice(ticker string) float64 { return se.Price } func TestAnalyze_MakeTrade(t *testing.T) { se := StubExchange{Price: 10} maxTradePrice := 11 traded, err := analyze(se, "TSLA", maxTradePrice) if err != nil { t.Errorf("expected err == nil received: %s", err) } if !traded { t.Error("expected traded == true") } } func TestAnalyze_DontTrade(t *testing.T) { se := StubExchange{Price: 10} maxTradePrice := 9 traded, err := analyze(se, "TSLA", maxTradePrice)
Potongan dari layanan pertukaran digunakan di atas, berkat cabang yang menarik bagi kami dalam
analyze
diluncurkan. Kemudian, pernyataan dibuat di setiap tes untuk memastikan bahwa analisis melakukan apa yang diperlukan. Meskipun ini adalah program pengujian, pengalaman saya menunjukkan bahwa komponen / arsitektur, di mana antarmuka digunakan kira-kira dengan cara ini, diuji dengan cara ini untuk daya tahan dalam kode pertempuran juga !!! Berkat antarmuka, kita dapat menggunakan
StockExchange
dikontrol dalam memori, yang menyediakan pengujian yang andal, mudah dapat dikonfigurasi, mudah dimengerti, direproduksi, dan secepat kilat !!!
Lepas sematan - Konfigurasi PeneleponSekarang kita telah membahas bagaimana menggunakan antarmuka untuk melepaskan penelepon dari callee, dan bagaimana melakukan beberapa implementasi, kami masih belum menyentuh pada aspek kritis. Bagaimana cara mengkonfigurasi dan menyediakan implementasi spesifik pada waktu yang ditentukan secara ketat? Anda dapat langsung memanggil fungsi analisis, tetapi apa yang harus dilakukan dalam konfigurasi produksi?
Di sinilah implementasi dependensi berguna.
func main() { var ticker = flag.String("ticker", "", "stock ticker symbol to trade for") var maxTradePrice = flag.Float64("maxtradeprice", "", "max price to pay for a share of the ticker symbol." se := HTTPStockExchange{} analyze(se, *ticker, *maxTradePrice) }
Sama seperti dalam kasus pengujian kami, implementasi spesifik StockExchange yang akan digunakan dengan
analyze
dikonfigurasikan oleh penelepon di luar analisis. Kemudian dilewatkan (disuntikkan) untuk
analyze
. Ini memastikan bahwa analisis TIDAK ADA yang diketahui tentang bagaimana
HTTPStockExchange
dikonfigurasi. Mungkin kami ingin memberikan domain http yang akan kami gunakan dalam bentuk bendera baris perintah, dan kemudian menganalisis tidak harus berubah. Atau apa yang harus dilakukan jika kita perlu menyediakan semacam otentikasi atau token untuk mengakses
HTTPStockExchange
, yang akan diekstraksi dari lingkungan? Sekali lagi, analisis tidak boleh berubah.
Konfigurasi terjadi pada tingkat di luar
analyze
, sehingga benar-benar membebaskan analisis dari kebutuhan untuk mengkonfigurasi dependensinya sendiri. Dengan demikian, pemisahan tugas yang ketat tercapai.
Keputusan rakMungkin contoh di atas sudah cukup, tetapi masih ada banyak keuntungan lain untuk antarmuka dan injeksi ketergantungan. Antarmuka memungkinkan menunda keputusan tentang implementasi spesifik. Meskipun keputusan mengharuskan kita untuk memutuskan perilaku apa yang akan kita dukung, mereka masih memungkinkan kita untuk membuat keputusan tentang implementasi spesifik nanti. Misalkan kita tahu bahwa kita ingin melakukan transaksi otomatis, tetapi belum yakin penyedia penawaran mana yang akan kita gunakan. Kelas solusi serupa terus ditangani ketika bekerja dengan gudang data. Apa yang harus digunakan oleh program kami: mysql, postgres, redis, sistem file, cassandra? Pada akhirnya, semua ini adalah detail implementasi, dan antarmuka memungkinkan kita untuk menunda keputusan akhir tentang masalah ini. Mereka memungkinkan kami untuk mengembangkan logika bisnis dari program kami, dan beralih ke solusi teknologi spesifik pada saat terakhir!
Terlepas dari kenyataan bahwa teknik ini sendiri menyisakan banyak kemungkinan, sesuatu yang ajaib terjadi pada tingkat perencanaan proyek. Bayangkan apa yang akan terjadi jika kita menambahkan satu lagi ketergantungan pada antarmuka pertukaran.

Di sini kita akan mengonfigurasi ulang arsitektur kita dalam bentuk grafik asiklik terarah, sehingga segera setelah kita menyetujui rincian antarmuka pertukaran, kita bisa SEPENUHNYA terus bekerja dengan pipa menggunakan
HTTPStockExchange
. Kami menciptakan situasi di mana penambahan orang baru ke proyek membantu kami bergerak lebih cepat. Dengan mengutak-atik arsitektur kita dengan cara ini, kita lebih baik melihat di mana, kapan, dan untuk berapa lama kita bisa melibatkan orang-orang tambahan dalam proyek untuk mempercepat pengiriman seluruh proyek. Selain itu, karena koneksi antara antarmuka kami lemah, biasanya mudah untuk terlibat dalam pekerjaan, dimulai dengan antarmuka implementasi. Anda dapat mengembangkan, menguji, dan menguji
HTTPStockExchange
sepenuhnya terlepas dari program kami!
Analisis dependensi arsitektur dan perencanaan sesuai dengan dependensi ini dapat secara drastis mempercepat proyek. Dengan menggunakan teknik khusus ini, saya dapat dengan cepat menyelesaikan proyek yang diberikan beberapa bulan.
Di depanSekarang harus lebih jelas bagaimana antarmuka dan implementasi dependensi memastikan daya tahan program yang dirancang. Misalkan kita mengubah penyedia kutipan kami, atau mulai mengalirkan kuota dan menyimpannya secara real time; ada banyak kemungkinan lain yang Anda inginkan. Metode analisis dalam bentuk saat ini akan mendukung implementasi yang cocok untuk integrasi dengan antarmuka
StockExchange
.
se.CurrentPrice(ticker)
Dengan demikian, dalam banyak kasus, Anda dapat melakukannya tanpa perubahan. Tidak semuanya, tetapi dalam kasus-kasus yang dapat diprediksi yang mungkin kita temui. Kami tidak hanya kebal dari kebutuhan untuk mengubah kode
analyze
dan memeriksa ulang fungsionalitas utamanya, tetapi kami dapat dengan mudah menawarkan implementasi baru atau beralih di antara pemasok. Kami juga dapat dengan lancar memperluas atau memperbarui implementasi spesifik yang sudah kami miliki tanpa perlu mengubah atau memeriksa ulang
analyze
!
Saya berharap contoh-contoh di atas secara meyakinkan menunjukkan bagaimana melemahnya komunikasi antara entitas dalam program melalui penggunaan antarmuka sepenuhnya reorientasi dependensi dan memisahkan penelepon dari penelepon. Berkat detasemen ini, program tidak bergantung pada implementasi spesifik, tetapi tergantung pada
perilaku tertentu. Perilaku ini dapat disediakan oleh berbagai implementasi. Prinsip desain kritis ini juga disebut
mengetik bebek .
Konsep antarmuka dan ketergantungan pada perilaku, dan bukan pada implementasi, sangat kuat sehingga saya menganggap antarmuka sebagai bahasa primitif - ya, ini sangat radikal. Saya berharap contoh-contoh yang dibahas di atas ternyata cukup meyakinkan, dan Anda akan setuju bahwa antarmuka dan injeksi ketergantungan harus digunakan sejak awal proyek. Di hampir semua proyek yang saya kerjakan, diperlukan bukan hanya satu, tetapi setidaknya dua implementasi: untuk produksi dan untuk pengujian.