ソフトウェアのコードは複雑であり、その複雑さは時間の経過とともに増していくと日々の業務で感じます。
私がフロントエンドエンジニアとして現在携わっているサービスも例外ではなく、その歴史の長さゆえにコードは膨大なものとなっており、過去に書かれたコードを保守しつつ、新しい機能を追加するコードを書いていかないといけない場面も多々あります。
その中で既存コードの確認に多くの時間を費やしたり、新しいコードを書いている途中で設計の方向性に迷いが生じることも少なくありません。
そうした経験を通じて、「プリンシプルオブプログラミング」という書籍から設計の原則を学びましたのでまとめます。
コード原則
1. KISS原則 - Keep It Simple, Stupid
KISS原則は 「シンプルに保つこと」 を大切にしなさいということです。
複雑なソフトウェア設計は避けるべきであり、可能な限りシンプルにすることで、コードの理解と保守が容易になります。
具体例:
-
不要な複雑さを排除する:
// 複雑なコード例 function calculateTotal(price1, price2, price3, taxRate) { const tax1 = price1 * taxRate; const tax2 = price2 * taxRate; const tax3 = price3 * taxRate; const total1 = price1 + tax1; const total2 = price2 + tax2; const total3 = price3 + tax3; const finalTotal = total1 + total2 + total3; return finalTotal; } // シンプルなコード例 function calculateTotal(price1, price2, price3, taxRate) { const total = price1 + price2 + price3; const finalTotal = total * (1 + taxRate); return finalTotal; }
シンプルなコードでは、個別に計算するのではなく、まず合計を取得してから一度だけ税率を適用するようにしています。それにより冗長な部分を削除でき、コードが見やすくなりました。
-
明確で直感的な命名:
// 悪い例 function cal(p, t) { return p * t; } // 良い例 function calculateArea(width, height) { return width * height; }
変数、引数、関数名などをわかりやすくすることで、コードの可読性が向上します。
-
単一責任原則を遵守:
// 単一責任を守らない例 function calculateAndPrintArea(width, height) { const area = width * height; console.log(area); } // 単一責任を守る例 function calculateArea(width, height) { return width * height; } function printArea(content) { console.log(`面積は:${content}m2`); } const printContentNum = calculateArea(10 * 3); printArea(printContentNum)
単一責任を守らない例では一つの関数の中に、面積の計算と出力の両方の責任を担っていました。
単一責任を守る例では各関数が一つの責任に集中することで、コードがよりモジュール化されます。
2. DRY原則 - Don’t Repeat Yourself
DRY原則は 「同じコードを繰り返さないこと」 を推奨しています。
同じコードやロジックを複数箇所に書くと、保守が難しくなり、変更時にバグが発生しやすくなります。
コードの重複でもっとも多いのはひとまとまりのロジックを、安易に別の部分にコピペして使用してしまうことです。
これにより同じロジックが複数箇所にばらまかれてしまうので、コピペは基本的にはNGと考えた方が良さそうです。
そのために共通処理を記述したファイルなどを作成して使い回すことで同じコードやロジックを複数記述されることを防ぐことができます。
また、単純なコードの重複ではないですが、ただ単にコードをそのまま説明しているコメントや、コードを母国語に訳しているだけのコメントも重複にあたります。
コメントについては後述のPIEのセクションでも後述します。
具体例:
関数やモジュールを再利用する
-
重複コード例:
// ユーザーの名前をapiから取得し、表示させる処理 async function printUserName() { const url = 'https://example.com/user/profile' const res = await fetch(url, { headers: { "Content-Type": "application/json", Accept: 'application/json', Authentication: 'token', } }); const profileObj = await res.json(); console.log(profileObj.userName); } // ユーザーのアクションをapiから取得し、アクションログを生成する処理 async function createUserActionLog() { const url = 'https://example.com/user/action' const res = await fetch(url, { headers: { "Content-Type": "application/json", Accept: 'application/json', Authentication: 'token', } }); const actionObj = await res.json(); const actionLogData = `${actionObj.date}:${actionObj.action}`; return actionLogData; }
-
DRYに則って重複を共通化した例:
// apiからfetchする関数 async function fetchWithHeaders(url) { const res = await fetch(url, { headers: { "Content-Type": "application/json", Accept: 'application/json', Authorization : 'token', } }); const result = await res.json(); return result; } async function getUserName() { const profileObj = await fetchWithHeaders('https://example.com/user/'); console.log(profileObj.userName); } async function createUserActionLog() { const actionObj = await fetchWithHeaders('https://example.com/user/action'); const logData = `${actionObj.date}:${actionObj.action}`; return logData; }
apiへ送るリクエストヘッダ等、fetchの処理を共通化することで同じコードを複数回書かずに済み、コードの冗長性が排除できました。
3. YAGNI原則 - You Ain’t Gonna Need It
YAGNI原則は 「将来必要になるかもしれないという理由で、現在不要な機能を実装しない」 という考え方です。
エンハンス業務をしていると、機能拡張が必要になることがよくあり、予想される機能はあらかじめ用意しておきたい気持ちになることがよくあります。
しかし、コードの予測は外れます。外れるということはそのコードを書いた時間が無駄になり、使われていない不要なコードが生成されるということです。
時間が経つとなぜこのような使われていないコードがあるのか、何か意味があるのか、理由がわからなくなり、難解で保守しにくいコードができあがってしまいます。
具体例:
-
現在必要な機能に集中:
// 過剰な機能例 // 要件には割引、クーポン使用は含まれていない function calculateTotal(price, taxRate, discount = 0, coupon = '') { let total = price + (price * taxRate); total -= discount; if (coupon) { total -= applyCoupon(coupon, total); } return total; } // 必要な機能に集中した例 function calculateTotal(price, taxRate) { return price + (price * taxRate); }
現時点で必要な機能に絞ることで、コードがシンプルになります。
4. PIE原則 - Program Intently and Expressively
PIE原則は 「意図を表現してプログラミングする」 ことを推奨します。
これにより、コードが目的に沿って明確で、他の開発者が容易に理解できるようになります。
コードだけがソフトウェアの動作を「正確」に「完全に」知ることができる手がかりです。
業務上、要件定義書や基本設計書など様々なドキュメントも作成されますが、それらでは”ソフトウェアがどう動くのか”を正確に知ることはできません。
ソフトウェアの動作を把握するにはコードを読むしかありません。
そのため、読み手にわかりやすい表現でコードを書き、コードで意図を伝えることが大切です。
具体例:
-
表現力豊かなコード:
// 表現力に欠けるコード let a = 10; let b = 20; if (a > b) { console.log('aの方が大きい'); } // 表現力豊かなコード let currentScore = 10; let highScore = 20; if (currentScore > highScore) { console.log('ハイスコア更新!'); }
コードが意図する内容を明確に伝える表現を使うことで、コードの理解が容易になります。
KISSの法則にもつながりますが、変数や関数をわかりやすく命名することが大切になってきます。
5. SLAP原則 - Single Level of Abstraction Principle
SLAP原則は「一つの関数やモジュールが、同一レベルの抽象化を持つべきである」という考え方です。これにより、コードの可読性が向上し、保守が容易になります。
具体例:
-
SLAPを守らない例:
function processOrder(order) { // 高レベルの処理 validateOrder(order); // 低レベルの処理 for (let item of order.items) { if (item.stock < 1) { throw new Error('Out of stock'); } item.stock--; } // 高レベルの処理 saveOrder(order); }
-
SLAPを守る例:
function processOrder(order) { // 同一レベルの処理 validateOrder(order); updateStock(order); saveOrder(order); }
各関数が同一レベルの抽象化を持つようにすることで、コードが読みやすくなります。
6. OCP - Open/Closed Principle
OCP(オープン/クローズド原則)は「クラスやモジュールは拡張に対してオープンであり、修正に対してクローズドであるべき」という考え方です。これにより、新しい機能を追加する際に既存のコードを変更せず対応できるようになります。
具体例:
- OCPに従わない例:
// 四角形の面積を計算する
function calculateRectangleArea(width, height) {
return width * height;
}
// 三角形の面積を計算する
function calculateTriangleArea(base, height) {
return (base * height) / 2;
}
// 面積の計算をする
function calculateArea(shape, dimensions) {
switch (shape) {
case 'rectangle':
return dimensions.width * dimensions.height;
case 'triangle':
return (dimensions.base * dimensions.height) / 2;
default:
throw new Error('形がサポートされていません');
}
}
新しい形状を追加する際、calculateArea
関数のswitch文の中身も書き換える必要があります。
- OCPに従う例:
class Shape {
area() {
throw new Error('Method not implemented');
}
}
class Rectangle extends Shape {
constructor(width, height) {
super();
this.width = width;
this.height = height;
}
area() {
return this.width * this.height;
}
}
class Triangle extends Shape {
constructor(width, height) {
super();
this.width = width;
this.height = height;
}
area() {
return (this.width * this.height) / 2;
}
}
class AreaCalculator {
calculate(shape) {
return shape.area();
}
}
新しい形状を追加する際は、Shapeクラスを拡張するだけで、AreaCalculator
のコードを変更する必要がなくなります。
おわりに
正直なところ、私のように英語が得意でない場合、これらの原則を頭文字で覚えるのは難しいと感じるかもしれません。
しかし、これらの法則が存在するのは、それだけ重要な考え方だからです。
心に刻んでおくことで、今後コードを書く上で迷いが少なくなり、より良い設計ができるようになるのではないでしょうか。
この記事の執筆を通じて、私自身もこれらの原則を再確認し、今後の開発に生かしていきたいと思います。
参考文献
プリンシプル オブ プログラミング3年目までに身につけたい一生役立つ101の原理原則 上田 勲 (著)
リーダブルコード ―より良いコードを書くためのシンプルで実践的なテクニック (Theory in practice)Dustin Boswell (著), Trevor Foucher (著), 須藤 功平 (解説), 角 征典 (翻訳)