はじめに
開発効率を向上させ、品質の高いシステムを構築するには、コード品質を一定以上に保つ意識が非常に大切です。
コード品質を向上させる取り組みは、プログラムの言語仕様を覚えること以上に重要です。最も大切なのは、次の3点です。
- 可読性を高める: コードが読みやすければ、他の開発者が理解しやすく、バグの発見や修正が容易になります。
- 保守性を高める: 変更や機能追加が必要になった際に、影響範囲を特定しやすく、効率的に作業を進められます。
- 再利用性を高める: 汎用的な機能はコンポーネントとして切り出し、複数の場所で利用することで、開発の手間を省き、コードの一貫性を保てます。
このためには、以下の取り組みが大切です。
- プログラムを適切な単位に分割する
- 適切な命名を行う
- 適切なコメントを残す
これらのことを意識することで、開発コストの削減につながり、長い目で見て利益になります。
コンポーネント分割の考え方について
本章では注文情報を処理するシステムを例に、コンポーネント分割の考え方について説明していきます。
基本となる考え方「単一責任原則」
-
1つのコンポーネント(部品として使うための、変数・定数、メソッド、クラスなど)は、1つの機能しか持たせてはなりません。
-
部品として使う変数、メソッド、クラスの具体例
- 変数・定数の例:
MAX_LOGIN_ATTEMPTS(ログイン試行の最大回数) のような定数があります。システム全体でログイン失敗の許容回数を統一するために、多くの場所で参照される共通の「部品」です。 - メソッドの例:
isValidEmail(emailAddress)(メールアドレスの形式チェック) のようなメソッドがあります。ユーザー登録、問い合わせフォームなど、システム内の様々な場所でメールアドレスの形式を検証する際に「部品」として再利用されます。 - クラスの例:
DatabaseConnector(データベース接続管理) のようなクラスがあります。データベースへの接続確立、切断、トランザクション管理といった技術的な処理に特化しており、複数のデータ操作クラスから「部品」として利用されます。
- 変数・定数の例:
-
複数のコンポーネントを統括したクラスの具体例
OrderProcessingService(注文処理サービス) クラスのprocessNewOrder(orderRequest)メソッドを考えてみましょう。このメソッドは、新しい注文を受け付けた際に一連のビジネスロジックを実行します。- まず、
OrderValidator.validate(orderRequest)というバリデーション部品を呼び出し、注文内容が正しいか確認します。 - 次に、
InventoryService.allocateStock(productId, quantity)という在庫管理部品を呼び出し、商品の在庫を引き当てます。 - 続いて、
OrderRepository.save(order)というデータ保存部品を呼び出し、注文情報をデータベースに保存します。 - 最後に、
EmailService.sendOrderConfirmation(customerEmail, orderDetails)というメール送信部品を呼び出し、顧客に注文確認メールを送ります。
- まず、
このように、
OrderProcessingServiceは、個々の「部品」コンポーネント(バリデーション、在庫管理、データ保存、メール送信)を組み合わせて、「新しい注文を処理する」というビジネス的な一連の大きな機能を提供します。これは、各部品コンポーネントがそれぞれの専門的な役割に集中しつつ、上位のコンポーネントがそれらを組み合わせてより大きな価値を生み出す良い例です。 -
部品として使う変数、メソッド、クラスの具体例
-
コンポーネントを変更する目的が2つ以上あった場合、コンポーネント分割を検討するべきです。
- コンポーネントを分割すべき例:
UserProcessorというクラスが 直接 、「ユーザーの認証」「データベースのCRUD」「メール送信」という3つの役割を持っているとします。この時に、「認証ロジックの変更」「DBスキーマの変更」「メールテンプレートの変更」といった様々な理由でこのクラスを変更する必要が生じるため、影響範囲が広くなりやすいです。 - 分割後のコンポーネントの例:
Authenticator(認証)、UserRepository(DB操作)、EmailSender(メール送信)のように役割ごとにクラスを分割します。これにより、各クラスはそれぞれの責務に集中でき、変更が必要な場合もそのクラスのみを修正すれば済む可能性が高まります。
ただし、この場合に
UserProcessorというクラスを作成し、中身はAuthenticatorクラス、UserRepositoryクラス、EmailSenderクラスを操作することのみに従事させるということは行っても問題ありません。これは、UserProcessorが「ユーザー関連の様々な処理を統括する」という単一の責任を持つため、単一責任原則に反しません。 - コンポーネントを分割すべき例:
「部品として振る舞うべきもの」をコンポーネントとして切り出すのは、コピペによる「車輪の再発明」を防ぎ、コードの保守性を高める上で重要です。
具体例
「注文情報を処理する」という機能を単一責任原則に基づいて分析してみましょう。以下の手順で考えると良いでしょう。
-
「必要な機能は何か?」を考えます
- お客様からの注文(新規発注、注文変更、注文取り消し)を受け付ける
- 注文内容に基づいて、在庫テーブルを操作する(在庫確認、引当て、引き戻し)
- 注文情報、引当情報に基づいて、出荷指示を出す
-
1.で出てきた機能を、「〇〇する」と一言で言いきれる単位に分割します
-
注文管理
- 新規の注文を受領する (
OrderService.createOrder()) - 既存の注文を変更する (
OrderService.updateOrder()) - 既存の注文を取り消す (
OrderService.cancelOrder())
- 新規の注文を受領する (
-
在庫管理
- 在庫を確認する (
InventoryService.checkStock()) - 在庫を引き当てる (
InventoryService.allocateStock()) - 在庫を引き戻す (
InventoryService.releaseStock())
- 在庫を確認する (
-
出荷管理
- 出荷指示を出す (
ShippingService.dispatchOrder())
- 出荷指示を出す (
-
注文管理
- ここからさらに、「注文テーブルへのアクセス」「在庫テーブルへのアクセス」、さらには「DBアクセスを行う」といった、より抽象的な単位への分割が考えられます。
「単一責任原則」の考え方を身に着ける上で大切なこと
- これから作ろうとしている機能が、すでにコンポーネントやライブラリとして提供されていないかをまず検討します。
-
例: 日付計算が必要な場合、自分でロジックを実装するのではなく、
JavaScriptであればmoment.jsやdate-fnsのような既存のライブラリの利用を検討しましょう。
-
例: 日付計算が必要な場合、自分でロジックを実装するのではなく、
- 「単一責任原則に基づいた分割ができているか?」の判断はある程度慣れが必要なため、熟練者によるコードレビューを受けることが重要ですし、効果的です。
適切な命名とコメント
コンポーネント分割によって機能の責務が明確になったとしても、個々のコードが読みづらければ、その効果は半減してしまいます。ここでは、コードの意図を明確にするための「命名規則」と「コメント」の重要性について解説します。
例えば、次のようなコードを見た場合、その意図をすぐに理解できるでしょうか?
const EIGHT = 8;
const TEN = 10;
const a = new A();
if( a.b == EIGHT ) {
// 何らかの処理
} else if ( a.b == TEN ){
// 何らかの処理
}
何を行いたいのか分かりませんよね?
このようなコードを改善させるためには、またこのようなコードを作らないためには、「命名規則」と「適切なコメント」の概念を知っておく必要があります。
命名規則の考え方について
-
それが何をするためのものなのか?を考えて命名します。
-
悪い例: 関数名が実際の動作(固定値5を返すこと)と一致せず、誤解を招きます。
function random(): number { return 5; }
-
悪い例: 関数名が実際の動作(固定値5を返すこと)と一致せず、誤解を招きます。
-
プロジェクト全体で、統一的なルールを定めます。
-
原則、英語で命名します。
-
(TypeScriptでは)変数、メソッドはcamelCaseを使います。
-
良い例:
userName,calculateTotalPrice() -
悪い例:
user_name,CalculateTotalPrice()
-
良い例:
-
let a: number;のような1文字変数は禁止です(ループカウンタなどの例外を除く)。-
良い例:
customerCount,productPrice -
悪い例:
cnt,prc
-
良い例:
-
strName、iPriceのようなハンガリアン記法は禁止です。- 良い例: userName, price
-
boolean値はisXxx、hasXxxなど、YesかNoが答えになるような命名をします。-
良い例:
isValid,hasPermission,isLoggedIn -
悪い例:
loggedInStatus
-
良い例:
-
定数やEnumはSNAKE_CASEを使います。
-
良い例:
MAX_CONNECTIONS,STATUS_ACTIVE
-
良い例:
-
クラスはPascalCaseを使います。
-
良い例:
UserService,ProductOrder
-
良い例:
-
プロジェクト固有の言葉については、用語集を作成して勝手な命名が行われないように管理します。
-
悪い例: 検査を受ける「患者」をあらわす変数名として、
patient、client、user、customerなど複数種類の単語で命名。(同じ概念を指す言葉が複数あると、開発者がどの変数を使えばよいか迷い、コードの一貫性が失われ、バグの温床になります。) -
OK: 「患者」を意味する変数名は
patientで統一。(別にclientで統一しても構いません。問題なのは「システム内で、同じものや概念を指す言葉」が統一されていない状態です。)
-
悪い例: 検査を受ける「患者」をあらわす変数名として、
-
getXxx(取得),setXxx(設定),createXxx(作成),deleteXxx(削除) など、具体的かつ的確な接頭辞をつけることで、変数やメソッドの目的を明確化します。-
良い例:
getUserById(),setProductName(),createOrder(),deleteItem()
-
良い例:
-
例外1: 同一スコープ内において数行程度で使い捨てられ、その機能にとって本質的に意味のないもの(スワップ関数で入れ替え時のバッファに使う一時変数など)は
tmpのような意味のない名前をあえて使います。// 例: 値の交換 let a: number = 10; let b: number = 20; let tmp: number = a; // tmpは一時的な変数であることを示す a = b; b = tmp; -
例外2: 単にループ回数を管理するだけのループカウンタはi, j, kのような1文字の変数をあえて使います。
for (let i = 0; i < list.length; i++) { // ... }ただし、ループインデックスを処理の結果として用いない場合は、
forループではなくforEachループを用いるのが一般的です。
-
-
命名規則に則っているかを確認するため、AIを活用することも有効です。
プロンプト例 :
このプロジェクトでは、camelCaseで変数名を命名し、isXxxでboolean値を命名するという規則を採用している。
商品の在庫があるかどうかを示す変数として、productStockStatusという変数名を定義した。
これは適切な命名か?より適切なものはあるか?
コメントについて
-
JSDocなどを用いて、標準的なコメントの記載ルールを守ります。(JSDocの書き方については各自自習することをお勧めします。)
-
その処理が 何のために行われるのか? をコメントとして記載し、入出力、例外も記載します。
-
良い例:
/** * ユーザーのログイン認証を行います。 * 認証成功時にはセッション情報を生成し、失敗時には例外をスローします。 * @param {string} username ユーザー名 * @param {string} password パスワード * @returns {string} 認証されたユーザーのセッションID * @throws {AuthenticationException} 認証に失敗した場合 */ function authenticateUser(username: string, password: string): string { // 認証ロジック // ... }
-
良い例:
-
for文の頭に「10回ループする」など、コードを見れば一目瞭然なコメントは禁忌です。-
悪い例:
// 10回ループする for (let i = 0; i < 10; i++) { // ... } -
良い例: コメントを書かない
for (let i = 0; i < 10; i++) { // ... } -
更に良い例: なぜループ回数が10回固定なのかが書かれている。
// テストデータを10件作成する for (let i = 0; i < 10; i++) { // ... }
-
-
何らかの目的であえて定石外しをしているコードについて、その理由をコメントで記載します。(原則として、定石を外さないことが前提です。)
-
例:
// [理由]: パフォーマンス最適化のため、通常のデータベーストランザクションではなく、 // 外部サービスの非同期APIを直接呼び出しています。 // エラーハンドリングは後続のポーリング処理で担保されます。 externalService.sendAsyncRequest(data);
-
例:
-
コメントに嘘を書いてはいけません。コードを修正した場合、コメントを最新化しましょう。
-
悪い例: コメントとコードの内容が一致していない
/** * この関数はユーザーIDに基づいて、画面表示用のユーザー名を返します。 */ function getUserNameForDisplay(userId: number): string { // ユーザーの姓と名を表示文字列とする return userDAO.getEmail(userId); } -
良い例: コメントとコードの内容が一致している
/** * この関数はユーザーIDに基づいて、画面表示用のユーザー名を返します。 */ function getUserNameForDisplay(userId: number): string { // ユーザーのメールアドレス表示文字列とする return userDAO.getEmail(userId); }
-
-
詳細設計書の内容をコメントとして記述し、それをコードとして「翻訳」する意識(コードからでは分かりにくい、処理の目的や補足などはコメントとして残す意識)でコーディングすると、コードの内容がより明確となります。すなわち、日本語の文書として整理できていないものをいきなりコーディングしようとしないことです。
例示したコードの修正案
// 数字の意味が定数名でわかるようになります。
const LOW_TAX_RATE = 8;
const HIGH_TAX_RATE = 10;
// Productクラスのインスタンスであることも命名で明確化します。
const product = new Product();
if (product.taxRate === LOW_TAX_RATE) {
// 何らかの処理
} else if (product.taxRate === HIGH_TAX_RATE) {
// 何らかの処理
}
最後に
コード品質を意識することは、未来の自分やチームメンバーへの投資です。今日からできることを少しずつ始めてみませんか?
また、以下に実践的なテクニックを書きました。もし良ければ、読んでいただけると幸いです。