ホームページ  >  記事  >  Java  >  D - 依存性逆転の原則(DIP)

D - 依存性逆転の原則(DIP)

Mary-Kate Olsen
Mary-Kate Olsenオリジナル
2024-10-06 06:13:03589ブラウズ

D - Dependency Inversion Principle(DIP)

DIP (依存性反転原理) を理解する前に、高レベルおよび低レベルのモジュールと抽象化が何であるかを知ることが重要です。

高レベルモジュール:

高レベル モジュールとは、コア機能またはビジネス ロジックを管理するアプリケーションの部分を指します。これらのモジュールは通常、アプリケーションのより大規模で重要な機能を処理します。一般に、高レベルのモジュールはより広範で複雑なタスクを実行し、作業を完了するために他のモジュールに依存する場合があります。

たとえば、 電子商取引アプリケーションを構築している場合、注文管理システムは上位モジュールになります。注文の受け取り、処理、配達の管理などの主要なタスクを処理します。

低レベルモジュール:

一方、低レベルのモジュールは、特定の小規模なタスクを担当します。これらのモジュールは、アプリケーションの基本レベルで動作し、データベースへのデータの保存、ネットワーク設定の構成など、より詳細な作業を実行します。通常、低レベル モジュールは、高レベル モジュールがタスクを実行するのを支援します。

電子商取引の例を続けると、ペイメント ゲートウェイの統合は低レベルのモジュールになります。その仕事は特に支払いを処理することであり、これは注文管理システムのより広範な機能と比較して、より特殊なタスクです。

抽象化:

簡単に言うと、抽象化とは、システムまたはプロセスの一般的なアイデアまたは表現を指します。何が起こるべきかを定義しますが、それがどのように起こるかについては指定しません。基礎となる実装の詳細には踏み込まず、重要な機能に焦点を当てています。

例:

車を運転するところを想像してみてください。車の運転の概念 (つまり、抽象化) には次のものが含まれます。

  • 車の始動
  • 車を停める

ただし、抽象化では、これらのアクションがどのように実行されるかは指定されていません。それがどのような種類の車であるか、どのような種類のエンジンが搭載されているか、または車を始動または停止させる具体的なメカニズムについては説明されません。これは、何を行う必要があるかについての一般的なアイデアを提供するだけです。

ソフトウェア設計における抽象化:

ソフトウェア設計において、抽象化とは、ルールやアクションのセットを定義するインターフェースや抽象クラスを使用して一般的な契約や構造を作成することを意味しますが、それらのアクションの具体的な実装は分離されたままになります。これにより、どのように行うかを指定せずに、何を行う必要があるかを定義できます。

例:

支払いを処理するメソッドを宣言する PaymentInterface を使用できます。このインターフェイスは支払いを処理する必要があることを指定しますが、支払いがどのように処理されるかは定義しません。これは、PayPal、Stripe、その他の支払い方法などのさまざまな実装を通じて行うことができます。

本質的に、抽象化は一般的な機能に重点を置きながら実装を柔軟にし、将来の変更や拡張を容易にします。

依存性反転原理(DIP)とは何ですか?

依存性反転原則 (DIP) によれば、高レベルのモジュールは低レベルのモジュールに直接依存すべきではありません。代わりに、どちらもインターフェイスや抽象クラスなどの抽象化に依存する必要があります。これにより、高レベルと低レベルのモジュールの両方が互いに独立して動作できるようになり、システムがより柔軟になり、変更の影響が軽減されます。

簡単な説明:

複数の機能を備えたアプリケーションがあると想像してください。ある機能が別の機能に直接依存している場合、ある機能を変更するにはコードの多くの部分を変更する必要があります。 DIP は、直接の依存関係の代わりに、アプリケーションのさまざまな部分を共通のインターフェイスまたは抽象化を通じて動作させる必要があることを示唆しています。このようにして、各モジュールは独立して機能することができ、1 つの機能に変更を加えても、システムの残りの部分への影響は最小限に抑えられます。

DIP の 2 つの重要なポイント:

  • 高レベルのモジュールは低レベルのモジュールに依存しないでください。代わりに、両方とも抽象化に依存する必要があります。

  • 具象 (特定の実装) ではなく、抽象化に依存する必要があります。

