Flutter SOLID原則(Flutter Roadmap SOLID Principles)

## はじめに
Flutterを網羅的に学習するにあたって[Roadmap](https://roadmap.sh/flutter)を使って学習を進めることにしました。

この記事では、Flutter初学者やこれからFlutterを学習し始める方に向けて、**SOLID原則**についてまとめています。

RoadmapはFlutterだけでなく、他の言語やスキルのロードマップも提供されており、何から学習して良いか分からないと悩んでいる方にとって有用なサイトになっています。
ぜひ[Roadmap](https://roadmap.sh/)を利用して学習してみてください。

## Roadmapとは

簡潔に言えば、Roadmap.shは学習者にとっての**ガイドブック**であり、**学習の方向性を提供する学習ロードマップサイト**です。

初心者から上級者まで、ステップバイステップでスキルを習得するための情報が提供されています。

学習の進め方が分かりやすく示されているだけでなく、個々の項目に参考資料やリソースへのリンクも提供されているので、学習者は目標を設定し、自分自身のペースで学習を進めることができます。

## SOLID Principles
FlutterロードマップSOLID Principlesでは以下の3つのサイトが紹介されています。興味のある方はぜひお読みください。

- S.O.L.I.D Principles: https://www.youtube.com/watch?v=fvNTJang7l4
- Overview of S.O.L.I.D Principles In Dart: https://medium.flutterdevs.com/s-o-l-i-d-principles-in-dart-e6c0c8d1f8f1
- The S.O.L.I.D Principles in Pictures: https://medium.com/backticks-tildes/the-s-o-l-i-d-principles-in-pictures-b34ce2f1e898

## SOLID原則とは
**SOLID原則**とは、オブジェクト指向プログラミングにおける**5つの基本的な設計原則の頭文字を取ったもの**で、ソフトウェアの設計をより堅牢で保守性の高いものにするためのガイドラインです。

### 単一責任の原則 (Single Responsibility Principle - SRP)

**コンポーネント(クラスや関数)が保持する責務（役割）は1つであるべきという原則**です。複数の役割を持つクラスでは複数の利用者の要件を同時に満たせなくなるケースが発生します。

**責任は、責任の適用範囲とセット**で定義されなければなりません。

以下の`User`クラスは複数の責務を持つクラスです。**複数の責務がある場合、責務が密接に関連しているため、1つの責務を変更すると、他の責務にも影響が及ぶ**可能性があります。

```dart: 単一原則に違反している例
class User {
  // ユーザ情報関連の責務
  getUserName() {
    // ユーザ名を取得する処理
  }
  getEmail() {
    // メールアドレスを取得する処理
  }

  // 認証関連の責務
  login() {
    // ログイン処理
  }
  logout() {
    // ログアウト処理
  }

  // 支払い関連の責務
  makePayment() {
    // 支払い処理
  }
}
```

上記のクラスはユーザ情報、認証、支払い関連の3つの責務を持っています。しかし、**SRP**に従うと、クラスが担う責務はたった一つに限定すべきです。**責務を一つに限定することで、バグが発生した際に、不正な動作がどこのクラス、またはメソッドで発生したのかを分かりやすくします。**

```dart: 単一原則を遵守している例
class UserProfile {
  getUserName() {
    // ユーザ名を取得する処理
  }

  getEmail() {
    // メールアドレスを取得する処理
  }
}

class Auth {
  login() {
    // ログイン処理
  }

  logout() {
    // ログアウト処理
  }
}

class UserPayment {
  makePayment() {
    // 支払い処理
  }
}

```

有名なソフトウェア原則に、**DRY原則**があります。
DRYとは`Don't Repeat Yourself` の略で、直訳すると「**繰り返しを避けろ**」という意味です。

ここで注意して欲しいのが、**DRYにすべきは、それぞれの概念単位**ということです。

**同じようなロジック、似ているロジックであっても、概念が違えばDRYにすべきではない**のです。

例えば、上記のコードで`makePayment`メソッドと`getEmail`メソッドの処理内容が酷似していたとします。その場合でも、それぞれ責務が違えば、クラスは分けなければなりません。

このように**クラスやメソッドに持たせる責務を単一にする**ことによって、コードは**理解しやすく、保守性が向上する**のです。

**単一責任の原則 Single Responsibility Principle - SRP**についてもっと詳しく知りたい方は[こちら](https://qiita.com/MinoDriven/items/76307b1b066467cbfd6a)にわかりやすい記事があったので、是非読んでみてください。 

### 開放/閉鎖の原則 (Open/Closed Principle - OCP)

**ソフトウェアの構成要素は拡張に対して開いていて、修正に対して閉じていなければならないという原則**です。簡単に言うと、**新しい機能を追加する際に既存のコードを変更せずに拡張できるべき**という原則になります。

例として、オンラインストアの注文処理システムを考えてみましょう。以下のコードでは商品を表すクラスと注文処理クラスがあります。
```dart
class Product {
  String name;
  double price;

  Product(this.name, this.price);
}

class OrderProcessor {
  List<Product> items = [];

  void addItem(Product product) {
    items.add(product);
  }

  double calculateTotal() {
    double total = 0;
    for (var item in items) {
      total += item.price;
    }
    return total;
  }
}
```

上記のソースコードでは商品をカートに追加して、合計金額を計算することができます。ここで、新しい要件として商品に税金が適用されるようになった場合、どのようにして**OCP**を遵守して拡張できるようにするのでしょうか？

```dart: 開放/閉鎖の原則に違反している例
class OrderProcessor {
  List<Product> items = [];

  void addItem(Product product) {
    items.add(product);
  }

  double calculateTotal() {
    double total = 0;
    for (var item in items) {
      total += item.price;
    }
    return total;
  }

  double calculateTotalWithTax() {
    double total = calculateTotal();
    return total * 1.1; // 10% の税金を適用
  }
}
```

上記のコードでは、税金を計算するという新しい用件に適合するために、`calculateTotalWithTax`メソッドを追加しました。しかし、これは**OCPには違反**しています。なぜなら**新しい税金ルールが追加されるたびに、このクラスを変更する必要がある**からです。そこで、以下のようにコードを修正します。

```dart:開放/閉鎖の原則を遵守している例:
abstract class TaxCalculator {
  double calculateTax(double subtotal);
}

class OrderProcessor {
  List<Product> items = [];
  TaxCalculator taxCalculator; // TaxCalculatorのインスタンスを保持

  OrderProcessor(this.taxCalculator); // TaxCalculatorのインスタンスを注入

  void addItem(Product product) {
    items.add(product);
  }

  double calculateTotal() {
    double total = 0;
    for (var item in items) {
      total += item.price;
    }
    return total;
  }

  double calculateTotalWithTax() {
    double subtotal = calculateTotal();
    double tax = taxCalculator.calculateTax(subtotal);
    return subtotal + tax;
  }
}

class TenPercentTaxCalculator implements TaxCalculator {
  @override
  double calculateTax(double subtotal) {
    return subtotal * 0.1; // 10% の税金を適用
  }
}
```
上記のコードではまず、税金を計算するという新しい要件に従って、**機能を拡張するため**に、`TaxCalculator`という**抽象クラスを作成**しています。抽象クラスで作成することによって、異なる税金計算方法の実装を追加できるようになるのです。

次に、`OrderProcesser`クラスに税金計算のための`TaxCaluculator`インスタンスを保持させて、税金を計算します。これにより、新しい税金計算方法を追加するたびに、既存のコードを変更せずに新しい計算方法を利用できます。

最後に、10%の税金を適用する`TenPercentTaxCalculator`クラスを作成します。この設計により、既存の`OrderProcessor`クラスは変更せずに、新しい`TaxCalculator`の実装を注入するだけで新しい税金計算を利用できるようになります。

このように、**OCP**を遵守した設計は既存のコードを修正する必要がなく、新しい要件に対応することができます。これによって、システム全体の安定性が維持されるのです。

### リスコフの置換原則 (Liskov Substitution Principle - LSP)

**サブクラスはその親クラスと互換性があり、サブクラスのインスタンスは親クラスのインスタンスとして振る舞えるべきという原則**です。簡単にいうと、クラスを継承するときは継承元と継承先のクラスの振る舞いを同じにしようという原則です。

例として、車(スーパークラス)とタクシー(サブクラス)クラスを考えてみましょう。以下のコードでは`Taxi`クラスは`Car`クラスを`extends`して継承しています。

**継承の目的は、メソッドやプロパティをスーパークラスに定義してコードの重複をなくすこと**です。継承は**is-aの関係**(A is a Bは、AはBの一種である)が成立していなければいけません。以下のコードを見ると、タクシーは車の一種であるが成り立ちます。

```dart: リスコフの置換原則に違反している例
class Car {
  void start() {
    print('Car is starting');
  }

  void stop() {
    print('Car is stopping');
  }
}

class Taxi extends Car {
  // タクシーはスタート時にドアをロックする
  @override
  void start() {
    print('Taxi is starting and locking the door');
  }
}

void main() {
  Car car = Taxi(); // タクシーをCar型の変数に代入
  car.start(); // タクシーのstart()メソッドを呼び出し
  car.stop(); // Carのstop()メソッドを呼び出し
}
```

しかし、上記のコードではリスコフの置換原則に違反しています。なぜなら`Car`クラスと`Taxi`クラスでは振る舞いが変わっているからです。

**LSP**に従うためには、**サブクラスは親クラスのメソッドを置換し、その振る舞いを拡張することができても、変更してはいけません**。しかし、`Taxi`クラスでは`start`メソッドが親クラスの`Car`クラスの`start`メソッドの振る舞いを変更しています。このため、`Car`型の変数に`Taxi`オブジェクトを代入した場合、`start`メソッドの振る舞いが予測できなくなります。そこで、以下のようにコードを修正します。

```dart: リスコフの置換原則を遵守している例
abstract class Vehicle {
  void start();
  void stop();
}

class Car implements Vehicle {
  @override
  void start() {
    print('Car is starting');
  }

  @override
  void stop() {
    print('Car is stopping');
  }
}

class Taxi implements Vehicle {
  @override
  void start() {
    print('Taxi is starting and locking the door');
  }

  @override
  void stop() {
    print('Taxi is stopping');
  }
}

void main() {
  Vehicle vehicle = Taxi(); // タクシーをVehicle型の変数に代入
  vehicle.start(); // タクシーのstart()メソッドを呼び出し
  vehicle.stop(); // タクシーのstop()メソッドを呼び出し
}
```

上記のコードでは、`Car`と`Taxi`クラスは共に`Vehicle`クラスを実装しています。このようにすることで、どちらのクラスも`start`と`stop`メソッドを持ち、互換性があるようになります。**LSP**が遵守されているため、`Vehicle`型の変数に`Car`または`Taxi`オブジェクトを代入しても、予測可能な動作が得られるようになるのです。

### インターフェース分離の原則 (Interface Segregation Principle - ISP)

クライアントは**使用しないメソッドに依存すべきではないという原則**です。クライアントが必要としないメソッドに依存しないためには、**インターフェースを小さく、特定の用途に合わせて分割する**必要があります。

例として、動物クラスで考えてみましょう。以下のコードでは`Animal`クラスという抽象クラスを元に`Crow`(カラス)クラス、`Turtle`(カメ)クラスを`implements`(実装)しています。

```dart: インターフェース分離の原則に違反している例
// インターフェース
abstract class Animal {
  void sleep();
  void fly();
  void swim();
}

class Crow implements Animal {
  @override
  void sleep() {
    // 睡眠の処理
  }

  @override
  void fly() {
    // 飛行の処理
  }

  @override
  void swim() {
    // 泳ぎの処理
  }
}

class Turtle implements Animal {
  @override
  void sleep() {
    // 睡眠の処理
  }

  @override
  void fly() {
    // 飛行の処理
  }

  @override
  void swim() {
    // 泳ぎの処理
  }
}
```

上記のコードでは`Crow`クラスに`swim`メソッドがあり、`Turtle`クラスに`fly`メソッドがあります。しかし、実際にカラスは泳ぐことができませんし、カメは飛ぶことができません。つまり上記のコードでは**ISP**に違反しています。そこで、以下のようにコードを修正します。

```dart: インターフェース分離の原則を遵守している例
// インターフェース
abstract class Sleep {
  void sleep();
}

abstract class Fly {
  void fly();
}

abstract class Swim {
  void swim();
}

class Crow implements Sleep, Fly {
  @override
  void sleep() {
    // 睡眠の処理
  }

  @override
  void fly() {
    // 飛行の処理
  }
}

class Turtle implements Sleep, Swim {
  @override
  void sleep() {
    // 睡眠の処理
  }

  @override
  void swim() {
    // 泳ぎの処理
  }
}

```

上記のように**インターフェースを小さい単位に分離させる**ことで、**各クライアントは利用するメソッドだけに依存する**ようになり、他のクライアントの変更の影響を最小限に抑えることができるようになります。

**ISP**を遵守することによって、クライアントが不要な依存関係を持たないようになります。そうすることで、シンプルなコードになり、**コードの理解とメンテナンスが容易になる**のです。

### 依存性逆転の原則 (Dependency Inversion Principle - DIP)

**上位モジュールは下位モジュールに依存してはいけず、両者は抽象に依存すべきで、また具体的な実装は抽象に依存すべきであり、抽象は具体的な実装に依存するべきではないという原則**です。

簡単にいうと**プログラムの重要な部分が、重要でない部分に依存しないよう設計すべきで、抽象的な実装は具体的な実装に依存すべきではない**ということです。

例として電子メールを考えてみましょう。

```dart: 依存性逆転の原則に違反している例
class EmailSender {
  void sendEmail(String recipient, String message) {
    // メールを送信する具体的な実装
  }
}

class EmailService {
  EmailSender sender = EmailSender();

  void sendEmail(String recipient, String message) {
    // EmailSenderの具体的な実装に依存している
    sender.sendEmail(recipient, message);
  }
}
```

上記のコードでは`EmailService`クラスが具体的な`EmailSender`クラスに依存しています。これは**依存性逆転の原則**に違反しています。なぜなら、高レベル(抽象)の`EmailService`クラスが低レベル(具体)の`EmailSender`クラスに直接依存しており、**抽象に依存していない**からです。そこで、以下のようにコードを修正します。

```dart: 依存性逆転の原則を遵守している例
abstract class MessageSender {
  void sendMessage(String recipient, String message);
}

class EmailSender implements MessageSender {
  @override
  void sendMessage(String recipient, String message) {
    print('Sending an email to $recipient: $message');
  }
}

class SMSSender implements MessageSender {
  @override
  void sendMessage(String recipient, String message) {
    print('Sending an SMS to $recipient: $message');
  }
}

class MessageService {
  late MessageSender sender;

  MessageService(MessageSender sender) {
    this.sender = sender;
  }

  void sendMessage(String recipient, String message) {
    // MessageSenderに依存しているが、具体的な実装には依存していない
    sender.sendMessage(recipient, message);
  }
}

void main() {
  // EmailSenderを使ってメッセージを送信する例
  final emailSender = EmailSender();
  final emailService = MessageService(emailSender);
  emailService.sendMessage('example@example.com', 'Hello, this is an email!');
  // Sending an email to example@example.com: Hello, this is an email!

  // SMSSenderを使ってメッセージを送信する例
  final smsSender = SMSSender();
  final smsService = MessageService(smsSender);
  smsService.sendMessage('1234567890', 'Hello, this is an SMS!');
  // Sending an SMS to 1234567890: Hello, this is an SMS!

}
```

上記のコードでは、`MessageService`クラスは抽象的な`MessageSender`クラスに依存しており、具体的な実装には依存していません。具体的な送信方法は`MessageSender`の実装クラスで行われています。このようにすることで、新しい送信方法を追加する場合でも、`MessageService`クラスの変更は不要で、**DIP**に遵守した設計となるのです。

**DIP**に遵守した設計にすることによって、コードの**カプセル化と柔軟性が向上し、依存関係の変更に強くなります**。


## 参考資料

https://xtech.nikkei.com/atcl/nxt/column/18/02319/010500002/

https://zenn.dev/kazu1/articles/dfc5d066e9d8b3

https://zenn.dev/kazu1/articles/c19d5566fd40bb

https://qiita.com/k2491p/items/d442344a462d3a574acc#:~:text=%E3%83%AA%E3%82%B9%E3%82%B3%E3%83%95%E3%81%AE%E7%BD%AE%E6%8F%9B%E5%8E%9F%E5%89%87%E3%81%A8,%E3%81%AB%E3%82%82%E9%80%9A%E3%81%98%E3%82%8B%E3%81%A8%E3%81%93%E3%82%8D%E3%81%8C%E3%81%82%E3%82%8A%E3%81%BE%E3%81%99%E3%80%82

https://zenn.dev/chida/articles/5373e135be11f0

https://zenn.dev/chida/articles/882aad07effa5c