はじめに
こんにちは。新卒1年目エンジニアの清水と申します。
私は昨年の4~9月にかけてエンジニア研修を受けていたのですが、個人的に研修の目標としていたことがあります。それは良いコードを書けるようになるということです。
そのため、優れたコードを書くための知識を色々と勉強していたのですが、中でも「プリンシプル オブ プログラミング」という本で紹介されている7つのプログラミング原則が良いコードを書くのにおいてどういった観点が必要なのかを端的に表していて非常に勉強になったので、共有したいと思います。
7つのプログラミング原則
・KISS (日本語: シンプルにしておけ、愚か者よ 英語: Keep It Simple, Stupid)
コードを書くときの最優先の価値を単純性、簡潔性に置く原則です。
コードが複雑にならないように意識して、コードをシンプルに保ち続けます。
コードは自然に任せておくと、どんどん複雑になっていきます。
複雑なコードは読みにくい上に修正しにくく、無理やり修正を繰り返していくと変更不能な負債コードへと辿り着きます。
コードは必ず変更されるものであり、修正容易性を確保するためにシンプルであることはとても重要です。
具体的な対策法: とにかく余計なコードを書かない。問題解決のために必要な、最も簡潔かつ短いコードだけを書く。
参考例
// Bad Pattern
for(let i = 0; i < 4; i++) {
// 直接変数iを出力すれば良いのに、無意味な条件分岐をしている
switch(i) {
case 0:
console.log(0);
break;
case 1:
console.log(1);
break;
case 2:
console.log(2);
break;
case 3:
console.log(3);
break;
case 4: // 使う予定のないケース
console.log(4);
break;
default:
console.log("error");
}
}
// Good Pattern
// 行数を大幅に削減できた
for(let i = 0; i < 4; i++) {
console.log(i);
}
・DRY (日本語: 繰り返すな 英語: Don't Repeat Yourself)
コードの重複を許さない原則です。コードのコピー&ペーストや同じ条件を扱う制御文の重複、あるいは同じものを示すマジックナンバーが複数回コードに表れるといった無駄な繰り返しを避ける原則です。
コードに重複があると、障害修正や機能追加などといったコードの改善が難しくなります。
具体的には以下のような問題が発生すると考えられます。
-
コードを読む作業が難しくなる
- 同じようなコードが複数あると、読むべきコード量が多くなり、コードのロジックが複雑になります。そのためコードの内容を把握することが難しくなります。
-
コードを修正する作業が難しくなる
- 同じようなコードが複数あると、コードの変更時に同一のロジックを持つ複数の箇所を同時に変更することが必要になります。
複数箇所の変更が必要な状況では、意図せぬ修正漏れによってバグを埋め込む可能性が非常に高いです。
- 同じようなコードが複数あると、コードの変更時に同一のロジックを持つ複数の箇所を同時に変更することが必要になります。
具体的な対策法: コードのロジックを「関数化」「モジュール化」、またデータであれば「定数化」することによって、重複を排除する。
参考例
// Bad Pattern
let eggPrice = 100;
let eggPriceWithTax = eggPrice * 1.10; // 税率がマジックナンバーとなっていることでコードの重複が生まれている
let milkPrice = 200;
let milkPriceWithTax = milkPrice * 1.10; // 税込価格を計算するロジックが複数箇所に繰り返し現れているとも言える
// Good Pattern
const TAX = 1.10; // データを定数として宣言する
let eggPrice = 100;
let eggPriceWithTax = calcPriceWithTax(eggPrice);
let milkPrice = 200;
let milkPriceWithTax = calcPriceWithTax(milkPrice);
function calcPriceWithTax(price) { // 税込計算を関数化することで、計算ロジックの変更が容易になった
return price * TAX;
}
・YAGNI (日本語: それはきっと必要にならない 英語: You Aren't Going to Need It)
コードを書く際には必要最低限の内容だけを書くようにしましょう。
この原則は必要になりそうなコードをあらかじめ書いておくのではなく、本当に必要になった時、必要な分だけ書くという原則です。
「拡張性」や「汎用性」を考えて、将来使うであろうコードを盛り込んでも、結局は利用されないことがほとんどです。
今後必要となる機能を予想してコードを書いても、その予想は往々にして外れます。多くの場合は時間を浪費するだけです。
さらに拡張性や汎用性を考慮に入れると、それゆえにコードへ余計な複雑性を盛り込むことになります。
不要な拡張性や汎用性によって、無意味にロジックの難解なコードが出来上がります。
具体的な対策法:
汎用性よりも単純性を考えましょう。再利用性や拡張性よりも、実際のニーズを基礎とするシンプルさを重視します。
それが結果として、修正容易性から生まれる汎用性につながることも多いです。
参考例
// Bad Pattern
const order = {
userId: 1,
choice: "Cola",
// 後々使うかもしれないから、一応付けとく
userName: "user1",
mailAddress: "hogehoge@test.com",
telNumber: "xxx-xxxx-xxx"
}
// Good Pattern
const order = {
userId: 1,
choice: "Cola",
// 今、必要ない分は書かない
}
・PIE (日本語: 意図を表現してプログラミングせよ 英語: Program Intently and Expressively)
コードを書く時に大切なことは、コードの意図が明確に伝わるよう書くことです。
コードはコンパイラではなく人が読みます。コードの表現を工夫して、読み手へ正確に伝わるよう書きましょう。
結局のところ、コードだけがソフトウェアの動作を正確に知るためのドキュメントです。ソフトウェアの挙動が知りたければコードを読むしかありません。
わかりやすいコードは、コードを読む効率を上げ、開発効率の向上に貢献します。
具体的な対策法:
コードを書く際は「読みやすさ」を最優先にしましょう。コードは書きやすさよりも読みやすさを重視します。
また賢さをアピールしようと、無意味に難しい書き方をしてはいけません。
参考例
// Bad Pattern
let something = prompt(); // よくわからない関数の戻り値を、意図の伝わらない変数に代入
let price = something === "Cola" ? 150 : 100; // 一般的に使われていない、三項演算子を使用して賢さアピール
// Good Pattern
let drinkOrder = prompt("ご注文内容(ColaもしくはTea)を入力してください"); // コードを読めば意図がわかるように情報を詰め込む
// 多くのプログラマが理解できるif else文で代入される値を制御
let price = 0;
if (drinkOrder === "Cola") {
price = 150;
} else {
price = 100;
}
・SLAP (日本語: 抽象化レベルの統一 英語: Single Level of Abstraction Principle)
コードを書く際は、コードの抽象レベルによって関数を階層化して記述しましょう。
抽象レベルとは、その関数が抱えている機能の複雑さを表すものであり、より複雑な機能を持つ関数ほど高水準の抽象レベルを持つと言えます。
多くの機能を持つ高い抽象レベルの関数を、より低い抽象レベルの関数に分割していき、
同じ関数内に属するコードの抽象レベルを統一することで、階層化された直感的に理解しやすいコードを書くことができます。
抽象レベルの統一されたコードは高水準から中水準の関数が書籍の目次のように働き、低水準の処理が書籍の本文として機能します。
つまり、正しく抽象レベルの階層が作られていれば、適切な目次を持つ本のように容易くコード構造の把握ができます。
参考資料
// 高水準の抽象レベルを持つ関数内に中水準の関数があり、さらにその中に低水準の関数が内包される形になっている
function 高水準() { // 高水準の抽象レベルの関数
中水準1();
中水準2();
}
function 中水準1() { // 中水準の抽象レベルの関数
低水準1();
低水準2();
}
function 中水準2() {
低水準3();
}
function 低水準1() { // 低水準の抽象レベルの関数
//処理
}
function 低水準2() {
//処理
}
function 低水準3() {
//処理
}
具体的な対策法:
関数を階層化しましょう。関数内の処理を意図の伝わりやすい、より低い抽象レベルの小さなステップ群の関数に分割します。
適切に関数が階層化されていれば、処理が1行であっても構いません。
参考例
// Bad Pattern
// コンソールに複数文を表示する高水準の抽象レベルを持つ関数。ただし関数内の抽象レベルがバラバラで理解しにくい
function printParagraph() {
printFirstSentence();
printSecondSentence();
// 上の2行よりも低い抽象レベルのコード。関数の構造を知りたいだけの場合でも、詳細な処理を読む必要がある。
let thirdSentence = "I'm just a huge fan of yours.";
console.log(thirdSentence);
}
function printFirstSentence() {
let firstSentence = "Hi," + " " + "my name is Tom.";
console.log(firstSentence);
}
function printSecondSentence() {
let secondSentence = "Nice to meet you.";
console.log(secondSentence);
}
// Good Pattern
// 抽象レベルが揃えられていることで、3文を出力する関数であるとすぐ理解できる
function printParagraph() {
printFirstSentence();
printSecondSentence();
printThirdSentence();
}
function printFirstSentence() {
let firstSentence = "Hi," + " " + "my name is Tom.";
console.log(firstSentence);
}
function printSecondSentence() {
let secondSentence = "Nice to meet you.";
console.log(secondSentence);
}
function printThirdSentence() {
let thirdSentence = "I'm just a huge fan of yours.";
console.log(thirdSentence);
}
・OCP(日本語: 開放-閉鎖原則 英語: Open-Closed Principle)
コードは高い拡張可能性と、高い堅牢性という2つの属性を同時に満たすように設計します。
高い拡張性とは「コードに新しい機能をリスクなく追加できる」ことを意味し、
高い堅牢性とは「新しい機能を追加しても、その他のコードはまったく影響を受けない」ことを意味します。
これら2つの属性を満たしたソフトウェアは、既存コードに影響を与えずに機能を追加することができるようになります。
こうしたソフトウェアは時間とともに変わっていく要求や仕様に対して柔軟に対応することができ、長期的な安定が見込めます。
具体的な対策法:
クライアントからある機能をもったモジュールを利用する時は、モジュールを直接呼び出すのではなく、インタフェースを介して呼び出すようにしましょう。
仕事で言えばメールの送信先を部署の個人(モジュール)ではなく、部署そのもの(インターフェース)にして連絡するようなイメージです。
部署の個人に連絡していた場合、担当者が変わった場合には自分(クライアント)もメールの送信先を変更して対応しなくてはなりませんが、
部署そのものと連絡を取っていた場合は担当者が変わってもメールの送信先は変わりません。
参考例
// Bad Pattern
// テスト用APIクラス
class TestAPI {
fetchData() {
console.log("This is Test API");
}
}
// 本番用APIクラス
class ProdAPI {
fetchData() {
console.log("This is Prod API");
}
}
class ViewModel {
private readonly api: TestAPI; // ProdAPIを使用する際は、api変数の型を変更する必要がある
constructor(api: TestAPI) {
this.api = api;
}
fetchData() {
this.api.fetchData();
}
}
const viewModel = new ViewModel(new ProdAPI()); // 使用するAPIクラスをProdAPIに変更。
// Good Pattern
// APIクラスのインターフェースを追加
interface APIProtocol {
fetchData();
}
// テスト用APIクラス
class TestAPI implements APIProtocol {
fetchData() {
console.log("This is Test API");
}
}
// 本番用APIクラス
class ProdAPI implements APIProtocol {
fetchData() {
console.log("This is Prod API");
}
}
class ViewModel {
private readonly api: APIProtocol; // インタフェースを指定することで、ProdAPIを使用する際もapi変数の型を変更せずに利用可能
constructor(api: APIProtocol) {
this.api = api;
}
fetchData() {
this.api.fetchData();
}
}
const viewModel = new ViewModel(new ProdAPI());
・名前重要(英語: Naming is Important)
プログラミングでは、命名を最重要課題として認識し、慎重に取り組む必要があります。
命名における名前を付ける行為と名前そのもの、その両方にとても重要な価値があります。
名前を付ける行為は、適切な名前を考える過程を通してその要素を正しく理解し、より名前に適した理想的な形へと設計し直す機会になります。
名前そのものは、コードを通じてコミュニケーションするための重要な要素となります。
プログラマ同士では大抵の場合、コードを通じてコミュニケーションすることになるため、名前が適切でないと意思疎通ができません。
具体的な対策法:
コードを書く際は、コードに出てくる各要素に対して意図が正確に伝わるような名前を付けるようにしましょう。
また命名は常に、「使う側」「読む側」の視点に立って命名してください。
以下のようなことを守ると、より伝わりやすい名前になります。
-
名前は短いコメントと考え、多くの情報を詰め込む
-
他の意味に誤解されない名前を選ぶ
-
名前は「効果」と「目的」に言及し、「手段」には言及しない
-
テストコードを書いてみることで、コードを「使用する側」から命名を考える
-
名前は発音可能なものにする
-
名前は検索可能なものにする(短すぎない、固有名)
参考例
// Bad Pattern
// 何を格納している変数か分からない命名
let ms = 10;
let bs = 20;
let p1 = 100;
let p2 = 200;
// Good Pattern
// 意図の明確な変数の命名
let melonStock = 10;
let bananaStock = 20;
let player1HitPoint = 100;
let player2HitPoint = 200;
まとめ
本記事では、「プリンシプル オブ プログラミング」から7つのプログラミング原則をご紹介しました。
プログラミングのルールとしては基本的な内容であるものの、SLAPやOCPといった概念はなかなかに理解が難しく大変でした。(説明するのも難しかったです)
ご紹介した内容が皆さまの新たな学びであったり、自らの知識の見直しの機会となりますと幸いです。
ここまで読んでいただきありがとうございました。
参考
- 書籍 - プリンシプル オブ プログラミング,上田勲(著)
- KISS原則~プログラムは、シンプルでさえあればいい
- 汝、SLAPを愛せよ。
- 【SOLID原則】新人エンジニアが教える「オープン・クローズドの原則(OCP)」