例:

PayPal と Stripe の両方を支払い方法として使用できる支払いシステムを検討してください。 PayPal または Stripe と直接連携している場合、後から新しい支払いゲートウェイを追加するには、アプリケーション全体で広範なコード変更が必要になります。

ただし、DIP に従う場合は、一般的な PaymentInterface を使用することになります。 PayPal と Stripe は両方ともこのインターフェイスを実装します。こうすることで、将来新しい支払いゲートウェイを追加する場合は、既存のインターフェースを実装するだけで済み、プロセスがはるかに簡単になります。

この原則に従うことで、DIP はコードの保守性と柔軟性を強化し、大きな中断を伴うことなく、よりスムーズな適応と拡張を可能にします。

例 1:

さまざまな種類の車両を始動および停止する必要があるアプリケーションを考えてみましょう。抽象化として機能する Vehicle インターフェイスを作成できます。どの車両でもこのインターフェイスを実装して、独自の機能を提供できます。

Java コード:

 // Abstraction: Vehicle interface
interface Vehicle {
    void start();
    void stop();
}

// Car class implements Vehicle interface
class Car implements Vehicle {
    public void start() {
        System.out.println("Car started");
    }

    public void stop() {
        System.out.println("Car stopped");
    }
}

// Bike class implements Vehicle interface
class Bike implements Vehicle {
    public void start() {
        System.out.println("Bike started");
    }

    public void stop() {
        System.out.println("Bike stopped");
    }
}

// Main class
public class Main {
    public static void main(String[] args) {
        Vehicle car = new Car(); // Car as Vehicle
        car.start();
        car.stop();

        Vehicle bike = new Bike(); // Bike as Vehicle
        bike.start();
        bike.stop();
    }
}


説明:

ここで、Vehicle は、実行する必要があるアクション (つまり、開始と停止) を指定する抽象概念として機能しますが、それらのアクションがどのように実行されるべきかは定義しません。 Car クラスと Bike クラスは、独自のロジックに従ってこれらのメソッドを実装します。この設計により、既存のコードを変更せずに、トラックなどの新しい車両タイプを簡単に追加できます。

新しい車両の追加: トラック
新しい車両を追加するには、Vehicle インターフェースを実装し、start() メソッドと stop() メソッドの独自の実装を提供する Truck クラスを作成するだけです。

トラック用 Java コード:


class Truck implements Vehicle {
    public void start() {
        System.out.println("Truck started");
    }
    public void stop() {
        System.out.println("Truck stopped");
    }
}


何も変更せずにメインクラスでトラックを使用する:
ここで、Truck を Main クラスに含めるために、Truck のインスタンスを作成し、それを Vehicle として使用できます。 Main クラスやコードの他の部分を変更する必要はありません。

メインクラスの Java コード:


<p>public class Main {<br>
    public static void main(String[] args) {<br>
        Vehicle car = new Car(); // Car as Vehicle<br>
        car.start();<br>
        car.stop();</p>
<div class="highlight js-code-highlight">
<pre class="highlight plaintext">    Vehicle bike = new Bike(); // Bike as Vehicle
    bike.start();
    bike.stop();

    // Adding Truck
    Vehicle truck = new Truck(); // Truck as Vehicle
    truck.start();
    truck.stop();
}

}

出力:
コードをコピー
車が始動しました
車が止まった
バイク始動
自転車が止まってしまいました
トラックが出発しました
トラックが停止しました

全画面モードに入る 全画面モードを終了します




Explanation:

Here, the Truck has been added without making any changes to the existing code. The Truck class simply implements its own versions of the start() and stop() functions according to the Vehicle interface. There was no need to modify the Main class or any other part of the application. This demonstrates how the Dependency Inversion Principle (DIP) promotes flexibility and maintainability in code.

Benefits of DIP:

  • Scalability: New classes (like Truck) can be easily added to the system without affecting existing components.

  • Maintainability: The Main class and other high-level modules do not need to be modified when new features or vehicles are introduced.

  • Flexibility: It is possible to add new vehicles without relying on existing ones, allowing for easier integration of new functionality.

By adhering to the Dependency Inversion Principle, the system remains extensible and maintainable through the use of abstractions.

Importance of DIP:

In this context, Car, Bike, and Truck do not depend directly on the Main class. Instead, the Main class relies solely on the Vehicle abstraction. This means that if a new vehicle type is introduced, there is no need to make changes to the Main class. As a result, the code is much easier to maintain, and adaptability is significantly enhanced.

This approach not only reduces the risk of introducing bugs during modifications but also encourages better design practices by promoting loose coupling between components.

Example 2:

Let's say you want to create a notification system that can send different types of notifications, such as Email, SMS, or Push Notifications. If you directly rely on the Email or SMS classes, adding a new notification method would require changes throughout your code, violating the Dependency Inversion Principle (DIP).

Solution:

We will create an abstraction called Notifier, which will only specify what needs to be done (send notification) without defining how to do it. Different notifier classes (Email, SMS, Push) will implement this functionality in their own way.

JavaScript Code:

<br>
 // Abstraction: Notifier<br>
class Notifier {<br>
    sendNotification(message) {<br>
        throw new Error("Method not implemented.");<br>
    }<br>
}

<p>// EmailNotifier implements Notifier<br>
class EmailNotifier extends Notifier {<br>
    sendNotification(message) {<br>
        console.log(Sending email: ${message});<br>
    }<br>
}</p>

<p>// SMSNotifier implements Notifier<br>
class SMSNotifier extends Notifier {<br>
    sendNotification(message) {<br>
        console.log(Sending SMS: ${message});<br>
    }<br>
}</p>

<p>// PushNotifier implements Notifier<br>
class PushNotifier extends Notifier {<br>
    sendNotification(message) {<br>
        console.log(Sending Push Notification: ${message});<br>
    }<br>
}</p>

<p>// Usage<br>
function sendAlert(notifier, message) {<br>
    notifier.sendNotification(message);<br>
}</p>

<p>// Test different notifiers<br>
const emailNotifier = new EmailNotifier();<br>
const smsNotifier = new SMSNotifier();<br>
const pushNotifier = new PushNotifier();</p>

<p>sendAlert(emailNotifier, "Server is down!");  // Sending email<br>
sendAlert(smsNotifier, "Server is down!");  // Sending SMS<br>
sendAlert(pushNotifier,"Server is down!"); // Sending Push Notification</p>




Explanation:

  • Notifier is the abstraction that specifies that a notification needs to be sent.

  • EmailNotifier, SMSNotifier, and PushNotifier implement the Notifier rules in their own ways.

  • The sendAlert function only depends on the Notifier abstraction, not on any specific notifier. Therefore, if we want to add a new notifier, there will be no need to change the existing code.

Why DIP is Important Here:

To add a new notification method (like WhatsApp), we simply need to create a new class that implements Notifier, and the rest of the code will remain unchanged.

Example 3:

Let’s say you are creating an e-commerce site where payments need to be processed using various payment gateways (like PayPal and Stripe). If you directly rely on PayPal or Stripe, making changes or adding a new gateway would require significant alterations to your code, violating the Dependency Inversion Principle (DIP).

Solution:

We will create an abstraction called PaymentGateway. Any payment gateway will operate according to this abstraction, so adding a new gateway will not require changes to other parts of the code.

JavaScript Code:

<br>
 // Abstraction: PaymentGateway<br>
class PaymentGateway {<br>
    processPayment(amount) {<br>
        throw new Error("Method not implemented.");<br>
    }<br>
}

<p>// PayPal class implements PaymentGateway<br>
class PayPal extends PaymentGateway {<br>
    processPayment(amount) {<br>
        console.log(Processing $${amount} payment through PayPal);<br>
    }<br>
}</p>

<p>// Stripe class implements PaymentGateway<br>
class Stripe extends PaymentGateway {<br>
    processPayment(amount) {<br>
        console.log(Processing $${amount} payment through Stripe);<br>
    }<br>
}</p>

<p>// Usage<br>
function processOrder(paymentGateway, amount) {<br>
    paymentGateway.processPayment(amount);<br>
}</p>

<p>// Test different payment gateways<br>
const paypal = new PayPal();<br>
const stripe = new Stripe();</p>

<p>processOrder(paypal, 100);  // Processing $100 payment through PayPal<br>
processOrder(stripe, 200);  // Processing $200 payment through Stripe</p>




