Prinsip Penukaran Dependensi di Jawa

1. Gambaran keseluruhan

Dependency Inversion Principle (DIP) merupakan sebahagian daripada kumpulan prinsip pengaturcaraan berorientasikan objek yang dikenali sebagai SOLID.

Pada tulang kosong, DIP adalah paradigma pengaturcaraan yang mudah - namun kuat - yang dapat kita gunakan untuk menerapkan komponen perisian yang tersusun, sangat terputus dan dapat digunakan kembali .

Dalam tutorial ini, kita akan meneroka berbagai pendekatan untuk menerapkan DIP - satu di Java 8, dan satu di Java 11 menggunakan JPMS (Java Platform Module System).

2. Ketergantungan Suntikan dan Pembalikan Kawalan Bukan Pelaksanaan DIP

Pertama dan terpenting, mari kita buat perbezaan asas untuk mendapatkan asas yang betul: DIP bukan suntikan ketergantungan (DI) atau pembalikan kawalan (IoC) . Walaupun begitu, mereka semua bekerja bersama-sama.

Secara sederhana, DI adalah mengenai membuat komponen perisian untuk secara jelas menyatakan kebergantungan atau kolaborator mereka melalui API mereka, dan bukannya memperolehnya sendiri.

Tanpa DI, komponen perisian digabungkan erat antara satu sama lain. Oleh itu, mereka sukar untuk menggunakan semula, mengganti, mengejek dan menguji, yang menghasilkan reka bentuk yang kaku.

Dengan DI, tanggungjawab menyediakan kebergantungan komponen dan grafik objek pendawaian dipindahkan dari komponen ke kerangka suntikan yang mendasari. Dari perspektif itu, DI hanyalah cara untuk mencapai IoC.

Sebaliknya, IoC adalah corak di mana kawalan aliran aplikasi terbalik . Dengan metodologi pengaturcaraan tradisional, kod khusus kami mempunyai kawalan aliran aplikasi. Sebaliknya, dengan IoC, kawalan dipindahkan ke kerangka luaran atau bekas .

Kerangka kerja adalah pangkalan data yang dapat diperluas, yang menentukan titik kait untuk memasukkan kod kita sendiri .

Pada gilirannya, kerangka kerja memanggil kembali kod kami melalui satu atau lebih subkelas khusus, menggunakan implementasi antara muka, dan melalui anotasi. Kerangka Spring adalah contoh bagus dari pendekatan terakhir ini.

3. Asas DIP

Untuk memahami motivasi di sebalik DIP, mari kita mulakan dengan definisi formal, yang diberikan oleh Robert C. Martin dalam bukunya, Agile Software Development: Principles, Patterns, and Practices :

  1. Modul tahap tinggi tidak boleh bergantung pada modul tahap rendah. Kedua-duanya harus bergantung pada abstraksi.
  2. Abstraksi tidak boleh bergantung pada perincian. Perincian harus bergantung pada abstraksi.

Oleh itu, jelas bahawa intinya, DIP adalah mengenai membalikkan kebergantungan klasik antara komponen tahap tinggi dan tahap rendah dengan menyingkirkan interaksi antara mereka .

Dalam pembangunan perisian tradisional, komponen tahap tinggi bergantung pada komponen tahap rendah. Oleh itu, sukar untuk menggunakan semula komponen tahap tinggi.

3.1. Pilihan Reka Bentuk dan DIP

Mari pertimbangkan kelas StringProcessor sederhana yang mendapat nilai String menggunakan komponen StringReader , dan menulisnya di tempat lain menggunakan komponen StringWriter :

public class StringProcessor { private final StringReader stringReader; private final StringWriter stringWriter; public StringProcessor(StringReader stringReader, StringWriter stringWriter) { this.stringReader = stringReader; this.stringWriter = stringWriter; } public void printString() { stringWriter.write(stringReader.getValue()); } } 

Walaupun pelaksanaan kelas StringProcessor adalah asas, terdapat beberapa pilihan reka bentuk yang dapat kami buat di sini.

