paint-brush
Java の SOLID 原則: 初心者ガイド@ps06756
14,604 測定値
14,604 測定値

Java の SOLID 原則: 初心者ガイド

Pratik Singhal14m2024/03/22
Read on Terminal Reader

長すぎる; 読むには

SOLID 原則は、スケーラブルなソフトウェアを開発するために不可欠なオブジェクト指向プログラミングの原則です。原則は次のとおりです。 S: 単一責任の原則 O: オープン/クローズ原則 L: リスコフの置換原理 I: インターフェース分離の原則 D: 依存関係逆転の原則
featured image - Java の SOLID 原則: 初心者ガイド
Pratik Singhal HackerNoon profile picture
0-item

SOLID 設計原則は、クリーンなコードを書くために知っておく必要がある最も重要な設計原則です。SOLID 原則をしっかりと理解することは、あらゆるプログラマーにとって不可欠なスキルです。これらは、他の設計パターンを開発するための基礎となります。


この記事では、実際の例を使用して SOLID 設計原則を取り上げ、その重要性を理解します。


多態性、抽象化、継承とともに、SOLID 原則は、目的指向プログラミングを習得する上で非常に重要です。

SOLID 原則が重要な理由

SOLID 原則は、次のような複数の理由で重要です。


  • SOLID 原則により、クリーンかつ保守しやすいコードを作成できます。新しいプロジェクトを開始すると、実装する機能セットが限られているため、最初はコードの品質は良好です。ただし、より多くの機能を組み込むにつれて、コードが乱雑になり始めます。


  • SOLID 原則は、抽象化、ポリモーフィズム、継承の基礎の上に構築され、一般的なユースケースの設計パターンにつながります。これらの設計パターンを理解すると、プログラミングで一般的なユースケースを実装するのに役立ちます。


  • SOLID 原則は、コードのテスト可能性を向上させるクリーンなコードの作成に役立ちます。これは、コードがモジュール化され、疎結合されているためです。各モジュールは独立して開発され、独立してテストできます。


それでは、実際の例を用いて、SOLID の原則をそれぞれ詳しく見ていきましょう。

1. 単一責任の原則

SOLID 原則の S は、単一責任原則 (Single Responsibility Principle) を表します。単一責任原則では、クラスを変更する理由は 1 つだけであるべきであると規定されています。これにより、プロジェクトに追加の要件を組み込むときに変更が必要な場所の数が制限されます。

各クラスには変更する理由が 1 つだけ必要です。

