はじめに
“Clean Code for Dart”は、JavaScript向けの”clean-code-javascript”をDart言語に対応させたプロジェクトです。このプロジェクトは、コードの読みやすさ、保守性、拡張性を向上させるために役立つ原則やベストプラクティスを提供します。このようなリソースを提供してくれる著者に感謝です。
この内容は、「Clean Code for Dart」の内容を基に、和訳とメモや解説を個人用にまとめたものです。長いため、3回に分けて掲載します。
GitHub - williambarreiro/clean-code-dart: 📖 Clean Code concepts adapted for Dart
📖 Clean Code concepts adapted for Dart. Contribute to williambarreiro/clean-code-dart development by creating an account...
GitHub - ryanmcdermott/clean-code-javascript: :bathtub: Clean Code concepts adapted for JavaScript
:bathtub: Clean Code concepts adapted for JavaScript - ryanmcdermott/clean-code-javascript
変数
変数名は意味があり、発音しやすいものを使用すること
// 悪い例
// yyyymmdstr のような読みにくい略語が使われてる
final yyyymmdstr = DateFormat('yyyy/MM/dd').format(DateTime.now());
// 良い例
// currentDate のように、変数の目的や意味を明確に示す名前が使われている
final currentDate = DateFormat('yyyy/MM/dd').format(DateTime.now());
Dart同じ種類の変数には同じ用語を使用すること
// 悪い例
// 同じタイプの操作に対して異なる用語を使用している
getUserInfo();
getClientData();
getCustomerRecord();
// 良い例
// 同じタイプの操作に対して一貫した用語を使う
getUser();
Dart検索可能な名前を使用すること
// 悪い例
// 32の意味や意図が作者本人にしかわからない
Future.delayed(Duration(minutes: 32), launch);
// 良い例
// コンパイル時に値がわかっている場合は、const
// 変数が 1 回だけ割り当てられる場合は、final (.envから値を取ってくる場合など)
// lowerCamelCaseを使用する事
const setupTimeInMinutes = 32;
Future.delayed(Duration(minutes: setupTimeInMinutes), launch);
Dart説明的な変数を使用すること
// 悪い例
// 直接配列の要素を指定しているので、どの要素が何を表しているのかわかりづらい
const address = <String>['One Infinite Loop', 'Cupertino', '95014'];
saveCityZipCode(address[1], address[2]);
// 良い例
const address = <String>['One Infinite Loop', 'Cupertino', '95014'];
final city = address[1];
final zipCode = address[2];
saveCityZipCode(city, zipCode);
Dartメンタルマッピングを避けること
// 悪い例
// dispatch(l)のlが何を表しているのかすぐに理解できない
const locations = <String>['Austin', 'New York', 'San Francisco'];
locations.forEach((l) {
doStuff();
doSomeOtherStuff();
dispatch(l);
});
// 良い例
// 暗黙的な変数名より明示的な変数名を選択する(for文のiやjなどは例外)
// 明確な変数名を使用しているので、コードの意図がすぐに理解できる
const locations = <String>['Austin', 'New York', 'San Francisco'];
locations.forEach((location) {
doStuff();
doSomeOtherStuff();
dispatch(location);
});
Dart不必要な文脈を追加しないこと
// 悪い例
// クラス/オブジェクト名から何かがわかる場合、繰り返されるような表現を避ける
// Carというクラスのインスタンス変数に、不要な接頭辞を付けている
final car = Car(
carMake: 'Honda',
carModel: 'Accord',
carColor: 'Blue',
);
void paintCar(Car car, String color) {
car.carColor = color;
}
// 良い例
final car = Car(
make: 'Honda',
model: 'Accord',
color: 'Blue',
);
void paintCar(Car car, String color) {
car.color = color;
}
Dart条件式やショートサーキットではなくデフォルトパラメータを使用すること
// 悪い例
// ショートサーキット演算子 (??) を使ってデフォルト値を設定しているが、これは読みやすさに欠ける場合がある
void createMicrobrewery({String? name}) {
final breweryName = name ?? 'Hipster Brew Co.';
}
// 良い例
// デフォルトパラメータを関数の引数として直接指定することで、よりシンプルで読みやすいコードにしている
void createMicrobrewery({String breweryName = 'Hipster Brew Co.'}) {
}
Dart関数
関数の引数は2つ以下が理想的
// 悪い例
// パラメータが少ないほど、関数のテストが容易になる
// 3つ以上の引数がある場合は、引数を統合することを検討するべき
Menu getMenu(String title, String body, String buttonText, bool cancellable) {
// ...
}
// 良い例
// 名前付きパラメータを使用することで、関数がどのようなプロパティを期待しているかが明確になる
// 他にも、クラス/オブジェクトを引数に渡す事で簡潔に記述する事ができる
Menu getMenu({
required String title,
required String body,
required String buttonText,
required bool cancellable,
}) {
// ...
}
final menu = getMenu(
title: 'Foo',
body: 'Bar',
buttonText: 'Baz',
cancellable: true,
);
Dart関数は一つのことだけを行うべき
関数が複数のことを行うと、組み合わせが難しくなり、テストや理解が困難になります。関数を一つのアクションに分離できると、リファクタリングが容易になり、コードがより読みやすくなります。
// 悪い例
// クライアントのデータベース参照とアクティブなクライアントに対するメール送信が1つの関数で行われている
void emailClients(List<Client> clients) {
for(final client in clients) {
final clientRecord = database.lookup(client);
if (clientRecord.isActive()) {
email(client);
}
}
}
// 良い例
// アクティブなクライアントに対してのみメールを送信する関数が分離され、関数名や実装がシンプル
void emailActiveClients(List<Client> clients) {
clients
.where(isActiveClient)
.forEach(email);
}
bool isActiveClient(Client client) {
final clientRecord = database.lookup(client);
return clientRecord.isActive();
}
Dart関数名は何をするかを明確に示すべき
// 悪い例
// 関数名から何が追加されるかわかりにくい
void addToDate(DateTime date, int months) {
}
final currentDate = DateTime.now();
addToDate(currentDate, 1);
// 良い例
// 関数名が明確に何を行うかを示している
void addMonthsToDate(int months, DateTime date) {
}
final currentDate = DateTime.now();
addMonthsToDate(1, currentDate);
Dart関数は一つの抽象度レベルに留めるべき
// 悪い例
// 複数の抽象化レベルを持ち、複雑で読みづらい
void parseBetterAlternative(String code) {
const regexes = [
// ...
];
final statements = code.split(' ');
final tokens = [];
for (final regex in regexes) {
for (final statement in statements) {
tokens.add( /* ... */ );
}
}
final ast = <Node>[];
for (final token in tokens) {
ast.add( /* ... */ );
}
for (final node in ast) {
// parse...
}
}
// 良い例
// tokenize関数とlexer関数がそれぞれ一つの抽象化レベルで処理を行い、
// parseBetterAlternative関数ではこれらの関数を組み合わせて処理を行っている
// 機能を分割する事で再利用可能になり、テストが容易になる
List<String> tokenize(String code) {
const regexes = [
// ...
];
final statements = code.split(' ');
final tokens = <String>[];
for (final regex in regexes) {
for (final statement in statements) {
tokens.add( /* ... */ );
}
}
return tokens;
}
List<Node> lexer(List<String> tokens) {
final ast = <Node>[];
for (final token in tokens) {
ast.add( /* ... */ );
}
return ast;
}
void parseBetterAlternative(String code) {
final tokens = tokenize(code);
final ast = lexer(tokens);
for (final node in ast) {
// parse...
}
}
Dart重複したコードを削除する
// 悪い例
// コードが重複すると、ロジックを変更する必要がある場合に変更箇所が複数にわたり、バグの温床になる
Widget buildDeveloperCard(Developer developer) {
return CustomCard(
expectedSalary: developer.calculateExpectedSalary(),
experience: developer.getExperience(),
projectsLink: developer.getGithubLink(),
);
}
Widget buildManagerCard(Manager manager) {
return CustomCard(
expectedSalary: manager.calculateExpectedSalary(),
experience: manager.getExperience(),
projectsLink: manager.getMBAProjects(),
);
}
// 良い例
// buildEmployeeCard関数が引数の種類に応じて異なるprojectsLinkを取得し、共通のカスタムカードを構築
// 重複するコードが排除されている
// 複数の異なる要素が似ているが微妙に異なる場合、重複コードを削除するために
// 適切な抽象化を行うことが重要だが、悪い抽象化は重複コードよりも問題を引き起こすことがある
// 抽象化を行う際は、SOLIDの原則に従うことが重要
Widget buildEmployeeCard(Employee employee) {
String projectsLink;
if (employee is Manager) {
projectsLink = manager.getMBAProjects();
} else if (employee is Developer) {
projectsLink = developer.getGithubLink();
}
return CustomCard(
expectedSalary: employee.calculateExpectedSalary(),
experience: employee.getExperience(),
projectsLink: projectsLink,
);
}
Dart関数のパラメータとしてフラグを使用しない
// 悪い例
// フラグを使用すると、この関数が複数のことを行うことをユーザーに表す
// 関数は1つのことを行うべきで、ブール値に基づいて異なるコードパスを実行する場合は、関数を分割する事
void createFile(String name, bool temp) {
if (temp) {
File('./temp/${name}').create();
} else {
File(name).create();
}
}
// 良い例
// createFile関数とcreateTempFile関数が明確に分離
// 関数の役割が明確化され、コードがより理解しやすくなる
void createFile(String name) {
File(name).create();
}
void createTempFile(String name) {
File('./temp/${name}').create();
}
Dart副作用を避ける(part 1)
副作用 (Side Effect) とは、関数が値を受け取って別の値または複数の値を返す以外の何かを行う場合を指します。副作用には、ファイルへの書き込み、グローバル変数の変更などが含まれます。
// 悪い例
// splitIntoFirstAndLastName関数がグローバル変数 name を直接書き換えている(副作用)
// 他の関数が同じ変数を使用する場合に問題を引き起こす可能性がある
dynamic name = 'Ryan McDermott';
void splitIntoFirstAndLastName() {
name = name.split(' ');
}
splitIntoFirstAndLastName();
print(name); // ['Ryan', 'McDermott'];
// 良い例
// splitIntoFirstAndLastName関数は引数として name を受け取り、新しい値を返す(副作用の回避)
// プログラムには時々副作用が必要、たとえば、ファイルへの書き込みが必要な場合など
// その場合には、特定のファイルに書き込む複数の関数やクラスを持たず、1つだけその役割を担当するサービスを持つべき
List<String> splitIntoFirstAndLastName(name) {
return name.split(' ');
}
final name = 'Ryan McDermott';
final newName = splitIntoFirstAndLastName(name);
print(name); // 'Ryan McDermott';
print(newName); // ['Ryan', 'McDermott'];
Dart副作用を避ける(part 2)
// 悪い例
// 元の配列を直接書き換えている
// 副作用を持つ関数であり、他の関数に影響を与える可能性がある
void addItemToCart(List<int> cart, int item) {
cart.add(item);
}
final cart = <int>[1, 2];
addItemToCart(cart, 3);
print(cart); // [1, 2, 3]
// 良い例
// 戻り値として返す事で、元のデータを改変しない
// 関数が新しいカートを作成して返すことで、古いカートを変更せずに新しいカートを返す
// 大きなオブジェクトをクローンすると、パフォーマンス的に問題が起きる場合があるが、
// メモリ消費量を抑えて、素早く処理を行うための優れたライブラリ(Freezed など)がある
List<int> addItemToCart(List<int> cart, int item) {
return [...cart, item];
}
final cart = <int>[1, 2];
final newCart = addItemToCart(cart, 3);
print(cart); // [1, 2]
print(newCart); // [1, 2, 3]
Dart関数型プログラミングを優先する
// 悪い例
// 手続き型プログラミングのアプローチ
final programmerOutput = <Programmer>[
Programmer(name: 'Uncle Bobby', linesOfCode: 500),
Programmer(name: 'Suzie Q', linesOfCode: 1500),
Programmer(name: 'Jimmy Gosling', linesOfCode: 150),
Programmer(name: 'Gracie Hopper', linesOfCode: 1000),
];
var totalOutput = 0;
for (var i = 0; i < programmerOutput.length; i++) {
totalOutput += programmerOutput[i].linesOfCode;
}
// 良い例
// 関数型プログラミングのアプローチ
// foldメソッドを使用してリスト内の各要素に対して畳み込み演算を行い、
// 初期値を0として、linesOfCodeを加算して合計値を返す
// この例では、0がマジックナンバーになっているので、const initialOutput = 0; とする方が良い
// DartはHaskellのような純粋な関数型言語ではないが、関数型の要素を持っているので相性が良い
final programmerOutput = <Programmer>[
Programmer(name: 'Uncle Bobby', linesOfCode: 500),
Programmer(name: 'Suzie Q', linesOfCode: 1500),
Programmer(name: 'Jimmy Gosling', linesOfCode: 150),
Programmer(name: 'Gracie Hopper', linesOfCode: 1000),
];
final totalOutput = programmerOutput.fold<int>(
0, (previousValue, programmer) => previousValue + programmer.linesOfCode);
Dart条件分岐のカプセル化
// 悪い例
// 条件文が直接使用されていて、意味や目的が明確ではない
// 同じ条件が複数の場所で使用される場合、同じ内容を記述しないといけない
if (programmer.language == 'dart' && programmer.projectsList.isNotEmpty) {
// ...
}
// 良い例
// 条件文を関数としてカプセル化、コードの可読性と再利用性を向上させる
bool isValidDartProgrammer(Programmer programmer) {
return programmer.language == 'dart' && programmer.projectsList.isNotEmpty;
}
if (isValidDartProgrammer(programmer)) {
// ...
}
Dart否定的な条件文を避ける
// 悪い例
// isFileNotValidという否定的な関数が定義されている
// 否定的な関数を否定演算子で評価している為、理解しにくい
bool isFileNotValid(File file) {
// ...
}
if (!isFileNotValid(file)) {
// ...
}
// 良い例
bool isFileValid(File file) {
// ...
}
if (isFileValid(file)) {
// ...
}
Dart条件文を避ける
// 悪い例
// Airplane クラスが多くの責務を持ってしまっている
// if文があるとその関数が、1つ以上のことを行うことを意味する
class Airplane {
// ...
double getCruisingAltitude() {
switch (type) {
case '777':
return getMaxAltitude() - getPassengerCount();
case 'Air Force One':
return getMaxAltitude();
case 'Cessna':
return getMaxAltitude() - getFuelExpenditure();
}
}
}
// 良い例
// ポリモーフィズムを使用する事で同じタスクを達成できる
// 専用のサブクラスを作成し、getCruisingAltitude メソッドをオーバーライドして、それぞれの航空機タイプに適した高度を計算
// 条件文を避けてクラスを単純化し、各クラスが一つの責務を持つようにしている
class Airplane {
// ...
}
class Boeing777 extends Airplane {
// ...
double getCruisingAltitude() {
return getMaxAltitude() - getPassengerCount();
}
}
class AirForceOne extends Airplane {
// ...
double getCruisingAltitude() {
return getMaxAltitude();
}
}
class Cessna extends Airplane {
// ...
double getCruisingAltitude() {
return getMaxAltitude() - getFuelExpenditure();
}
}
Dartデッドコードの削除
// 悪い例
// 実行されないコードを保持する意味はない
// デッドコードを残すことは、保守性やコードの理解を難しくする原因となる
Future<void> oldRequest(url) {
// ...
}
Future<void> newRequest(url) {
// ...
}
await newRequest();
// 良い例
// 使われていないoldRequest関数を削除
Future<void> newRequest(url) {
// ...
}
await newRequest();
Dart