Mari kita bahagikan setiap pilihan reka bentuk menjadi item yang berasingan, untuk memahami dengan jelas bagaimana masing-masing dapat mempengaruhi reka bentuk keseluruhan:

  1. StringReader dan StringWriter , komponen tahap rendah, adalah kelas konkrit yang diletakkan dalam pakej yang sama. StringProcessor , komponen tahap tinggi diletakkan dalam pakej yang berbeza. StringProcessor bergantung kepada StringReader dan StringWriter . Tidak ada pembalikan kebergantungan, oleh itu StringProcessor tidak dapat digunakan kembali dalam konteks yang berbeza.
  2. StringReader dan StringWriter adalah antara muka yang diletakkan dalam pakej yang sama bersama dengan pelaksanaannya . StringProcessor sekarang bergantung pada pengambilan, tetapi komponen tahap rendah tidak. Kami belum mencapai pembalikan pergantungan.
  3. StringReader dan StringWriter adalah antara muka yang diletakkan dalam pakej yang sama bersama dengan StringProcessor . Sekarang, StringProcessor mempunyai hak milik eksplisit yang jelas. StringProcessor, StringReader, dan StringWriter semuanya bergantung pada abstraksi. Kami telah mencapai pembalikan kebergantungan dari atas ke bawah dengan mengaburkan interaksi antara komponen . StringProcessor kini boleh digunakan semula dalam konteks yang berbeza.
  4. StringReader dan StringWriter adalah antara muka yang diletakkan dalam pakej berasingan dari StringProcessor . Kami mencapai penyongsangan kebergantungan, dan ia juga lebih mudah untuk menggantikan StringReader dan StringWriter pelaksanaan. StringProcessor juga dapat digunakan semula dalam konteks yang berbeza.

Dari semua senario di atas, hanya item 3 dan 4 yang merupakan pelaksanaan DIP yang sah.

3.2. Menentukan Pemilikan Abstraksi

Item 3 adalah pelaksanaan DIP langsung, di mana komponen tingkat tinggi dan abstraksi ditempatkan dalam paket yang sama. Oleh itu, komponen peringkat tinggi memiliki abstraksi . Dalam pelaksanaan ini, komponen tahap tinggi bertanggungjawab untuk menentukan protokol abstrak di mana ia berinteraksi dengan komponen tahap rendah.

Begitu juga, item 4 adalah pelaksanaan DIP yang lebih terputus. Dalam varian corak ini, baik komponen tahap tinggi maupun tahap rendah memiliki hak milik abstraksi .

Abstraksi diletakkan dalam lapisan yang berasingan, yang memudahkan pertukaran komponen tahap rendah. Pada masa yang sama, semua komponen diasingkan antara satu sama lain, yang menghasilkan enkapsulasi yang lebih kuat.

3.3. Memilih Tahap Abstraksi yang Betul

Dalam kebanyakan kes, memilih abstraksi yang akan digunakan oleh komponen tahap tinggi harus cukup mudah, tetapi dengan satu peringatan yang perlu diperhatikan: tahap abstraksi.

Dalam contoh di atas, kami menggunakan DI untuk menyuntikkan jenis StringReader ke kelas StringProcessor . Ini akan berkesan selagi tahap abstraksi StringReader dekat dengan domain StringProcessor .

Sebaliknya, kita hanya akan kehilangan faedah intrinsik DIP jika StringReader , misalnya, objek File yang membaca nilai String dari file. Dalam kes itu, tahap abstraksi StringReader akan jauh lebih rendah daripada tahap domain StringProcessor .

Sederhananya, tahap abstraksi yang akan digunakan oleh komponen tahap tinggi untuk beroperasi dengan yang rendah mesti selalu dekat dengan domain yang sebelumnya .

4. Java 8 Pelaksanaan

Kami sudah melihat secara mendalam konsep utama DIP, jadi sekarang kami akan meneroka beberapa pelaksanaan praktik corak di Java 8.

4.1. Pelaksanaan DIP Langsung

Let's create a demo application that fetches some customers from the persistence layer and processes them in some additional way.

The layer's underlying storage is usually a database, but to keep the code simple, here we'll use a plain Map.

Let's start by defining the high-level component:

