前半からの続き
- 品質の高いコードの特徴
- 技術的負債と品質の放棄
- レガシーコードの管理
- リファクタリングと名前の変更
- 関数名はその意図を表す
- クラスと関数の命名
- コードの型付け
- 定数と設定ファイル
- 他の人が理解しやすいアプローチを選ぶべき
- 「偶発的な複雑さ」と「プレオプティマイゼーション」
- エラー管理
開発の原則について覚えておくべきこと
知ることは良いことですが、原則を使用することがもっと良いことです。
私の場合、実際に適用し始めたのはつい最近からです。
何事においても始めるに遅いことはありません。遅くてもやらないよりはマシです。
ただ、何事もそうですが、時間が必要です。
SRP(単一責任の原則)
単一責任の原則はおそらく最も重要です。
関数は一つのアクション、一つの目的しか持ちません。
その名前の範囲を超えることはなく、正確にその名前が示すことを行う。それだけです。
単に関数のアクションを最小限に抑えるように努めてください。
悪いコード
import React, { useEffect, useState } from 'react';
function UserProfile() {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch('https://api.example.com/user')
.then(response => response.json())
.then(data => {
setUser(data);
setLoading(false);
});
}, []);
if (loading) {
return <div>Loading...</div>;
}
return (
<div>
<h1>{user.name}</h1>
<p>Email: {user.email}</p>
</div>
);
}
コンポーネントが複数の責任を持っています。データの取得と表示を同時に行っています。
良いコード
import React from 'react';
import useUserData from './hooks/useUserData';
function UserProfile() {
const { user, loading } = useUserData('https://api.example.com/user');
if (loading) {
return <div>Loading...</div>;
}
return (
<div>
<h1>{user.name}</h1>
<p>Email: {user.email}</p>
</div>
);
}
import { useState, useEffect } from 'react';
function useUserData(url: string) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch(url)
.then(response => response.json())
.then(userData => {
setData(userData);
setLoading(false);
});
}, [url]);
return { user: data, loading };
}
export default useUserData;
良いコードでは、useUserData
フックがデータ取得の責任を持ち、UserProfile
コンポーネントはデータを表示する責任のみを持っています。これにより、各部分が独立しており、再利用やテストが容易になります。
KIS(シンプルに保つ)
複雑なものは決して良い結果を生み出さない。
ルイ=ナポレオン・ボナパルト、『ナポレオン三世の著作』(1854-1869)
最もシンプルな解決策が常に最良です。
複雑になったら、できるだけ細かく分割してみてください。
小さいほど理解しやすいです。
コードを機能ごとに分割してください。
DRY(繰り返しを避ける)
一見シンプルで簡単に聞こえますが、時と場合によっての判断が曖昧です…
例えば、二つのテーブルを持っているとします。これらのテーブルの情報は異なりますが、スタイルやテンプレートがとても似たり寄ったりしています。
オブジェクトを表示するためのインターフェースを使用し、スタイルの小さな違いは条件で管理すると考えるかもしれません。
いくつかの要素がある場合、別々のファイルに分ける方が良いのでしょうか。
一般的に、可能な限り以下を実行する必要があります。
- 類似のコードを最大限に共有する
- コードをコピー&ペーストしたいと思ったら、サービスの関数にそれを抽出する
- コードを分割することを恐れないでください。それによってコードがモジュール化されます
具体的には、コンポーネントや機能が異なる場合や、再利用性が低い場合、またはコンポーネントが特定の責任を超えて複雑になりすぎる場合には、ファイルを分割することを検討してください。
基本的には、コードの重複を避け、保守性と可読性を高めるために、共通の機能やコンポーネントを抽象化し、再利用可能なモジュールやサービスに分割することが推奨されます。
コードをコピペしても大丈夫です!
クリーンコードでは、すべての重複を避けるべきだとは言っていません。
重複を避けることは、時には過剰な抽象化を招くことがあります。
AからBへのコードのコピー&ペーストを行うのではなく、Cにその責任を委譲し、AとBを(独立して)Cにリンクします。
// コードAとコードBで繰り返されているこの部分をコピー&ペーストするのではなく
// コードA
if (user.roles.includes("admin")) {
return false;
}
// コードB
if (user.roles.includes("admin")) {
return false;
}
// この機能を関数で置き換えること
const checkUserRightFor = (userToCheck: User, role: string) => {
// コードC
if (userToCheck.roles.includes(role)) {
throw new Error(`User ${userToCheck.name} does not have role ${role}`);
}
}
// 必要な場所でそれを呼び出す(AとBで)
checkUserRightFor(user, 'admin');
ユーザーの権限をチェックするロジックをcheckUserRightFor
関数に抽象化し、必要な場所でこの関数を呼び出しています。
これにより、コードの重複を減らし、将来的な変更が容易になります。また、エラーメッセージを動的に生成しており、どのユーザーがどのロールを持っていないかを明確にしています。
YAGNI(必要になるまで作らない)
「それは必要ないでしょう」
ユーザーのニーズは何ですか?
シンプルなフォームがあるランディングページですか?
その場合、HTMLとCSSだけで十分でしょう。
もし編集可能な内容が必要なら、Goや他のバックエンド言語を使うこともできます。
主な考え方は、無駄に多くの依存関係を持たないことです。
Web開発において、必要以上に複雑な技術スタックを使用することを避け、プロジェクトの要件に応じて最もシンプルで適切な技術を選択した方がよいです。
例えば、単純なランディングページであれば、ReactやRedux、Socket.ioなどの複雑なJavaScriptフレームワークやライブラリを使う必要はなく、HTMLとCSSだけで十分かもしれません。
また、jQueryのようにかつては広く使われていたライブラリも、現代のWeb開発では必ずしも必要ではありません。
SOC(関心の分離)
複数のことを同時に行うよりも、一つのことに専念してそれを完璧にこなす方が効果的である
機能をコンポーネントに分けて、それぞれのコンポーネント内でパーツを作成してください。
大きなコンポーネントを小さな、管理しやすい部分に分割し、それぞれの部分をしっかりと完成させることが重要です。
これにより、各機能が独立していて、問題が発生した場合に特定しやすくなります。また、テストが容易になり、全体としての品質が向上します。
ACID(原子性、一貫性、独立性、耐久性)
ACID原則は、あるアクションが正しく実行され、期待通りの結果をもたらすことを保証するためのものです。
重要なポイントは、アクションが確実に完了していると確信が持てない場合には、決してOKと返答しないことです。
これにより、データの整合性と信頼性が保たれ、システム全体の安定性が向上します。
IRI(意図を明らかにするインターフェース)
IRI(Intention Revealing Interfaces)の概念をクリーンコードからではなく、DDD(ドメイン駆動設計)から学びました。
原則はシンプルです。ソースコードは意図を明らかにするべきであり、背後にあるビジネスアイデアはシンプルで具体的で読みやすいものにするべきです。
- メソッド名や変数名は具体的で説明的であるべき: これにより、コードを読むだけでその機能や振る舞いが理解できるようになります
- 不必要な詳細は隠蔽する: インターフェースを通じて必要な情報のみを公開し、実装の詳細は隠蔽します。これにより、インターフェースの使用者は、どのように動作するかではなく、何をするかに集中できます
- ドメイン用語を使用する: ドメイン固有の言語を使用することで、ビジネスの要件と技術的な実装の間のギャップを減らすことができます
IRIは、システムの各部分がどのように連携して機能するかを理解するのに役立ち、大規模なプロジェクトやチームでの開発において特に有効です。
シンプルなデザインのための4つのルール
ケント・ベック(『テスト駆動開発』の著者)さんによって提供されたシンプルなデザインのための4つのルールです。
1. テストをパスすること(期待通りに機能する)
コードは設計されたテストケースをクリアする必要があり、これにより機能が正しく動作していることが保証されます。
2. 意図を明らかにする(読むだけで何をしているかが分かる)
コードを読むだけで、そのコードが何を意図しているのかが理解できるようにするべきです。
3. 重複を避ける(ビジネスルールは一箇所にのみ存在する)
同じコードを繰り返し書かないようにし、重複を避けることでメンテナンスが容易になります。
4. 「小さく」コーディングする、できるだけ細かく物事を扱うが、やりすぎない(大きすぎると複雑さが増す)
コードは小さく、シンプルに保つことで、理解しやすく、管理しやすいものにするべきです。大きく複雑なコードは避け、必要に応じて分割することが推奨されます。
リンターとコーディングスタンダード
リンターは、IDE内でコードが改善できる場合に警告を表示するツールです。
リンターは特に以下のことを可能にします。
- ソースコード内のエラーを検出する: リンターはコードを解析し、潜在的なエラーや問題点を指摘します
- 構文の問題を修正する: コードの書き方が間違っている場合、リンターは正しい構文に修正する方法を提案します
- 言語のベストプラクティスを守る: 各プログラミング言語には推奨される書き方があり、リンターはそれを遵守するように促します
- コーディングスタンダードの実施: チーム全体で一貫したコーディングスタイルを保つために、リンターはコーディングスタンダードに従っているかどうかをチェックします
さらに、多くのリンターには「fixer」機能が備わっており、右クリック一つで簡単にエラーを修正できます。
リンターを使用することで、コードはより読みやすく、理解しやすいものになります。これにより、エラーの発生を減らし、開発の効率を向上させることができます。
- プロジェクトに応じて以下のリンターをインストールする時間を取ってください(eslint、sass-lint、rubocopなど)
- フレームワークによる規約を使用する
- 使用しているフレームワークに応じたベストプラクティスを適用します
- 各言語の「コーディングスタイル」を尊重する
- よく使用されるデザインパターンを学ぶ
- Singleton(シングルトン)、DTO(Data Transfer Object)、Adapter(アダプター)などのデザインパターンを理解し、適切に使用することが重要です
これらのステップに従うことで、コードの品質を向上させ、保守性や拡張性を高めることができます。
また、チーム内でのコーディングの一貫性を保ちながら、効率的に開発を進めることが可能になります。
コードのクリーニング
クリーンコードの理論では、コードを触った後は触る前よりも綺麗にして去るべきだとされています。
しかし、複雑性が高い場合、コードのリファクタリングが困難になることがあります。
リグレッション(機能退行)に注意が必要です。プロジェクト全体のコードの品質を考慮することが開発者には求められます。自分のコードの品質だけでなく、プロジェクト全体のコードの品質です。
- コメントアウトされたコードや「
console.log
」を見つけたら削除しましょう - 入れ子のループに注意しましょう。複雑になりがちです
- 三項演算子の乱用を避けましょう。
- 例:
let result = variable === x ? (x === y ? [...x, ...y, ...z] : x) : y;
- 例:
- コードを触った後は、より論理的で読みやすい状態にしてください
- 理解するのに苦労した部分はコメントを残しましょう
- 疑問がある場合は、そのコードを変更すべきである可能性が高いです
これらのガイドラインに従うことで、より高品質でメンテナンスしやすいコードを書くことができます。
コードのコメントの正しい方法
コードのコメントの付け方については、常にコミュニティ内で議論されるテーマです。
一部の人々はすべてにコメントを付けるべきだと言い、他の人々は何もコメントを付けるべきではないと言います。
例えば、以下のような場合、コメントは適切でしょうか。
/**
* IDに基づいて顧客を返します。
*
* @param id 顧客のID
* @return Customer 顧客オブジェクト
*/
export const getCustomerById = (id: number): Customer => {
// ここに顧客を取得するロジックを実装します
}
確かに、クリーンなコードは適切なコメントの使用を通じて達成されます。
両方の世界の良い点を取り入れ、実用的であることが大切です。
- 明白な宣言にはコメントを付ける必要はありません
- 例:
/** 月の日付 */ export const dayOfMonth: number;
のように、その内容が自明である場合、コメントは不要です
- 例:
- 複雑な部分や必要を感じた場合にコメントを付ける: 次にコードを読む開発者が感謝するでしょう
- TODOコメントに注意する: これらはソースコード上に永遠に残る可能性があります
- コメントを通じて意図を表現する
- 例: 「これを行った理由は、あれが原因だからです」といった形で、なぜそのような実装を選んだのかを説明します
コード内のコメントのリスク
コメントが技術的な何かを説明するために使われる場合、次のことを自問してみてください。
これらのコメントは必要ですか?
もし必要だとしたら、コメントが必要なほどコードが複雑すぎるのではないでしょうか?
コメントはコードが何をしているかを説明するために使うべきではありません。
むしろ、ビジネス上の複雑な概念(コードではシンプル)を説明したり、追加のアイデアを提供するために使うべきです。
最悪なのは、しばしばコードは更新されるけれども、コメント形式のドキュメントは更新されないことです...
ファイルの分割と行の長さ
メソッドがユニークであればあるほど、読みやすく、デバッグもしやすくなります。さらに、パフォーマンスも向上します。
たくさんのファイルを作成することを躊躇してはいけません。
ファイルが特定のスコープを持つように努めてください。
- ファイルが300行を超えると多いです
- HTMLを含め、最大140文字の幅を保持してください(水平スクロールはコードの可読性と理解を損ないます)
- 関数は30行以内が望ましいです
- メソッド内のインデントは3レベルまでにしてください(プライベート関数を使用する、コードを分割する)
- 引数(またはパラメータ)が5つ以上ある場合は、関数内の処理を分割するか、オブジェクトを渡すことを検討してください
- 1つの関数は1つのことのみを行うべきです
- 関数のパラメータを不変(変更不可)にすることは良いことです
- 関数の戻り値が複雑な場合、専用の型でカプセル化することができます
- 小さく考える: 一般的に、より細かく分割するほど良いです
- 関数はそれを呼び出すコードよりも上に定義されるべきではありません
- 良質なコードは、ファイルの長さにも関連しています
これらの数字は、より読みやすいコードを目指すための指標です。絶対的なものではありません。
まとめ
高品質なコードを書くことはとても難しいことです。自分も含めてわたしたちは皆、何が「品質の高いコード」かについて自分なりの考えを持っています。
品質の高いソースコードを書くためには、読みやすく、きれいで、誰にでも理解できるコードを書く必要があります。
品質が高くメンテナンスしやすいコードを書くことで、プロジェクトの進行に伴い、ガイドラインを適用して、開発プロセスがスムーズになり、エラーの発生が減少し、全体的な生産性が向上します。
また、チーム内での一貫性を保ちながら、効率的に開発を進めることが可能になります。
この記事が、より良いプログラミングに向けたヒントを提供したことを願っています。