たとえば、Java で銀行アプリケーションを設計しているとします。このアプリケーションには、debit、credit、 sendUpdatesなどの基本的な操作が可能なSavingsAccountクラスがありますsendUpdateメソッドはNotificationMedium (Email、SMS など) と呼ばれる列挙型を受け取り、適切なメディアで更新を送信します。そのためのコードを以下のように記述します。


 public class SavingsAccount { public int balance; public String name; public SavingsAccount(int initialBalance, String name) { this.balance = initialBalance; this.name = name; System.out.println("Created a savings account with balance = " + initialBalance); } public void debit(int amountToDebit) { // debit business logic } public void credit(int amountToCredit) { // credit business logic } public void sendNotification(NotificationMedium medium) { if (medium == NotificationMedium.SMS) { // Send SMS here } else if (medium == NotificationMedium.EMAIL) { // Send Email here } } }


 public enum NotificationMedium { SMS, EMAIL }


さて、上記のSavingsAccountクラスを見ると、複数の理由により変更される可能性があります。


  1. SavingsAccountクラスのコアロジック ( debitcreditなど) に変更がある場合。


  2. 銀行が新しい通知媒体(たとえば WhatsApp)を導入することを決定した場合。


これは、SOLID 原則の単一責任原則に違反しています。これを修正するには、通知を送信する別のクラスを作成します。


上記のコードをSOLID原則に従ってリファクタリングしてみましょう。


 public class SavingsAccount { public int balance; public String name; public SavingsAccount(int initialBalance, String name) { this.balance = initialBalance; this.name = name; System.out.println("Created a savings account with balance = " + initialBalance); } public void debit(int amountToDebit) { // debit business logic } public void credit(int amountToCredit) { // credit business logic } public void printBalance() { System.out.println("Name: " + name+ " Account Balance: " + balance); } public void sendNotification(Medium medium) { Sender.sendNotification(medium, this); } }


 public enum NotificationMedium { SMS, EMAIL }


 public class Sender { public static void sendNotification(NotificationMedium medium, SavingsAccount account) { // extract account data from the account object if (medium == NotificationMedium.SMS) { //logic to send SMS here } else if (medium == NotificationMedium.EMAIL) { // logic to send Email here } } }


コードをリファクタリングしたので、 NotificationMediumやフォーマットに変更があった場合は、 Senderクラスを変更します。ただし、 SavingsAccountのコアロジックに変更があった場合は、 SavingsAccountクラスに変更が加わります。


これにより、最初の例で確認された違反が修正されます。

2. オープン/クローズ原則

オープン クローズ原則は、クラスを拡張 (追加機能の追加の場合) に対してオープンでありながら、変更に対してはクローズするように設計する必要があることを述べています。変更に対してクローズであることは、次の 2 つの点で役立ちます。


  • 多くの場合、元のクラス ソースは利用できないことがあります。プロジェクトによって消費される依存関係である可能性があります。


  • 元のクラスを変更せずに維持すると、バグが発生する可能性が減ります。変更するクラスに依存する他のクラスが存在する可能性があるためです。


オープン クローズ原則の例を示すために、ショッピング カート (e コマース Web サイトに実装されているものなど) の例を見てみましょう。


Cartというクラスを作成します。このクラスには、追加できるItemのリストが含まれます。アイテムの種類と課税に応じて、 Cartクラス内でカートの合計値を計算するメソッドを作成します。


クラスは拡張に対してはオープンで、変更に対してはクローズであるべきである


import java.util.ArrayList; import java.util.List; public class Cart { private List<Item> items; public Cart() { this.items = new ArrayList<>(); } public void addToCart(Item item) { items.add(item); } public double calculateCartValue() { double value = 0.0; for(Item item: items) { if (item.getItemType() == GIFT) { // 8% tax on gift + 2% gift wrap cost value += (item.getValue()*1.08) + item.getValue()*0.02 } else if (item.getItemType() == ItemType.ELECTRONIC_ITEM) { value += (item.getValue()*1.11); } else { value += item.getValue()*1.10; } } return value; } }


 @Getter @Setter public abstract class Item { protected double price; private ItemType itemType; public double getValue() { return price; } }
 public enum ItemType { ELECTRONIC, GIFT }


上記の例では、 calculateCartValueメソッドはカート内のすべてのアイテムを反復処理し、アイテムの種類に基づいてロジックを呼び出すことでカートの値を計算します。


このコードは正しいように見えますが、SOLID 原則に違反しています。


カートの値を計算する際に、異なるタイプのアイテム (たとえば Grocery) に新しいルールを追加する必要があるとします。その場合、元のクラスCartを変更し、その中にGroceryタイプのアイテムをチェックする別のelse if条件を記述する必要があります。


ただし、少しリファクタリングするだけで、コードをオープン/クローズ原則に準拠させることができます。その方法を見てみましょう。


まず、 Itemクラスを抽象化し、以下に示すように、さまざまな種類のItemの具体的なクラスを作成します。


 public abstract class Item { protected double price; public double getValue() { return price; } }


 public class ElectronicItem extends Item { public ElectronicItem(double price) { super.price = price; } @Override public double getValue() { return super.getValue()*1.11; } }


 public class GiftItem extends Item { public GiftItem(double price) { super.price = price; } @Override public double getValue() { return super.getValue()*1.08 + super.getValue()*0.02; } }


 public class GroceryItem extends Item { public GroceryItem(double price) { super.price = price; } @Override public double getValue() { return super.getValue()*1.03; } }


GroceryItemGiftItemElectronicItemの各具体的な Item クラス内では、課税と値の計算に関するビジネス ロジックを含むgetValue()メソッドを実装します。


ここで、 Cartクラスを抽象クラスItemに依存させ、以下に示すように各アイテムに対してgetValue()メソッドを呼び出します。


 import java.util.ArrayList; import java.util.List; public class Cart { private List<Item> items; public Cart(Payment paymentOption) { this.items = new ArrayList<>(); } public void addToCart(Item item) { items.add(item); } public double calculateCartValue() { double value = 0.0; for(Item item: items) { value += item.getValue(); } return value; } }


さて、このリファクタリングされたコードでは、新しいタイプのItemが導入されても、 Cartクラスは変更されません。ポリモーフィズムにより、 items ArrayList内のItemの実際のタイプが何であっても、そのクラスのgetValue()メソッドが呼び出されます。

3. リスコフの置換原則

リスコフの置換原則は、特定のコードでスーパークラスのオブジェクトを子クラスのオブジェクトに置き換えても、コードは壊れないというものです。つまり、サブクラスがスーパークラスを継承してそのメソッドをオーバーライドする場合、スーパークラスのメソッドの動作との一貫性が維持される必要があります。


たとえば、次のクラスVehicleと 2 つのクラスCarクラスおよびBicycleクラスを作成します。ここで、Vehicle クラス内にstartEngine()というメソッドを作成するとします。このメソッドはCarクラスではオーバーライドできますが、 Bicycleクラスではサポートされません。これは、 Bicycleにはエンジンがないためです (以下のコード サンプルを参照)。


サブクラスは、メソッドをオーバーライドするときに、スーパークラスの動作との一貫性を維持する必要があります。

 class Vehicle { public void startEngine() { // start engine of the vehicle } } class Car extends Vehicle { @Override public void startEngine() { // Start Engine } } class Bicycle extends Vehicle { @Override public void startEngine(){ throw new UnsupportedOperationException("Bicycle doesn't have engine"); } }


さて、vehicle 型のオブジェクトを期待し、 startEngine()メソッドに依存するコードがあるとします。そのコードを呼び出すときに、 Vehicle型のオブジェクトを渡す代わりにBicycleオブジェクトを渡すと、コードに問題が発生します。startEngine startEngine()メソッドが呼び出されると、 Bicycle (s) クラスのメソッドが例外をスローするためです。これは、SOLID 原則 (Liskov の置換原則) に違反します。


この問題を解決するには、 MotorizedVehicleNonMotorizedVehicle 2つのクラスを作成し、 Car MotorizedVehicleクラスから継承し、 BicycleNonMotorizedVehicleから継承します。


 class Vehicle { } class MotorizedVehicle extends Vehicle { public void startEngine() { // start engine here } } class Car extends MotorizedVehicle { @Override public void startEngine() { // Start Engine } } class NonMotorizedVehicle extends Vehicle { public void startRiding() { // Start without engine } } class Bicycle extends NonMotorizedVehicle { @Override public void startRiding(){ // Start riding without the engine. } }


4. インターフェース分離の原則

SOLID 原則の「I」は、インターフェース分離原則を表します。


インターフェース分離の原則は、未使用のメソッドを実装クラスに強制する大きなインターフェースを持つのではなく、より小さなインターフェースを持ち、クラスを実装するべきであると述べています。この方法では、クラスは関連するメソッドのみを実装し、クリーンな状態を維持します。

インターフェースを 1 つの大きなインターフェースではなく、複数の小さなインターフェースに分割します。

例えば、Javaに組み込まれているCollectionsフレームワークを見てみましょう。Javaは他のデータ構造の中でも、 LinkedListArrayListデータ構造も提供しています。


ArrayListクラスは、 SerializableCloneableIterableCollectionListRandomAccessの各インターフェースを実装します。

LinkedListクラスは、 SerializableCloneableIterableCollectionDequeList 、およびQueueを実装します。


インターフェースがかなりたくさんありますね!


Java 開発者は、多数のインターフェースを持つ代わりに、 SerializableCloneableIterableCollectonList 、およびRandomAccessを 1 つのインターフェース、つまりIListインターフェースに組み合わせることができました。これで、 ArrayListLinkedListクラスの両方がこの新しいIListインターフェースを実装できるようになりました。


ただし、 LinkedListランダム アクセスをサポートしていないため、 RandomAccessインターフェイスのメソッドを実装し、誰かがそれを呼び出そうとするとUnsupportedOperationExceptionがスローされる可能性があります。


ただし、これは SOLID 原則のインターフェイス分離原則に違反することになります。これは、必須ではないにもかかわらず、LinkedList クラスにRandomAccessインターフェイス内のメソッドを実装するように「強制」することになるからです。


したがって、共通の動作に基づいてインターフェースを分割し、各クラスに 1 つの大きなインターフェースではなく多数のインターフェースを実装させる方が適切です。

依存性逆転の原則

依存性逆転の原則では、上位レベルのクラスは下位レベルのクラスに直接依存してはならないと規定されています。これにより、2 つのレベル間の密接な結合が生まれます。


その代わりに、下位クラスは上位クラスが依存するインターフェースを提供する必要があります。


クラスではなくインターフェースに依存する


たとえば、上で見たCartの例を引き続き使用して、支払いオプションをいくつか追加して拡張してみましょう。 DebitCardPaypalの 2 種類の支払いオプションがあると仮定します。ここで、 Cartクラスで、カートの値を計算し、指定された支払い方法に基づいて支払いを開始するメソッドをplaceOrderに追加します。


これを実現するには、 Cartクラス内のフィールドとして 2 つの支払いオプションを追加することで、上記のCart例に依存関係を追加できます。ただし、そうすると、 CartクラスがDebitCardクラスおよびPaypalクラスと密結合されます。


その代わりに、 Paymentインターフェースを作成し、 DebitCardクラスとPaypalクラスの両方にPaymentインターフェースを実装させます。これで、 Cartクラスは、個々の支払いタイプではなく、 Paymentインターフェースに依存するようになります。これにより、クラスは疎結合のままになります。


以下のコードを参照してください。


 public interface Payment { void doPayment(double amount); } public class PaypalPayment implements Payment { @Override public void doPayment(double amount) { // logic to initiate paypal payment } } public class DebitCardPayment implements Payment { @Override public void doPayment(double amount) { // logic to initiate payment via debit card } } import java.util.ArrayList; import java.util.List; public class Cart { private List<Item> items; private Payment paymentOption; public Cart(Payment paymentOption) { this.items = new ArrayList<>(); this.paymentOption = paymentOption; } public void addToCart(Item item) { items.add(item); } public double calculateCartValue() { double value = 0.0; for(Item item: items) { value += item.getValue(); } return value; } public void placeOrder() { this.paymentOption.doPayment(calculateCartValue()); } }


オブジェクト指向プログラミング、GoFデザインパターン、低レベルデザインインタビューについてさらに詳しく知りたい場合は、私の高評価コース「オブジェクト指向プログラミング+Javaデザインパターン」をぜひチェックしてください。