Explanation:

  • PaymentGateway is an abstraction that specifies the requirement to process payments.

  • The PayPal and Stripe classes implement this abstraction in their own ways.

  • The processOrder function only depends on the PaymentGateway abstraction, so if you want to add a new gateway (like Bitcoin), you don’t need to make any changes to the existing code.

DIP is Important Here:

If you need to add a new payment gateway, you simply create a new class that implements the PaymentGateway, and there will be no changes to the core code. This makes the code more maintainable and flexible.

DIP কেন গুরুত্বপূর্ণ?

1. Maintainability: নতুন কিছু যোগ করতে বা পরিবর্তন করতে হলে বড় কোড পরিবর্তন করতে হয় না। DIP মেনে abstraction ব্যবহার করে সিস্টেমকে সহজে মেইনটেইন করা যায়।

2. Flexibility: নতুন ফিচার যোগ করা অনেক সহজ হয়ে যায় কারণ high-level module এবং low-level module আলাদা থাকে।

3. Scalability: DIP এর মাধ্যমে সিস্টেমে নতুন ফিচার বা ফাংশনালিটি যোগ করা সহজ এবং দ্রুত করা যায়।

4. Decoupling: High-level module এবং low-level module সরাসরি একে অপরের উপর নির্ভর না করে abstraction এর মাধ্যমে কাজ করে, যা সিস্টেমের dependencies কমিয়ে দেয়।

Dependency Inversion Principle(DIP) in React

The Dependency Inversion Principle (DIP) can make React applications more modular and maintainable. The core idea of DIP is that high-level components should not depend on low-level components or specific implementations; instead, they should work with a common rule or structure (abstraction).

To implement this concept in React, we typically use props, context, or custom hooks. As a result, components are not directly tied to specific data or logic but operate through abstractions, which facilitates easier changes or the addition of new features in the future.

Example:

  • If you are building an authentication system, instead of directly relying on Firebase, create an abstraction called AuthService. Any authentication service (like Firebase or Auth0) can work according to this abstraction, allowing you to change the authentication system without altering the entire codebase.

  • Similarly, when making API calls, instead of depending directly on fetch or Axios, you can use an abstraction called ApiService. This ensures that the rules for making API calls remain consistent while allowing changes to the implementation.

By adhering to DIP, your components remain reusable, flexible, and maintainable.

Example 1: Authentication Service

You are building a React application where users need to log in using various authentication services (like Firebase and Auth0). If you directly work with Firebase or Auth0, changing the service would require significant code modifications.

Solution:

By using the AuthService abstraction and adhering to the Dependency Inversion Principle (DIP), you can create an authentication system that allows different authentication services to work together.

JSX Code:

<br>
 import React, { createContext, useContext } from "react";

<p>// Abstraction: AuthService<br>
const AuthServiceContext = createContext();</p>

<p>// FirebaseAuth implementation<br>
const firebaseAuth = {<br>
  login: (username, password) => {<br>
    console.log(Logging in ${username} using Firebase);<br>
  },<br>
  logout: () => {<br>
    console.log("Logging out from Firebase");<br>
  }<br>
};</p>

<p>// Auth0 implementation<br>
const auth0Auth = {<br>
  login: (username, password) => {<br>
    console.log(Logging in ${username} using Auth0);<br>
  },<br>
  logout: () => {<br>
    console.log("Logging out from Auth0");<br>
  }<br>
};</p>

<p>// AuthProvider component for dependency injection<br>
const AuthProvider = ({ children, authService }) => {<br>
  return (<br>
    fe05d5eac77346ea84deedde2c5fa67a<br>
      {children}<br>
    5d1ea9ac180cbe596576b5ba761237c4<br>
  );<br>
};</p>

<p>// Custom hook to access the AuthService<br>
const useAuthService = () => {<br>
  return useContext(AuthServiceContext);<br>
};</p>

<p>// Login component that depends on abstraction<br>
const Login = () => {<br>
  const authService = useAuthService();</p>

<p>const handleLogin = () => {<br>
    authService.login("username", "password");<br>
  };</p>

<p>return c883a3f67e1b99af028c932340f2d8acLogin65281c5ac262bf6d81768915a4a77ac0;<br>
};</p>

