はじめに
この内容は、”Clean Code for Dart”の内容を基に、和訳とメモや解説を個人用にまとめたものです。長い為、3回に分けて掲載しています。これは2回目の内容になります。
オブジェクトとデータ構造
必要なときだけゲッターとセッターを使用する
// 悪い例
// 他の言語とは異なり、Dartでは、属性にアクセスする際にロジックが必要な場合にのみ
// ゲッターとセッターを使用する事が推奨されている
// BankAccount クラスで balance 属性に対するゲッターとセッターが定義されているが、
// これらは単に属性を設定または取得するだけなので冗長
class BankAccount {
// "_" configure as private
int _balance;
int get balance => _balance;
set balance(int amount) => _balance = amount;
BankAccount({
int balance = 0,
}) : _balance = balance;
}
final account = BankAccount();
account.balance = 100;
// 良い例
// ゲッターとセッターを使用する必要がない単純な属性に対して、冗長なコードを避ける
class BankAccount {
int balance;
// ...
BankAccount({
this.balance = 0,
// ...
});
}
final account = BankAccount();
account.balance = 100;
Dart必要なときだけプライベートメソッドと属性を使用する
// 悪い例
// メソッドまたは属性がクラス内でのみアクセスする必要がある場合、プライベートにするべき
// 例ではクラス外からアクセスできてしまっている
class Employee {
String name;
Employee({required this.name});
}
final employee = Employee(name: 'John Doe');
print(employee.name); // John Doe
employee.name = 'Uncle Bob';
print(employee.name); // Uncle Bob
// 良い例
// クラス外からはアクセスできない
class Employee {
String _name;
Employee({required String name}) : _name = name;
}
final employee = Employee(name: 'John Doe');
print(employee.name);
Dartクラス
メソッドチェーン(カスケード表記)の使用
// 悪い例
// Car クラスのインスタンスを作成し、その後色を変更して save() メソッドを呼び出している
class Car {
String make;
String model;
String color;
Car({
required this.make,
required this.model,
required this.color,
});
save() => print('$make, $model, $color');
}
final car = Car(make: 'Ford', model: 'F-150', color: 'red');
car.color = 'pink';
car.save();
// 良い例
// インスタンスを作成した後に .. を使って連続的にメソッドを呼び出し、メソッドチェーンを実現している
// .. はカスケード記法
// オブジェクト自体を返しながら連続して操作を行うので、返り値を無視する操作に注意
// 可読性が下がったり、戻り値に制限があったりするので適切に使用する事
class Car {
String make;
String model;
String color;
Car({
required this.make,
required this.model,
required this.color,
});
save() => print('$make, $model, $color');
}
final car = Car(make: 'Ford', model: 'F-150', color: 'red')
..color = 'pink'
..save();
Dart継承よりもコンポジションを選ぶ
// 悪い例
// Employeeクラス(従業員)を継承してEmployeeTaxDataクラス(税金)を作成している
// "is-a" 関係ではないため、適切ではない
// Employeeは税金データを「持っている」のではない、EmployeeTaxDataはEmployeeの一種ではない
class Employee {
String name;
String email;
Employee({
required this.name,
required this.email,
});
// ...
}
class EmployeeTaxData extends Employee {
String ssn;
double salary;
EmployeeTaxData({
required this.ssn,
required this.salary,
required super.name,
required super.email,
});
// ...
}
// 良い例
// クラスをそれぞれ独立して定義し、従業員が税金データを持つ関係を表現
// 継承を選びがちな場合、代わりにコンポジションが問題をより適切にモデル化できないか考える
// 継承がコンポジションよりも適切な場合
// 継承が「is-a」関係を表す場合、「has-a」関係ではない場合(Human->Animal vs. User->UserDetails)
// 基底クラスからコードを再利用する必要がある場合(Humanはすべての動物と同じように移動できる)
// 派生クラス全体に対して基底クラスを変更することで、グローバルな変更を行いたい場合(動物が移動するときのカロリー消費を変更する)
class EmployeeTaxData {
String ssn;
double salary;
EmployeeTaxData({
required this.ssn,
required this.salary,
});
// ...
}
class Employee {
String name;
String email;
EmployeeTaxData? taxData;
Employee({
required this.name,
required this.email,
});
void setTaxData(String ssn, double salary) {
taxData = EmployeeTaxData(ssn: ssn, salary: salary);
}
// ...
}
DartSOLID
単一責任の原則 (SRP)
// 悪い例
// クラスが変更される理由は1つに限られるべき
// クラスが概念的にまとまりがないと、変更される理由が多くなる
// 1つのクラスにあまりにも多くの機能があると、その一部を変更した場合に他の依存モジュールにどのように影響するか理解するのが難しくなる
class UserSettings {
String user;
UserSettings({
required this.user,
});
void changeSettings(Settings settings) {
if (verifyCredentials()) {
// ...
}
}
bool verifyCredentials() {
// ...
}
}
// 良い例
// UserSettings クラスが変更される理由を1つに限定するために、認証関連の機能を UserAuth クラスに分離
// これにより、UserSettings クラスは設定の変更という責任を持ち、認証の機能は UserAuth クラスが担当する
class UserAuth {
String user;
UserAuth({
required this.user,
});
bool verifyCredentials() {
// ...
}
}
class UserSettings {
String user;
UserAuth auth;
UserSettings({
required this.user,
}) : auth = UserAuth(user: user);
void changeSettings(Settings settings) {
if (auth.verifyCredentials()) {
// ...
}
}
}
DartOpen/Closedの原則 (OCP)
// 悪い例
double getArea(Shape shape) {
if (shape is Circle) {
return getCircleArea(shape);
} else if (shape is Square) {
return getSquareArea(shape);
}
}
double getCircleArea(Shape shape) {
// ...
}
double getSquareArea(Shape shape) {
// ...
}
// 良い例
// 既存のコードを変更せずに新しい機能を追加できるようにする
// Shape クラスを抽象化し、Circle や Square のような具体的な図形クラスが Shape を継承
// これにより、新しい図形を追加する際には既存の getArea 関数を変更する必要がなく、拡張が容易に行えるようになる
abstract class Shape {
double getArea();
}
class Circle extends Shape {
@override
double getArea() {
// ...
}
}
class Square extends Shape {
@override
double getArea() {
// ...
}
}
// ...
final area = shape.getArea();
Dartリスコフの置換原則 (LSP)
リスコフの置換原則は、オブジェクト指向設計において継承やポリモーフィズムを正しく活用するための基本的なガイドラインです。
この原則は、サブタイプ(派生クラスや実装クラス)がその基本型(基底クラスやインターフェース)の代わりに使われる場合、プログラムの振る舞いが変わらないことを保証するべきだとしています。
- 基本型のオブジェクト(例えば、基底クラスのインスタンス)を置換しても、プログラムの動作が変わらないようにする
- すべての派生型は、基本型と同じ振る舞いを示さなければならない
- 派生型で基本型の操作を無効化したり変更したりする事なく、基本型の操作を追加する事ができる
// 悪い例
class Rectangle {
double width;
double height;
Rectangle({
this.width = 0,
this.height = 0,
});
// setWidth e setHeight used just for example
void setWidth(double value) => width = value;
void setHeight(double value) => height = value;
double getArea() {
return width * height;
}
}
class Square extends Rectangle {
Square({
super.width = 0,
super.height = 0,
});
@override
void setWidth(double value) {
width = value;
height = value;
}
@override
void setHeight(double value) {
width = value;
height = value;
}
}
final rectangles = [Rectangle(), Rectangle(), Square()];
for (final rectangle in rectangles) {
rectangle.setWidth(4);
rectangle.setHeight(5);
final area = rectangle.getArea();
print(area); // BAD: Returns 25 for Square. Should be 20.
}
// 良い例
// Rectangle と Square は共通の親クラス Shape を持ち、それぞれが getArea() メソッドを実装している
// Rectangle と Square は互換性があり、Square を Rectangle の代わりに使っても正しい結果が得られるようになっている
// これにより、リスコフの置換原則が守られています
abstract class Shape {
double getArea();
}
class Rectangle extends Shape {
double width;
double height;
Rectangle({
required this.width,
required this.height,
});
@override
double getArea() {
return width * height;
}
}
class Square extends Shape {
double length;
Square({
required this.length,
});
@override
double getArea() {
return length * length;
}
}
final rectangles = [
Rectangle(width: 4, height: 5),
Rectangle(width: 4, height: 5),
Square(length: 4),
];
for (final rectangle in rectangles) {
final area = rectangle.getArea();
print(area); // Show the correct values: 20, 20, 16.
}
Dartインターフェース分離の原則 (ISP)
インターフェース分離の原則(Interface Segregation Principle, ISP)は、オブジェクト指向プログラミングにおける設計原則の一つです。この原則は、クライアントが利用するインターフェースが、クライアントが必要としないメソッドを含まないようにすることを要求しています。
- クライアントに特化したインターフェースを定義する(1つの大きなインターフェースよりも、複数の小さなインターフェースが好ましい)
- インターフェースを分割する(クライアントが必要としない機能を持つ大きなインターフェースを回避する)
インターフェースの明確さとシンプルさを保ち、依存性を最小化する事を目的としています。
// 悪い例
// 下の例では、インターフェースを実装するクラスが throw UnimplementedError() を使用する場合
// インターフェース分離の原則に適合していない
// ダウンロードできない本に対しても、download()の実装が強制されてしまっている
abstract class Book {
int getNumberOfPages();
void download();
}
class EBook implements Book {
@override
int getNumberOfPages() {
// ...
}
@override
String download() {
// ...
}
}
class PhysicalBook implements Book {
@override
int getNumberOfPages() {
// ...
}
@override
void download() {
throw UnimplementedError(); // Physical book doesn't download.
}
}
// 良い例
// インターフェース分離の原則に従い、Book インターフェースをより細かいインターフェースに分割
// Book インターフェースはページ数を取得するメソッドだけを持つ
// DownloadableBook インターフェースはダウンロード可能な本が持つダウンロードメソッドを定義
abstract class Book {
int getNumberOfPages();
}
abstract class DownloadableBook {
void download();
}
class EBook implements Book, DownloadableBook {
@override
int getNumberOfPages() {
// ...
}
@override
void download() {
// ...
}
}
class PhysicalBook implements Book {
@override
int getNumberOfPages() {
// ...
}
}
Dart依存性逆転の原則 (DIP)
依存性逆転の原則(Dependency Inversion Principle, DIP)は、ソフトウェアモジュール間の依存関係を適切に構築し、柔軟性と保守性を向上させるための設計原則の一つです。
高レベルのモジュールは、抽象に依存すべきであり、具体的な実装に依存させない
高レベルのモジュール(例えば、システム全体のビジネスロジックや制御フローを担当する部分)は、抽象的なインターフェースやクラスに依存すべきです。具体的な実装に依存すると、変更や置換が難しくなります。
抽象は具体に依存してはならない
抽象的なインターフェースやクラスは、具体的な実装に依存してはいけません。代わりに、具体的な実装は抽象に従うべきです。
具体的には、以下の方法で実現します。
インターフェースの導入
高レベルのモジュールが特定の低レベルのモジュールに直接依存しないようにするために、抽象的なインターフェース(または抽象クラス)を導入します。高レベルのモジュールはこのインターフェースに依存し、具体的な実装はそのインターフェースを実装することで依存関係を満たします。
依存性注入(Dependency Injection, DI)
抽象と具体の実装を接続するための一般的な方法として、依存性注入が使われます。依存性注入では、高レベルのモジュールがその依存関係を自身で生成するのではなく、外部から注入されたものを利用します。これにより、具体的な実装が柔軟に切り替えやすくなります。
ややこしいので、サンプルを残しておきます。
// 抽象的なログイン処理のインターフェース
abstract class AuthService {
Future<bool> login(String name, String pass)
}
// 具体的なログインサービスの実装クラス
// 具体的なログイン処理の実装は EmailAuthService クラスで行われているが、
// LoginManager クラスは AuthService のインターフェースに対してのみ依存している
// これにより、具体的なログイン処理の変更や置換が容易になる(LoginManager のコードは変更せずに新しい AuthService の実装を注入するだけで済む)
class EmailAuthService implements AuthService {
Future<bool> login(String username, String password) async {
// 仮のログイン処理(実際の処理はここで行う)
if (name== 'example@example.com' && pass== 'password') {
return true;
} else {
return false;
}
}
}
// 高レベルのモジュール(ログイン処理を実行するクラス)
// LoginManager クラスが AuthService の抽象に依存しています。
class LoginManager {
final AuthService _authService;
// コンストラクタでAuthServiceのインスタンスを受け取る
LoginManager(this._authService);
// ログインを試行するメソッド
Future<void> attemptLogin(String name, String pass) async {
bool isSuccess= await _authService.login(name, pass);
if (isSuccess) {
print('Logged in');
} else {
print('Login failure');
}
}
}
DartClean code for dartのコード
// 悪い例
// InventoryTracker クラスが InventoryRequester の具象クラスに依存している
class InventoryRequester {
void requestItem(item) {
// ...
}
}
// 低レベルのモジュールに依存している
class InventoryTracker {
final requester = InventoryRequester();
List<String> items;
InventoryTracker({
required this.items,
});
void requestItems() {
for (var item in items) {
requester.requestItem(item);
}
}
}
final inventoryTracker = InventoryTracker(items: ['apples', 'bananas']);
inventoryTracker.requestItems();
// 良い例
// InventoryTracker クラスは抽象クラスのInventoryRequester に依存し、
// 外部から具象クラスを注入することで柔軟性が向上
class InventoryTracker {
List<String> items;
InventoryRequester requester;
InventoryTracker({
required this.items,
required this.requester,
});
void requestItems() {
for (var item in items) {
requester.requestItem(item);
}
}
}
abstract class InventoryRequester {
void requestItem(item);
}
class InventoryRequesterV1 implements InventoryRequester {
@override
void requestItem(item) {
// ...
}
}
class InventoryRequesterV2 implements InventoryRequester {
@override
void requestItem(item) {
// ...
}
}
// 外部で依存関係を構築し、注入することで、リクエストモジュールを簡単に切り替えることができる
final inventoryTracker = InventoryTracker(
items: ['apples', 'bananas'],
requester: InventoryRequesterV2(),
);
inventoryTracker.requestItems();
Dart
コメント