プリンシプルとは
- プログラミングの指針となる「前提」「原則」「思想」「習慣」「視点」「手法」「法則」などを指す
- 特定の技術に特化したものではなく、抽象度の高い情報
- プリンシプルを理解していれば、具体的な技術を学んだ時に「なぜその技術が必要なのか」を理解することができ、習得が早く、深くなる
1.前提
プログラミングに銀の弾丸はない
- プログラミングには魔法のような解決策はない
- これさえあれば必ずうまくいくという「特効薬」はない
- コードは設計書である
- 基本設計、詳細設計、プログラミング、テスト、デバッグまでが設計であり、そのアウトプットが設計書である「コード」である
- ロゼッタストーン
- 将来の保守担当者に対する簡潔な手引書(ドキュメント)
- 開発環境⇒ビルド/テストプロセスをの実行方法を記述する
- アーキテクチャ⇒コードからは読み取れない全体図を記述する
- その他⇒設計理由を記述する
- コードは必ず変更される
- 障害対応や機能拡張などの「変更に強いコードを書く」
- 書いている時間より読んでいる時間のほうが長いため、「読みやすいコードを書く」
2.原則
KISS
- 「シンプル」「単純」「簡潔」を最優先の価値を考える
- 本当に必要なコードだけ書く
- 「今」必要なコードだけ書く
- 要件にない余分なコードを書かない
- シンプルなソフトウェアは使いやすく、結果として長く使われる
DRY
- コードを重複して書いてはいけない
- 量が多くなり複雑になる ⇒ 読みづらく修正が難しくなる
- コードを抽象化する
- 関数化、モジュール化する ⇒ 読みやすく修正が1か所で済む
SLAP
- コードのレベルを合わせる
- 同じところには同じ抽象度の処理を書く
- 1つの関数の中で、データベース接続(低水準)とビジネスロジックの実行(高水準)といった不揃いな書き方をしない
OCP
- 「拡張に対して開いている」「修正に対して閉じている」という2つの属性を同時に満たす設計
- インターフェースを実装することで、コードの変更に柔軟に対応する
名前重要
- 命名は最重要課題
- 「使う側」「読む側」の視点に立って命名する
3.思想
プログラミングセオリー
プログラミングセオリーを支える3つの価値
- コミュニケーション
- コードは人に見せる文書であり、読む人のことを考えて書くべき
- シンプル
- コードをどうにか動かそうとした痕跡による複雑性を排除する
- 柔軟性
- コードの変更が容易な設計をする
- ただし柔軟性を言い訳に複雑なコードや設計を正当化しない
プログラミングセオリーを支える6つの原則
- 結果の局所化
- 変更の影響が局所にとどまるようにコードを構成する(ex.モジュール化)
- 繰り返しの最小化
- 重複を極力排除し、修正の影響を局所化する(ex.関数化)
- ロジックとデータの一体化
- 「ロジック」と「ロジックが操作するデータ」は同じ関数/モジュールにまとめる
- データと操作は修正タイミングがたいてい同じ
- 対称性
- コードに一貫性を持たせる
- アクティブがあればパッシブがあり、addがあればdeleteがあるなど
- 関数内で呼び出す関数の抽象度をそろえる
- コードに一貫性を持たせる
- 宣言型の表現
- 「命令型」ではなく「宣言型」で表現する
- 命令型
- 「どうやってやるか」を意識し、手順を細かく指示する(for文、while文)
- 細かく制御できるが冗長になりやすい
- 宣言型
- 「何をしたいか」を意識し、結果を指示する(map, filter, SQL, JSX)
- コードが簡潔で分かりやすい
- 命令型
// 配列の偶数だけを集める // 命令型の場合 const nums = [1, 2, 3, 4, 5]; let evens = []; for (let i = 0; i < nums.length; i++) { if (nums[i] % 2 === 0) { evens.push(nums[i]); } } console.log(evens); // [2, 4] // 宣言型の場合 const nums = [1, 2, 3, 4, 5]; const evens = nums.filter(n => n % 2 === 0); console.log(evens); // [2, 4]
- 「命令型」ではなく「宣言型」で表現する
- 変更頻度
- コードを修正するタイミングが同じ=変更理由が同じ要素をグルーピングする
- 単一責任の原則
アーキテクチャ根底技法
パッケージ化/モジュール化/カプセル化
関心の分離
- 「関心 = ソフトウェアの機能や目的」ごとにコードを分離(モジュール化)する
- 代表例はMVCで、ビジネスロジック/ユーザーへの表示/入力処理に分離している
- 影響範囲が関心内にとどまり、変更時の品質が安定する
ポリシーと実装の分離
- ポリシー:ソフトウェアの"前提に依存する"ビジネスロジックや引数
- 実装:ソフトウェアの"前提に依存しない"独立したロジック部分
- ポリシーが変更されたときに実装の変更が必要にならないよう、モジュールを分ける
インターフェースと実装の分離
- インターフェース
- アクセス可能な関数のシグネチャで構成
- モジュールの使用方法を定義する
- 実装
- ロジックとデータで構成
- モジュールが持つ機能を定義する
- モジュール同士の呼び出しにはインターフェースが使用されるようにする
具体例:ユーザー情報取得サービス
// インターフェース
// IUserService.ts
export interface IUserService {
getUser(id: number): Promise<User>;
}
export type User = {
id: number;
name: string;
};
// APIからユーザー情報を取得するサービス
// ApiUserService.ts
import { IUserService, User } from "./IUserService";
export class ApiUserService implements IUserService {
async getUser(id: number): Promise<User> {
// 実際には fetch などでAPIから取得
console.log(`Fetching user ${id} from API...`);
return { id, name: "Alice(API)" };
}
}
// ローカルDBからユーザー情報を取得するサービス
// LocalUserService.ts
import { IUserService, User } from "./IUserService";
export class LocalUserService implements IUserService {
async getUser(id: number): Promise<User> {
console.log(`Fetching user ${id} from local DB...`);
return { id, name: "Bob(Local)" };
}
}
// 利用側(インターフェースだけを意識)
// main.ts
import { IUserService } from "./IUserService";
import { ApiUserService } from "./ApiUserService";
import { LocalUserService } from "./LocalUserService";
async function showUserInfo(service: IUserService, id: number) {
const user = await service.getUser(id);
console.log(`User: ${user.name}`);
}
(async () => {
const apiService = new ApiUserService();
await showUserInfo(apiService, 1); // API実装を利用
const localService = new LocalUserService();
await showUserInfo(localService, 2); // ローカルDB実装を利用
})();
アーキテクチャ非機能要件
- リリース後の運用の大きなトラブルのほとんどが、パフォーマンスやシステムダウンなど、非機能な特性に起因する => 機能テストと同程度に非機能テストが重要
- 変更容易性(ChangeAbility)
- 保守性
- 簡単に修正できるか
- 拡張性
- 簡単に機能追加、モジュールのバージョンアップ、不要な機能の除去ができるか
- 再構築
- 簡単にモジュール間の関係の再組織化を行えるか、配置変更できるか
- 移植性
- 簡単に別のプラットフォームに移行できるか
- 保守性
- 相互運用性(InterOperability)
- プロトコルやデータ形式の選定では、業界の標準規格を選択する
- 効率性(Efficiency)
- リソースは限られているため、時間効率と資源効率を考慮する
- 時間効率
- スループット、レスポンスタイム、アラウンドタイムなど
- 資源効率
- CPU使用時間、メモリ使用量、ストレージ消費量、ネットワーク伝送量など
- 時間効率
- リソースは限られているため、時間効率と資源効率を考慮する
- 信頼性(Reliability)
- 例外発生時や不正な方法で使用された場合において、機能を維持する能力
- フォールトトレランス
- 障害が発生したときに、正常な動作を保ち続ける能力
- 例外発生時も正しいふるまいを保証し、内部的には修復を行う
- システムの冗長化やフェールソフトな設計を行う
- ロバストネス
- 不正な使用方法や入力ミスから保護する能力
- 例外を起こした処理は、必ずしも繰り返したり内部修復を行わない
- フェールセーフな設計を行う
- テスト容易性(Testability)
- ソフトウェアが大きくなるにつれて、テストは困難で高価なものになる
- テストの品質=本体の品質ととらえ、小さい単位でテストが可能な設計をする
- 再利用性(Reusability)
- 効率よく、品質のよい開発を行うには「できる限り作らず、借りてくる」
- 再利用可能なモジュールを作るのは、他と比べて3倍難しい(一般化した問題を想定するため)
7つの設計原理(コードレビュー観点)
- 単純原理
複雑なところにバグは出る。初心者でも読める単純なコードを書く。 - 同型原理
同じことは、同じように扱う。モジュールで使用する数値の単位や引数の型を統一する。 - 対称原理
アクティブがあればパッシブというように、処理の「対」になるものを考える。「set/get」「start/stop」「begin/end」など命名も一般的な対称性を考慮する。 - 階層原理
リソースの獲得を行ったら同じ階層でリソースの開放を行うなど、階層構造にこだわる。 - 線形原理
複雑な条件文や繰り返し文をなるべく避け、直線的な処理にこだわる。 - 明証原理
不確実性を取り除き、ロジックが明瞭なコードを書く。 - 安全原理
ありえないという条件をあえて考慮してコードを書く。(else, default, =NULLなど)
4.視点
凝集度
モジュールに含まれている機能の純粋さを表す尺度(参考)
結合度
モジュール同士の関係の密接さを表す尺度(参考)
- その他関連性のある考え方
- 冪等性(べきとうせい):ある操作を何回行っても結果が同じであること
- 安全性:操作対象の状態を変化させないこと(副作用)
直行性
あるコード同志が「独立性」「分離性」を持ち、片方を変更しても他方に影響を与えない
可逆性
問題があった時、やり直しができるようにしておく
コードの臭い
コードの中で、理解しにくい、修正しにくい、拡張しにくいと感じられる部分。
臭いには以下のような傾向があげられる。
- 関数内の処理が長すぎる
- モジュールが担う責務が大きすぎる
- モジュールが担う責務を分割しすぎた結果、細かいモジュールが多すぎる
- 名前と実際のコード内容があっていない
技術的負債
「修正しにくい」「理解しにくい」といった問題のある汚いコードを技術的負債と呼ぶ。
技術的負債は素早く返すことが何よりの対策となる。仮に素早く返せない事情があれば、本来書くべきであったコードの設計をドキュメントなどに残しておくことが重要。