<p>// App component<br>
const App = () => {<br>
  return (<br>
    71e940f84cca9479943a63f90ece4428<br>
      c853468068e5ce2b25356d786316be3d<br>
    c76493c01919e3cc2f1554665d0ca98b<br>
  );<br>
};</p>

<p>export default App;</p>




Explanation:

  • AuthServiceContext acts as a context that serves as an abstraction for the authentication service.

  • The AuthProvider component is responsible for injecting a specific authentication service (either Firebase or Auth0) into the context, allowing any child components to access this service.

  • The Login component does not directly depend on any specific service; instead, it uses the useAuthService hook to access the authentication abstraction. This means that if you change the authentication service in the AuthProvider, the Login component will remain unchanged and continue to work seamlessly.

This design pattern adheres to the Dependency Inversion Principle (DIP), promoting flexibility and maintainability in the application.

Example 2: API Service Layer with Functional Components

You are making various API calls using fetch or Axios. If you work directly with fetch or Axios, changing the API service would require numerous code changes.

Solution:

We will create an abstraction called ApiService, which will adhere to the Dependency Inversion Principle (DIP) for making API calls. This way, if the service changes, the components will remain unchanged.

JSX Code:

<br>
 import React, { createContext, useContext } from "react";

<p>// Abstraction: ApiService<br>
const ApiServiceContext = createContext();</p>

<p>// Fetch API implementation<br>
const fetchApiService = {<br>
  get: async (url) => {<br>
    const response = await fetch(url);<br>
    return response.json();<br>
  },<br>
  post: async (url, data) => {<br>
    const response = await fetch(url, {<br>
      method: "POST",<br>
      body: JSON.stringify(data),<br>
      headers: { "Content-Type": "application/json" }<br>
    });<br>
    return response.json();<br>
  }<br>
};</p>

<p>// Axios API implementation (for example purposes, similar API interface)<br>
const axiosApiService = {<br>
  get: async (url) => {<br>
    const response = await axios.get(url);<br>
    return response.data;<br>
  },<br>
  post: async (url, data) => {<br>
    const response = await axios.post(url, data);<br>
    return response.data;<br>
  }<br>
};</p>

<p>// ApiProvider for injecting the service<br>
const ApiProvider = ({ children, apiService }) => {<br>
  return (<br>
    6b264275a8e320357bd39a49ba9ac4e1<br>
      {children}<br>
    8cab631425ec8c7bf14965faddaa1658<br>
  );<br>
};</p>

<p>// Custom hook to use the ApiService<br>
const useApiService = () => {<br>
  return useContext(ApiServiceContext);<br>
};</p>

<p>// Component using ApiService abstraction<br>
const DataFetcher = () => {<br>
  const apiService = useApiService();</p>

<p>const fetchData = async () => {<br>
    const data = await apiService.get("https://jsonplaceholder.typicode.com/todos");<br>
    console.log(data);<br>
  };</p>

<p>return 30e510242e32dc21a8f77857377a72f1Fetch Data65281c5ac262bf6d81768915a4a77ac0;<br>
};</p>

<p>// App component<br>
const App = () => {<br>
  return (<br>
    728e439b6de9082b69fb30a1e596e6b4<br>
      9823bd4f87bdf0bf7d485827fd44f5aa<br>
    5ad2b31ff55d0b689ab3f4e12587c807<br>
  );<br>
};</p>

<p>export default App;</p>




Explanation:

  • ApiServiceContext is a context that serves as an abstraction for making API calls.

  • The ApiProvider component injects a specific API service.

  • The DataFetcher component does not directly depend on fetch or Axios; it relies on the abstraction instead. If you need to switch from fetch to Axios, you won't need to modify the component.

Example 3: Form Validation with Functional Components

You have created a React app where form validation is required. If you write the validation logic directly inside the component, any changes to the validation logic would require modifications throughout the code.

Solution:

We will create an abstraction called FormValidator. Different validation libraries (such as Yup or custom validation) will work according to this abstraction.

JSX Code:

<br>
 import React from "react";

<p>// Abstraction: FormValidator<br>
const FormValidatorContext = React.createContext();</p>

<p>// Yup validator implementation<br>
const yupValidator = {<br>
  validate: (formData) => {<br>
    console.log("Validating using Yup");<br>
    return true; // Dummy validation<br>
  }<br>
};</p>