public class CustomerService { private final CustomerDao customerDao; // standard constructor / getter public Optional findById(int id) { return customerDao.findById(id); } public List findAll() { return customerDao.findAll(); } }

As we can see, the CustomerService class implements the findById() and findAll() methods, which fetch customers from the persistence layer using a simple DAO implementation. Of course, we could've encapsulated more functionality in the class, but let's keep it like this for simplicity's sake.

In this case, the CustomerDao type is the abstraction that CustomerService uses for consuming the low-level component.

Since this a direct DIP implementation, let's define the abstraction as an interface in the same package of CustomerService:

public interface CustomerDao { Optional findById(int id); List findAll(); } 

By placing the abstraction in the same package of the high-level component, we're making the component responsible for owning the abstraction. This implementation detail is what really inverts the dependency between the high-level component and the low-level one.

In addition, the level of abstraction of CustomerDao is close to the one of CustomerService, which is also required for a good DIP implementation.

Now, let's create the low-level component in a different package. In this case, it's just a basic CustomerDao implementation:

public class SimpleCustomerDao implements CustomerDao { // standard constructor / getter @Override public Optional findById(int id) { return Optional.ofNullable(customers.get(id)); } @Override public List findAll() { return new ArrayList(customers.values()); } }

Finally, let's create a unit test to check the CustomerService class' functionality:

@Before public void setUpCustomerServiceInstance() { var customers = new HashMap(); customers.put(1, new Customer("John")); customers.put(2, new Customer("Susan")); customerService = new CustomerService(new SimpleCustomerDao(customers)); } @Test public void givenCustomerServiceInstance_whenCalledFindById_thenCorrect() { assertThat(customerService.findById(1)).isInstanceOf(Optional.class); } @Test public void givenCustomerServiceInstance_whenCalledFindAll_thenCorrect() { assertThat(customerService.findAll()).isInstanceOf(List.class); } @Test public void givenCustomerServiceInstance_whenCalledFindByIdWithNullCustomer_thenCorrect() { var customers = new HashMap(); customers.put(1, null); customerService = new CustomerService(new SimpleCustomerDao(customers)); Customer customer = customerService.findById(1).orElseGet(() -> new Customer("Non-existing customer")); assertThat(customer.getName()).isEqualTo("Non-existing customer"); }

The unit test exercises the CustomerService API. And, it also shows how to manually inject the abstraction into the high-level component. In most cases, we'd use some kind of DI container or framework to accomplish this.

Additionally, the following diagram shows the structure of our demo application, from a high-level to a low-level package perspective:

4.2. Alternative DIP Implementation

As we discussed before, it's possible to use an alternative DIP implementation, where we place the high-level components, the abstractions, and the low-level ones in different packages.

For obvious reasons, this variant is more flexible, yields better encapsulation of the components, and makes it easier to replace the low-level components.

Of course, implementing this variant of the pattern boils down to just placing CustomerService, MapCustomerDao, and CustomerDao in separate packages.

Therefore, a diagram is sufficient for showing how each component is laid out with this implementation:

5. Java 11 Modular Implementation

It's fairly easy to refactor our demo application into a modular one.

This is a really nice way to demonstrate how the JPMS enforces best programming practices, including strong encapsulation, abstraction, and component reuse through the DIP.

We don't need to reimplement our sample components from scratch. Hence, modularizing our sample application is just a matter of placing each component file in a separate module, along with the corresponding module descriptor.

Here's how the modular project structure will look:

project base directory (could be anything, like dipmodular) |- com.baeldung.dip.services module-info.java   |- com |- baeldung |- dip |- services CustomerService.java |- com.baeldung.dip.daos module-info.java   |- com |- baeldung |- dip |- daos CustomerDao.java |- com.baeldung.dip.daoimplementations module-info.java |- com |- baeldung |- dip |- daoimplementations SimpleCustomerDao.java |- com.baeldung.dip.entities module-info.java |- com |- baeldung |- dip |- entities Customer.java |- com.baeldung.dip.mainapp module-info.java |- com |- baeldung |- dip |- mainapp MainApplication.java 

5.1. The High-Level Component Module

Let's start by placing the CustomerService class in its own module.

We'll create this module in the root directory com.baeldung.dip.services, and add the module descriptor, module-info.java:

module com.baeldung.dip.services { requires com.baeldung.dip.entities; requires com.baeldung.dip.daos; uses com.baeldung.dip.daos.CustomerDao; exports com.baeldung.dip.services; }

For obvious reasons, we won't go into the details on how the JPMS works. Even so, it's clear to see the module dependencies just by looking at the requires directives.

The most relevant detail worth noting here is the uses directive. It states that the module is a client module that consumes an implementation of the CustomerDao interface.

Of course, we still need to place the high-level component, the CustomerService class, in this module. So, within the root directory com.baeldung.dip.services, let's create the following package-like directory structure: com/baeldung/dip/services.

Finally, let's place the CustomerService.java file in that directory.

5.2. The Abstraction Module

Likewise, we need to place the CustomerDao interface in its own module. Therefore, let's create the module in the root directory com.baeldung.dip.daos, and add the module descriptor:

module com.baeldung.dip.daos { requires com.baeldung.dip.entities; exports com.baeldung.dip.daos; }

Now, let's navigate to the com.baeldung.dip.daos directory and create the following directory structure: com/baeldung/dip/daos. Let's place the CustomerDao.java file in that directory.

5.3. The Low-Level Component Module

Logically, we need to put the low-level component, SimpleCustomerDao, in a separate module, too. As expected, the process looks very similar to what we just did with the other modules.

Let's create the new module in the root directory com.baeldung.dip.daoimplementations, and include the module descriptor:

module com.baeldung.dip.daoimplementations { requires com.baeldung.dip.entities; requires com.baeldung.dip.daos; provides com.baeldung.dip.daos.CustomerDao with com.baeldung.dip.daoimplementations.SimpleCustomerDao; exports com.baeldung.dip.daoimplementations; }

In a JPMS context, this is a service provider module, since it declares the provides and with directives.

In this case, the module makes the CustomerDao service available to one or more consumer modules, through the SimpleCustomerDao implementation.

Let's keep in mind that our consumer module, com.baeldung.dip.services, consumes this service through the uses directive.

This clearly shows how simple it is to have a direct DIP implementation with the JPMS, by just defining consumers, service providers, and abstractions in different modules.

Likewise, we need to place the SimpleCustomerDao.java file in this new module. Let's navigate to the com.baeldung.dip.daoimplementations directory, and create a new package-like directory structure with this name: com/baeldung/dip/daoimplementations.

Finally, let's place the SimpleCustomerDao.java file in the directory.

5.4. The Entity Module

Additionally, we have to create another module where we can place the Customer.java class. As we did before, let's create the root directory com.baeldung.dip.entities and include the module descriptor:

module com.baeldung.dip.entities { exports com.baeldung.dip.entities; }

In the package's root directory, let's create the directory com/baeldung/dip/entities and add the following Customer.java file:

public class Customer { private final String name; // standard constructor / getter / toString }

5.5. The Main Application Module

Next, we need to create an additional module that allows us to define our demo application's entry point. Therefore, let's create another root directory com.baeldung.dip.mainapp and place in it the module descriptor:

module com.baeldung.dip.mainapp { requires com.baeldung.dip.entities; requires com.baeldung.dip.daos; requires com.baeldung.dip.daoimplementations; requires com.baeldung.dip.services; exports com.baeldung.dip.mainapp; }

Now, let's navigate to the module's root directory, and create the following directory structure: com/baeldung/dip/mainapp. In that directory, let's add a MainApplication.java file, which simply implements a main() method:

public class MainApplication { public static void main(String args[]) { var customers = new HashMap(); customers.put(1, new Customer("John")); customers.put(2, new Customer("Susan")); CustomerService customerService = new CustomerService(new SimpleCustomerDao(customers)); customerService.findAll().forEach(System.out::println); } }

Akhirnya, mari kita menyusun dan menjalankan aplikasi demo - sama ada dari dalam IDE kami atau dari konsol arahan.

Seperti yang dijangkakan, kita akan melihat senarai objek Pelanggan yang dicetak ke konsol ketika aplikasi dimulakan:

Customer{name=John} Customer{name=Susan} 

Di samping itu, rajah berikut menunjukkan kebergantungan setiap modul aplikasi:

6. Kesimpulannya

Dalam tutorial ini, kami menyelami konsep utama DIP, dan kami juga menunjukkan pelaksanaan pola yang berbeza di Java 8 dan Java 11 , dengan yang terakhir menggunakan JPMS.

Semua contoh untuk pelaksanaan Java 8 DIP dan pelaksanaan Java 11 tersedia di GitHub.