<p>// Custom validator implementation<br>
const customValidator = {<br>
  validate: (formData) => {<br>
    console.log("Validating using custom logic");<br>
    return true; // Dummy validation<br>
  }<br>
};</p>

<p>// FormValidatorProvider for injecting the service<br>
const FormValidatorProvider = ({ children, validatorService }) => {<br>
  return (<br>
    8553961e3a27fe594a74ff19d52fc2be<br>
      {children}<br>
    8561d8edc91149f5ea3a2f97d62455cc<br>
  );<br>
};</p>

<p>// Custom hook to use the FormValidator<br>
const useFormValidator = () => {<br>
  return React.useContext(FormValidatorContext);<br>
};</p>

<p>// Form component using FormValidator abstraction<br>
const Form = () => {<br>
  const validator = useFormValidator();</p>

<p>const handleSubmit = () => {<br>
    const formData = { name: "John Doe" };<br>
    const isValid = validator.validate(formData);<br>
    console.log("Is form valid?", isValid);<br>
  };</p>

<p>return 5f246b67d575bdf323b33dd46abe4228Submit65281c5ac262bf6d81768915a4a77ac0;<br>
};</p>

<p>// App component<br>
const App = () => {<br>
  return (<br>
    0f14f915bb98fbf6f184fe80b11facb6<br>
      28bedc64ab01fc2a6e99f9b156cc3159<br>
    4f68b725a5460ba517315003078e8baf<br>
  );<br>
};</p>

<p>export default App;</p>




Explanation:

  • FormValidatorContext acts as an abstraction for form validation.

  • The FormValidatorProvider component injects a specific validation logic (e.g., Yup or custom logic).

  • The Form component does not depend directly on Yup or custom validation. Instead, it works with the abstraction. If the validation logic needs to be changed, such as switching from Yup to custom validation, the Form component will remain unchanged.

Disadvantages of Dependency Inversion Principle (DIP):

While the Dependency Inversion Principle (DIP) is a highly beneficial design principle, it also comes with some limitations or disadvantages. Below are some of the key drawbacks of using DIP:

1. Increased Complexity: Following DIP requires creating additional interfaces or abstract classes. This increases the structure's complexity, especially in smaller projects. Directly using low-level modules makes the code simpler, but adhering to DIP requires introducing multiple abstractions, which can add complexity.

2. Overhead of Managing Dependencies: Since high-level and low-level modules are not directly connected, additional design patterns like dependency injection or context are needed to manage dependencies. This increases the maintenance overhead of the project.

3. Unnecessary for Small Projects: In smaller projects or in cases with fewer dependencies or low complexity, using DIP can be unnecessary. Implementing DIP creates additional abstractions that make the code more complicated, while directly using low-level modules can be simpler and more effective.

4. Performance Overhead: By introducing abstractions between high-level and low-level modules, there can be some performance overhead, especially if there are many abstraction layers. Each abstraction adds extra processing, which can slightly impact performance.

5. Misuse of Abstraction: Excessive or incorrect use of abstraction can reduce the readability and maintainability of the code. If abstractions are not thoughtfully implemented, they can create more disadvantages than the intended benefits of DIP.

6. Harder to Debug: Due to the use of abstractions and interfaces, debugging can become more challenging. Without directly working with the implementation, identifying where a problem originates from can take more time.

7. Dependency Injection Tools Required: Implementing DIP often requires using dependency injection frameworks or tools, which take time and effort to learn. Additionally, the use of these frameworks increases the complexity of the project.

Conclusion:

Although DIP is a powerful and beneficial principle, it does have its limitations. In smaller projects or less complex contexts, adhering to DIP may be unnecessary. Therefore, proper analysis is needed to determine when and where to apply this principle for the best results.

? Connect with me on LinkedIn:

I regularly share insights on JavaScript, Node.js, React, Next.js, software engineering, data structures, algorithms, and more. Let’s connect, learn, and grow together!

Follow me: Nozibul Islam

以上がD - 依存性逆転の原則(DIP)の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。
前の記事:インターフェイスと拡張機能の変数次の記事:インターフェイスと拡張機能の変数

関連記事

続きを見る