Clean Code PHP / Clean Code JavaScript
以下はClean Code JavaScriptの日本語訳です。
clean-code-javascript
Introduction
Robert C. Martinの著書Clean Codeは、JavaScriptにも当てはまることばかりです。
これはスタイルガイドではありません。
JavaScriptで3R(Readable、Reusable、Refactorable)なコードを推進するためのガイドです。
ここに書いてあることの全てに従わねばならないわけではなく、普遍的に合意されているわけでもありません。
ただのガイドラインであり、それ以上のものではありません。
しかしこれらは、Clean Codeの著者らが長年の集合知の結果をまとめたものです。
ソフトウェアエンジニアリングの歴史は僅か50年程度のものでしかなく、我々はまだ多くのことを学ぶ必要があります。
今のところ、これらのガイドラインは、貴方と貴方のチームが作り出すJavaScriptコードの品質を評価するための試金石として役立つでしょう。
それともうひとつ、これらのことを知ったからといって、貴方がすぐにもっと優れた開発者になれるわけではありません。
長年JavaScriptに接し続けていても、間違いを犯さないわけではありません。
全てのコードは、粘土が徐々に形作られていくように、曖昧な形から始まるものです。
その後でコードレビューを行い、不要なコードを削除していきます。
最初のコードがひどくても諦めないでください。
かわりにもっとコードを打つんだ。
Variables
Use meaningful and pronounceable variable names
変数名には、意味のある発音可能な名前を使用します。
const yyyymmdstr = moment().format('YYYY/MM/DD');
const currentDate = moment().format('YYYY/MM/DD');
Use the same vocabulary for the same type of variable
同じような種類のコードには同じ名前を使用します。
getUserInfo();
getClientData();
getCustomerRecord();
getUser();
Use searchable names
検索しやすい名前にします。
コードは読みやすく、そして検索しやすくすることが重要です。
意味のある名前を使用しないことによって、コードを解読する側は不必要な努力を強いられます。
マジックナンバーには名前を付けるべきです。
buddy.jsやESLintなどでマジックナンバーを検出することが可能です。
// 86400000って何?
setTimeout(blastOff, 86400000);
// 意味を定数宣言する
const MILLISECONDS_IN_A_DAY = 86400000;
setTimeout(blastOff, MILLISECONDS_IN_A_DAY);
Use explanatory variables
説明用に中間変数を使用します。
const address = 'One Infinite Loop, Cupertino 95014';
const cityZipCodeRegex = /^[^,\\]+[,\\\s]+(.+?)\s*(\d{5})?$/;
saveCityZipCode(address.match(cityZipCodeRegex)[1], address.match(cityZipCodeRegex)[2]);
const address = 'One Infinite Loop, Cupertino 95014';
const cityZipCodeRegex = /^[^,\\]+[,\\\s]+(.+?)\s*(\d{5})?$/;
const [, city, zipCode] = address.match(cityZipCodeRegex) || [];
saveCityZipCode(city, zipCode);
Avoid Mental Mapping
仮名より、ちゃんとした名前を付けた方がいい。
const locations = ['Austin', 'New York', 'San Francisco'];
locations.forEach((l) => {
doStuff();
doSomeOtherStuff();
// ...
// ...
// ...
// あれ、lって何だったっけ?
dispatch(l);
});
const locations = ['Austin', 'New York', 'San Francisco'];
locations.forEach((location) => {
doStuff();
doSomeOtherStuff();
// ...
// ...
// ...
dispatch(location);
});
Don't add unneeded context
クラス名やオブジェクト名に意味が入っているのであれば、変数名で同じ言葉を繰り返す必要はありません。
const Car = {
carMake: 'Honda',
carModel: 'Accord',
carColor: 'Blue'
};
function paintCar(car) {
car.carColor = 'Red';
}
const Car = {
make: 'Honda',
model: 'Accord',
color: 'Blue'
};
function paintCar(car) {
car.color = 'Red';
}
Use default arguments instead of short circuiting or conditionals
短絡や条件式を使わず、デフォルト引数を使用しましょう。
デフォルト引数は、大抵短絡よりも綺麗に書くことができます。
ただし、デフォルト引数は値が無い場合にだけ使用されることに注意しましょう。
''、""、false、null、0、NaNなどのfalseっぽい値だったとしても引数が使用され、デフォルト引数は使用されません。
function createMicrobrewery(name) {
const breweryName = name || 'Hipster Brew Co.';
// ...
}
function createMicrobrewery(breweryName = 'Hipster Brew Co.') {
// ...
}
Functions
Function arguments (2 or fewer ideally)
関数の引数を制限することは、関数のテストが容易になるため重要です。
引数が3個以上になると各変数を様々なケースでテストすることが必要になり、組合せ爆発に直結します。
引数は1個か2個が理想的であり、3個以上は可能な限り避ける必要があります。
それ以上が必要になるのであれば引数をまとめましょう。
引数が2個より多いならば、その関数の行う仕事は多すぎます。
そうではないというのであれば、引数としてオブジェクトを渡すようにしましょう。
JavaScriptは"class"の定型文を使わずにオブジェクトをすぐに作成できるため、多くの引数が必要な場合はオブジェクトをかわりに使用できます。
関数では必要なプロパティを明示するため、ES2015/ES6の分割代入構文を使用できます。
これには幾つかの利点があります。
・関数定義を見るだけで、どのプロパティが必要か一目でわかる。
・分割代入は引数のプリミティブ値をコピーするため、副作用を防ぐことができる。 ※オブジェクトと配列はコピーされないので注意
・Linterは未使用のプロパティに警告を出すことができる。
function createMenu(title, body, buttonText, cancellable) {
// ...
}
function createMenu({ title, body, buttonText, cancellable }) {
// ...
}
createMenu({
title: 'Foo',
body: 'Bar',
buttonText: 'Baz',
cancellable: true
});
Functions should do one thing
ひとつの関数はひとつの処理だけを行うこと。
これはソフトウェア工学の最も重要なルールです。
関数に複数の処理を書いてしまうと、関数の作成、テスト、そして処理内容の推測が難しくなります。
1関数は1アクションだけに定義してしまえば、リファクタリングも簡単になり、コードはより洗練されたものになります。
このガイドの他の部分は全く守らなかったとしても、ここにだけ気をつけていればよりよい開発者になれることでしょう。
function emailClients(clients) {
clients.forEach((client) => {
const clientRecord = database.lookup(client);
if (clientRecord.isActive()) {
email(client);
}
});
}
function emailActiveClients(clients) {
clients
.filter(isActiveClient)
.forEach(email);
}
function isActiveClient(client) {
const clientRecord = database.lookup(client);
return clientRecord.isActive();
}
Function names should say what they do
関数名は処理内容を適切に表しましょう。
function addToDate(date, month) {
// ...
}
const date = new Date();
// 1が何なのかわからない
addToDate(date, 1);
function addMonthToDate(month, date) {
// ...
}
const date = new Date();
addMonthToDate(1, date);
Functions should only be one level of abstraction
1関数内にデータの抽象化が複数ある場合、その関数は仕事をしすぎています。
機能を分割することで、再利用性やテストのしやすさが高まります。
function parseBetterJSAlternative(code) {
const REGEXES = [
// ...
];
const statements = code.split(' ');
const tokens = [];
REGEXES.forEach((REGEX) => {
statements.forEach((statement) => {
// ...
});
});
const ast = [];
tokens.forEach((token) => {
// lex...
});
ast.forEach((node) => {
// parse...
});
}
function tokenize(code) {
const REGEXES = [
// ...
];
const statements = code.split(' ');
const tokens = [];
REGEXES.forEach((REGEX) => {
statements.forEach((statement) => {
tokens.push( /* ... */ );
});
});
return tokens;
}
function lexer(tokens) {
const ast = [];
tokens.forEach((token) => {
ast.push( /* ... */ );
});
return ast;
}
function parseBetterJSAlternative(code) {
const tokens = tokenize(code);
const ast = lexer(tokens);
ast.forEach((node) => {
// parse...
});
}
Remove duplicate code
コードの重複は避けなければなりません。
コードの重複は、何らかのロジックを変更したいときに、複数箇所を修正しなければならなくなるため、よくありません。
貴方はレストラン経営者で、トマト、タマネギ、ニンニク、スパイスなどの在庫を管理しているとします。
もし在庫管理台帳が複数あったとしたら、トマト料理をひとつ提供するたびに全ての台帳を書き換えなければなりません。
台帳がひとつだけであれば、書き換えが必要なのは一カ所だけです。
ほとんど同じ内容でありながらごく一部だけ違う処理を行うため、ほぼ同じコードを書かなければならないことはよくあります。
重複コードの削除とは、ひとつの関数/モジュール/クラスだけで該当の処理を行うようにコードを抽出することです。
正しく抽出することは重要で、そのためにはクラスのセクションで解説しているSOLIDの原則に従う必要があります。
下手に抽出すると、元のコードよりも悪くなる可能性があるので注意しましょう。
とはいえ、良い抽出ができるのであればしない理由はありません。
同じ処理を繰り返さないでください。
さもなければ、ひとつの内容を変更したいだけなのに複数箇所の修正が必要となってしまうことでしょう。
function showDeveloperList(developers) {
developers.forEach((developer) => {
const expectedSalary = developer.calculateExpectedSalary();
const experience = developer.getExperience();
const githubLink = developer.getGithubLink();
const data = {
expectedSalary,
experience,
githubLink
};
render(data);
});
}
function showManagerList(managers) {
managers.forEach((manager) => {
const expectedSalary = manager.calculateExpectedSalary();
const experience = manager.getExperience();
const portfolio = manager.getMBAProjects();
const data = {
expectedSalary,
experience,
portfolio
};
render(data);
});
}
function showEmployeeList(employees) {
employees.forEach((employee) => {
const expectedSalary = employee.calculateExpectedSalary();
const experience = employee.getExperience();
const data = {
expectedSalary,
experience
};
switch (employee.type) {
case 'manager':
data.portfolio = employee.getMBAProjects();
break;
case 'developer':
data.githubLink = employee.getGithubLink();
break;
}
render(data);
});
}
Set default objects with Object.assign
デフォルト値はObject.assignで設定しよう。
const menuConfig = {
title: null,
body: 'Bar',
buttonText: null,
cancellable: true
};
function createMenu(config) {
config.title = config.title || 'Foo';
config.body = config.body || 'Bar';
config.buttonText = config.buttonText || 'Baz';
config.cancellable = config.cancellable !== undefined ? config.cancellable : true;
}
createMenu(menuConfig);
const menuConfig = {
title: 'Order',
// Bodyを指定し忘れてた
buttonText: 'Send',
cancellable: true
};
function createMenu(config) {
config = Object.assign({
title: 'Foo',
body: 'Bar',
buttonText: 'Baz',
cancellable: true
}, config);
// 他の値は引数そのままだが、Bodyだけ'Bar'になる
// ...
}
createMenu(menuConfig);
Don't use flags as function parameters
引数にフラグ値を使わない。
フラグを使うということは、すなわちその関数が複数の処理を行うことを示しています。
関数はひとつのことを行うべきなので、フラグではなく別々の関数にするべきです。
function createFile(name, temp) {
if (temp) {
fs.create(`./temp/${name}`);
} else {
fs.create(name);
}
}
function createFile(name) {
fs.create(name);
}
function createTempFile(name) {
createFile(`./temp/${name}`);
}
Avoid Side Effects (part 1)
関数が、値を受け取って加工して返す以外のことを行う場合、その関数には副作用があります。
副作用はたとえばファイルへの書き込み、グローバル変数の変更、あるいは全財産を誰かにばらまくこと等です。
どうしても副作用のある関数を書かなければならないこともあるでしょう。
ファイルに何かを書き込む必要があるかもしれません。
そういうときに行わなければならないことは、副作用を起こす処理はひとまとめにすることです。
あるファイルに書き込む処理を、複数の関数やクラスに分散させてはいけません。
それを行うサービスをひとつだけ作り、そしてそのサービスはその処理だけを行うようにしてください。
オブジェクトを使い回してデータ構造も何もなくなってしまう、何処でも書き換えられるミュータブルなデータを使う、副作用のある処理を何カ所にも書く、といったよくある落とし穴はできる限り避けましょう。
それだけで他の多くのプログラマーより、きっとより幸せになれます。
// グローバル変数を使ってるうえに直接型変換してるので、そのうちきっと事故る
let name = 'Ryan McDermott';
function splitIntoFirstAndLastName() {
name = name.split(' ');
}
splitIntoFirstAndLastName();
console.log(name); // ['Ryan', 'McDermott'];
function splitIntoFirstAndLastName(name) {
return name.split(' ');
}
const name = 'Ryan McDermott';
const newName = splitIntoFirstAndLastName(name);
console.log(name); // 'Ryan McDermott';
console.log(newName); // ['Ryan', 'McDermott'];
Avoid Side Effects (part 2)
JavaScriptでは、プリミティブ値は値渡しされ、オブジェクトや配列は参照渡しになります。
たとえばショッピングカートにアイテムを追加するなど、関数内でオブジェクトに変更を加えると、そのショッピングカートはグローバルに変更され、他の関数もその追加の影響を受けます。
それは素晴らしいことかもしれませんが、それと同等以上にとても良くないことです。
ユーザが『購入』ボタンを押したときのことを考えましょう。
これはネットワーク接続が発生し、カート内のアイテムをサーバに送信します。
ネットワークが不調だった場合、購入機能は裏で購入リクエストを再送し続けます。
その間に、ユーザがうっかり不要なアイテムをカートに入れるボタンを誤クリックしてしまったらどうなるでしょう。
その後にネットワーク接続が成功すると、不要なアイテムが追加されたあとのカートが参照されてしまっているため、それも一緒に購入してしまいます。
最もスマートな解決方法は、関数が常に引数を複製してから操作を行い、そしてその複製を返すことです。
これにより、購入ボタンを押した後にカートが変更されても、その変更の影響を受けることはありません。
ただし注意点が2点ほどあります。
・本当に引数のオブジェクト自体を変更したいと思うことがあるかもしれないが、実際そのような事態は全く無い。ほぼ全ての関数は副作用が無いようにリファクタリングすることができる。
・大きなオブジェクトの複製にはパフォーマンス面で多大なコストがかかる可能性がある。ただ、幸いなことにこれは大きな問題ではない。そのような構造でも高速に動作するように作られたライブラリが既に存在している。
const addItemToCart = (cart, item) => {
cart.push({ item, date: Date.now() });
};
const addItemToCart = (cart, item) => {
return [...cart, { item, date: Date.now() }];
};
Don't write to global functions
グローバル汚染はよくない方法です。
他のライブラリと衝突してエラーになる可能性があり、そのようなライブラリを使い続けるほどユーザは親切ではありません。
具体例として、Arrayオブジェクトを拡張して2配列の差分を取得するメソッドを考えてみましょう。
まずArray.prototypeにdiffを生やす方法がありますが、これは他のライブラリと衝突することでしょう。
さらに他のライブラリが、配列の最初と最後の要素を比べるメソッドにdiffと名付けていたとしたら?
このため、ES2015/ES6のclassを使用してArrayを拡張する実装の方が、遙かに優れています。
Array.prototype.diff = function diff(comparisonArray) {
const hash = new Set(comparisonArray);
return this.filter(elem => !hash.has(elem));
};
class SuperArray extends Array {
diff(comparisonArray) {
const hash = new Set(comparisonArray);
return this.filter(elem => !hash.has(elem));
}
}
Favor functional programming over imperative programming
JavaScriptはHaskellのような関数型言語ではありませんが、関数型のような書き方をすることもできます。
関数型言語はより洗練されていて、テストも容易です。
可能であれば、この書き方でプログラミングしていくことをお勧めします。
const programmerOutput = [
{
name: 'Uncle Bobby',
linesOfCode: 500
}, {
name: 'Suzie Q',
linesOfCode: 1500
}, {
name: 'Jimmy Gosling',
linesOfCode: 150
}, {
name: 'Gracie Hopper',
linesOfCode: 1000
}
];
let totalOutput = 0;
for (let i = 0; i < programmerOutput.length; i++) {
totalOutput += programmerOutput[i].linesOfCode;
}
const programmerOutput = [
{
name: 'Uncle Bobby',
linesOfCode: 500
}, {
name: 'Suzie Q',
linesOfCode: 1500
}, {
name: 'Jimmy Gosling',
linesOfCode: 150
}, {
name: 'Gracie Hopper',
linesOfCode: 1000
}
];
const totalOutput = programmerOutput
.map(output => output.linesOfCode)
.reduce((totalLines, lines) => totalLines + lines);
Encapsulate conditionals
条件分岐はカプセル化する。
if (fsm.state === 'fetching' && isEmpty(listNode)) {
// ...
}
function shouldShowSpinner(fsm, listNode) {
return fsm.state === 'fetching' && isEmpty(listNode);
}
if (shouldShowSpinner(fsmInstance, listNodeInstance)) {
// ...
}
Avoid negative conditionals
否定条件は避ける。
function isDOMNodeNotPresent(node) {
// ...
}
if (!isDOMNodeNotPresent(node)) {
// ...
}
function isDOMNodePresent(node) {
// ...
}
if (isDOMNodePresent(node)) {
// ...
}
Avoid conditionals
条件分岐を避ける。
これは不可能なことのように思えます。
この話をすると、『if文無しでいったい何ができるんだ?』とまず最初に聞かれます。
答えは、たいていの場合ポリモーフィズムで同じことができる、です。
次に来る質問は、『へえ~すごいですネ!ところで何故それが必要なんだ?』
この答えはクリーンコードのコンセプト、すなわち、ひとつの関数はひとつのことだけをする、です。
if文のあるクラスや関数は、多くの仕事をしすぎています。
何度も言いますが、ひとつのことだけを行ってください。
class Airplane {
// ...
getCruisingAltitude() {
switch (this.type) {
case '777':
return this.getMaxAltitude() - this.getPassengerCount();
case 'Air Force One':
return this.getMaxAltitude();
case 'Cessna':
return this.getMaxAltitude() - this.getFuelExpenditure();
}
}
}
class Airplane {
// ...
}
class Boeing777 extends Airplane {
// ...
getCruisingAltitude() {
return this.getMaxAltitude() - this.getPassengerCount();
}
}
class AirForceOne extends Airplane {
// ...
getCruisingAltitude() {
return this.getMaxAltitude();
}
}
class Cessna extends Airplane {
// ...
getCruisingAltitude() {
return this.getMaxAltitude() - this.getFuelExpenditure();
}
}
Avoid type-checking (part 1)
JavaScriptには引数の型指定がありません。
つまり、関数は任意の型の引数を取ることができます。
フリーダムすぎるこの仕様に足を取られないように、どうしても引数の型チェックを行いたくなることがあります。
しかし、それを行うことを避ける方法はいくつもあります。
最初に考えるべきことは、一貫したAPIの仕様です。
function travelToTexas(vehicle) {
if (vehicle instanceof Bicycle) {
vehicle.pedal(this.currentLocation, new Location('texas'));
} else if (vehicle instanceof Car) {
vehicle.drive(this.currentLocation, new Location('texas'));
}
}
function travelToTexas(vehicle) {
vehicle.move(this.currentLocation, new Location('texas'));
}
Avoid type-checking (part 2)
引数が文字列や数値のようなプリミティブ型の場合、ポリモーフィズムを使うことはできません。
それでも型チェックを行いたいというのであれば、TypeScriptの使用を検討してください。
TypeScriptは、静的型付けが可能な優れたAltJSの一種です。
普通のJavaScriptで型チェックを行うときの問題点は、それを行ったことで得られる型の安全性が、それを行ったことにより失われる読みやすさを上回るほどの価値が見いだせないことです。
JavaScriptのコードは常にクリーンに保ち、テストを書き、コードレビューを行いましょう。
さもなければTypeScriptを使いましょう。
function combine(val1, val2) {
if (typeof val1 === 'number' && typeof val2 === 'number' ||
typeof val1 === 'string' && typeof val2 === 'string') {
return val1 + val2;
}
throw new Error('Must be of type String or Number');
}
function combine(val1, val2) {
return val1 + val2;
}
Don't over-optimize
最近のブラウザでは、実行時に多くの最適化が行われます。
手動で最適化を行っても、大抵の場合それは無駄になります。
最適化するべき場所をチェックする良いリソースが存在します。
最適化は、これら必要な部分だけを対象にしましょう。
// 古いブラウザでは`list.length`が毎回計算されるため余計な負荷がかかっていた
for (let i = 0, len = list.length; i < len; i++) {
// ...
}
// 最近のブラウザでは自動的に最適化される
for (let i = 0; i < list.length; i++) {
// ...
}
Remove dead code
デッドコードはコードのコピペと同じくらいの悪です。
このようなものを残しておく必要は一切ありません。
呼び出されていないコードは削除する、鉄則です。
もし後で必要になったとしても、バージョン履歴から拾ってこれます。
function oldRequestModule(url) {
// ...
}
function newRequestModule(url) {
// ...
}
const req = newRequestModule;
inventoryTracker('apples', req, 'www.inventory-awesome.io');
function newRequestModule(url) {
// ...
}
const req = newRequestModule;
inventoryTracker('apples', req, 'www.inventory-awesome.io');
Objects and Data Structures
Use getters and setters
オブジェクト内のデータにアクセスするには、プロパティに直接アクセスするよりsetter/getterを介するほうが遙かに優れています。
理由を箇条書きにすると以下のようになります。
・プロパティに何か変更を加えたい場合、コードからプロパティに触っている場所を全て探し出して変更する必要がない。
・set時にバリデーションを簡単に追加できる。
・内部表現をカプセル化できる。
・setter/getterのロギングやエラーハンドリングが簡単にできる。
・オブジェクトの遅延読み込みができる。
function makeBankAccount() {
// ...
return {
balance: 0,
// ...
};
}
const account = makeBankAccount();
account.balance = 100;
function makeBankAccount() {
// privateとする
let balance = 0;
// getterを用いて返す
function getBalance() {
return balance;
}
// setterを用いてセットする
function setBalance(amount) {
// ... バリデーションなど
balance = amount;
}
return {
// ...
getBalance,
setBalance,
};
}
const account = makeBankAccount();
account.setBalance(100);
Make objects have private members
ES5以降ではクロージャを使うことでprivateを実現できます。
const Employee = function(name) {
this.name = name;
};
Employee.prototype.getName = function getName() {
return this.name;
};
const employee = new Employee('John Doe');
console.log(`Employee name: ${employee.getName()}`); // Employee name: John Doe
delete employee.name;
console.log(`Employee name: ${employee.getName()}`); // Employee name: undefined
function makeEmployee(name) {
return {
getName() {
return name;
},
};
}
const employee = makeEmployee('John Doe');
console.log(`Employee name: ${employee.getName()}`); // Employee name: John Doe
delete employee.name;
console.log(`Employee name: ${employee.getName()}`); // Employee name: John Doe
Classes
Prefer ES2015/ES6 classes over ES5 plain functions
古典的なES5の機能だけでリーダブルなクラス・メソッド定義を行うことはほとんど不可能です。
継承が必要なときは、あるいはそうでなくとも、ES2015/ES6の機能を使いましょう。
しかし、オブジェクトは大規模で複雑なため、必要になるまでは関数を用いてもいいかもしれません。
const Animal = function(age) {
if (!(this instanceof Animal)) {
throw new Error('Instantiate Animal with `new`');
}
this.age = age;
};
Animal.prototype.move = function move() {};
const Mammal = function(age, furColor) {
if (!(this instanceof Mammal)) {
throw new Error('Instantiate Mammal with `new`');
}
Animal.call(this, age);
this.furColor = furColor;
};
Mammal.prototype = Object.create(Animal.prototype);
Mammal.prototype.constructor = Mammal;
Mammal.prototype.liveBirth = function liveBirth() {};
const Human = function(age, furColor, languageSpoken) {
if (!(this instanceof Human)) {
throw new Error('Instantiate Human with `new`');
}
Mammal.call(this, age, furColor);
this.languageSpoken = languageSpoken;
};
Human.prototype = Object.create(Mammal.prototype);
Human.prototype.constructor = Human;
Human.prototype.speak = function speak() {};
class Animal {
constructor(age) {
this.age = age;
}
move() { /* ... */ }
}
class Mammal extends Animal {
constructor(age, furColor) {
super(age);
this.furColor = furColor;
}
liveBirth() { /* ... */ }
}
class Human extends Mammal {
constructor(age, furColor, languageSpoken) {
super(age, furColor);
this.languageSpoken = languageSpoken;
}
speak() { /* ... */ }
}
Use method chaining
メソッドチェーンは非常に有用な記法で、jQueryやLoadshなど多くのライブラリでもその実装を見ることができます。
これによって冗長性を減らし、コードをよりクリーンにすることができるでしょう。
メソッドチェーンを実現するには、メソッド内でthisを返すだけです。
class Car {
constructor(make, model, color) {
this.make = make;
this.model = model;
this.color = color;
}
setMake(make) {
this.make = make;
}
setModel(model) {
this.model = model;
}
setColor(color) {
this.color = color;
}
save() {
console.log(this.make, this.model, this.color);
}
}
const car = new Car('Ford','F-150','red');
car.setColor('pink');
car.save();
class Car {
constructor(make, model, color) {
this.make = make;
this.model = model;
this.color = color;
}
setMake(make) {
this.make = make;
return this;
}
setModel(model) {
this.model = model;
return this;
}
setColor(color) {
this.color = color;
return this;
}
save() {
console.log(this.make, this.model, this.color);
return this;
}
}
const car = new Car('Ford','F-150','red')
.setColor('pink')
.save();
Prefer composition over inheritance
Gang of Fourのデザインパターンでも有名ですが、可能なかぎり継承ではなくコンポジションを使うべきです。
継承を使うべき理由はたくさんあり、コンポジションを使うべき理由もたくさんあります。
この格言のポイントは、継承を使おうと思ったときに、コンポジションを使った方がよりよく書けるのではないか考えてみましょう、ということです。
そして、場合によってはそのとおりになります。
ではいつ継承を使うべきなのかというと、それはもちろん対象の問題によってかわりますが、コンポジションより継承を使ったほうがよいケースは以下のようになります。
・関係が"has-a"(ユーザはユーザ詳細を持つ)ではなく"is-a"(人間は動物である)である。
・ベースクラスのコードを再利用できる(人間はほぼ動物と同様に動く)
・ベースクラスを書き換えて子クラスの挙動を一括で変更したい(全動物の消費カロリー量を変更する)
class Employee {
constructor(name, email) {
this.name = name;
this.email = email;
}
// ...
}
// EmployeeTaxData "is-a" Employeeではない
class EmployeeTaxData extends Employee {
constructor(ssn, salary) {
super();
this.ssn = ssn;
this.salary = salary;
}
// ...
}
class EmployeeTaxData {
constructor(ssn, salary) {
this.ssn = ssn;
this.salary = salary;
}
// ...
}
class Employee {
constructor(name, email) {
this.name = name;
this.email = email;
}
setTaxData(ssn, salary) {
this.taxData = new EmployeeTaxData(ssn, salary);
}
// ...
}
SOLID
Single Responsibility Principle (SRP)
Clean Codeには『クラスを変更する理由はひとつだけ』と記されています。
飛行機にスーツケースをひとつしか持ち込めない場合のように、多くの機能を詰め込んだクラスを作りたいと思うかもしれません。
しかし、そうすることでクラスにはまとまりがなくなり、クラスを変更する複数の理由ができてしまいます。
クラスを変更しなければならない理由は最小限に抑えることが重要です。
ひとつのクラスに多くの機能があると、その変更が他のモジュールにどのような影響を及ぼすかを把握することが困難になってしまいます。
class UserSettings {
constructor(user) {
this.user = user;
}
changeSettings(settings) {
if (this.verifyCredentials()) {
// ...
}
}
verifyCredentials() {
// ...
}
}
class UserAuth {
constructor(user) {
this.user = user;
}
verifyCredentials() {
// ...
}
}
class UserSettings {
constructor(user) {
this.user = user;
this.auth = new UserAuth(user);
}
changeSettings(settings) {
if (this.auth.verifyCredentials()) {
// ...
}
}
}
Open/Closed Principle (OCP)
Bertrand Meyerは、『ソフトウェア(クラス、モジュール、関数など)は拡張のために開かれていなければならないが、変更には閉じていなければならい』と言っています。
これはどういう意味でしょうか。
ここでは、既存のコードを変更せずに新たな機能を追加できるようにすべきということを表します。
class AjaxAdapter extends Adapter {
constructor() {
super();
this.name = 'ajaxAdapter';
}
}
class NodeAdapter extends Adapter {
constructor() {
super();
this.name = 'nodeAdapter';
}
}
class HttpRequester {
constructor(adapter) {
this.adapter = adapter;
}
fetch(url) {
if (this.adapter.name === 'ajaxAdapter') {
return makeAjaxCall(url).then((response) => {
// transform response and return
});
} else if (this.adapter.name === 'httpNodeAdapter') {
return makeHttpCall(url).then((response) => {
// transform response and return
});
}
}
}
function makeAjaxCall(url) {
// request and return promise
}
function makeHttpCall(url) {
// request and return promise
}
class AjaxAdapter extends Adapter {
constructor() {
super();
this.name = 'ajaxAdapter';
}
request(url) {
// request and return promise
}
}
class NodeAdapter extends Adapter {
constructor() {
super();
this.name = 'nodeAdapter';
}
request(url) {
// request and return promise
}
}
class HttpRequester {
constructor(adapter) {
this.adapter = adapter;
}
fetch(url) {
return this.adapter.request(url).then((response) => {
// transform response and return
});
}
}
Liskov Substitution Principle (LSP)
リスコフの置換原則は非常にシンプルながら深い意味のある概念です。
『SがTのサブタイプである場合、TはSで置き換えることができる』
すなわち、クラスTのオブジェクトはそのサブクラスSのオブジェクトに入れ替えても動かなければなりません。
これは恐るべき定義です。
この簡単な説明としては、親クラスと子クラスがあった場合に、うっかり間違って使うべきじゃなかった方のクラスを使ったとしても、特に問題なく動作しなければならない、ということです。
実例として、よくあるSquare-Rectangleの例を見てみましょう。
数学的にはSquareはRectangleですが、"is-a"を継承用いて表現している場合、それが問題になります。
class Rectangle {
constructor() {
this.width = 0;
this.height = 0;
}
setColor(color) {
// ...
}
render(area) {
// ...
}
setWidth(width) {
this.width = width;
}
setHeight(height) {
this.height = height;
}
getArea() {
return this.width * this.height;
}
}
class Square extends Rectangle {
setWidth(width) {
this.width = width;
this.height = width;
}
setHeight(height) {
this.width = height;
this.height = height;
}
}
function renderLargeRectangles(rectangles) {
rectangles.forEach((rectangle) => {
rectangle.setWidth(4);
rectangle.setHeight(5);
const area = rectangle.getArea(); // 20を返してほしいのに25が返ってくる。NG
rectangle.render(area);
});
}
const rectangles = [new Rectangle(), new Rectangle(), new Square()];
renderLargeRectangles(rectangles);
class Shape {
setColor(color) {
// ...
}
render(area) {
// ...
}
}
class Rectangle extends Shape {
constructor(width, height) {
super();
this.width = width;
this.height = height;
}
getArea() {
return this.width * this.height;
}
}
class Square extends Shape {
constructor(length) {
super();
this.length = length;
}
getArea() {
return this.length * this.length;
}
}
function renderLargeShapes(shapes) {
shapes.forEach((shape) => {
const area = shape.getArea();
shape.render(area);
});
}
const shapes = [new Rectangle(4, 5), new Rectangle(4, 5), new Square(5)];
renderLargeShapes(shapes);
Interface Segregation Principle (ISP)
JavaScriptにはインターフェイスがないため、インターフェイス分離の原則は他言語と同様に当て嵌められるわけではありません。
しかし、型のないJavaScriptにおいてもこの考え方は重要です。
インターフェイス分離の原則とは、『クライアントが使用しないインターフェイスへの依存を強制してはならない』ということです。
JavaScriptにおいてこの原則が大事なことを示すよい例は、大量の設定を必要とするクラスでしょう。
クライアントに大量の設定を行わせることは有益ではありません。
何故ならば、ほとんどの設定は大抵使用されないからです。
それらをオプショナルにすることで、"fat interface"を避けることができます。
class DOMTraverser {
constructor(settings) {
this.settings = settings;
this.setup();
}
setup() {
this.rootNode = this.settings.rootNode;
this.animationModule.setup();
}
traverse() {
// ...
}
}
const $ = new DOMTraverser({
rootNode: document.getElementsByTagName('body'),
animationModule() {} // 大抵の場合トラバーサルにアニメーションは使わない
// ...
});
class DOMTraverser {
constructor(settings) {
this.settings = settings;
this.options = settings.options;
this.setup();
}
setup() {
this.rootNode = this.settings.rootNode;
this.setupOptions();
}
setupOptions() {
if (this.options.animationModule) {
// ...
}
}
traverse() {
// ...
}
}
const $ = new DOMTraverser({
rootNode: document.getElementsByTagName('body'),
options: {
animationModule() {}
}
});
Dependency Inversion Principle (DIP)
この原則は2つの要素を含みます。
・『上位モジュールは下位モジュールに依存してはならない』。どちらも抽象に依存するべきです。
・『抽象は実装に依存してはならない』。実装が抽象に依存するべきです。
これは最初はよくわからないかもしれません。
AngularJSが、この問題を依存性注入という方法で実装しています。
上位モジュールが下位モジュールを直接操作するのを防ぐというDIPの考え方は、完全に同じ概念ではありませんが、DIを使って達成することができます。
この大きな利点は、モジュール間の結合度を減らすことができることです。
結合度の高いモジュールは非常に悪い作りです。
コードのリファクタリングが難しくなるからです。
前述のようにJavaScriptにはインターフェイスがありません。
そのため、あるクラスが他のクラスに公開するメソッドとプロパティについては暗黙の了解とされている必要があります。
以下の例では、InventoryTrackerに渡すモジュールがrequestItemメソッドを実装していることが暗黙の了解になります。
class InventoryRequester {
constructor() {
this.REQ_METHODS = ['HTTP'];
}
requestItem(item) {
// ...
}
}
class InventoryTracker {
constructor(items) {
this.items = items;
// 中に直接書いているので、完全にInventoryRequesterに依存してしまう。
this.requester = new InventoryRequester();
}
requestItems() {
this.items.forEach((item) => {
this.requester.requestItem(item);
});
}
}
const inventoryTracker = new InventoryTracker(['apples', 'bananas']);
inventoryTracker.requestItems();
class InventoryTracker {
constructor(items, requester) {
this.items = items;
this.requester = requester;
}
requestItems() {
this.items.forEach((item) => {
this.requester.requestItem(item);
});
}
}
class InventoryRequesterV1 {
constructor() {
this.REQ_METHODS = ['HTTP'];
}
requestItem(item) {
// ...
}
}
class InventoryRequesterV2 {
constructor() {
this.REQ_METHODS = ['WS'];
}
requestItem(item) {
// ...
}
}
// 依存性を外部で作成してから注入することで、モジュールを簡単に入れ替えることができる。
const inventoryTracker = new InventoryTracker(['apples', 'bananas'], new InventoryRequesterV2());
inventoryTracker.requestItems();
Testing
テストはデプロイより大切です。
テストが足りていないと、コードをデプロイするたびに、どこかが動かなくなっているかどうかがわかりません。
テストの適切な量を決めるのは開発チームの裁量に任されますが、100%のカバレッジを成し遂げることが信頼と安心を達成する早道です。
すなわち、優れたテストフレームワークを持った上で、優れたカバレッジツールを使う必要があります。
テストを書かない理由はありません。
優れたテストフレームワークはたくさんあるので、開発チームに合ったものを選びましょう。
適切なものを見付けたら、実装した全ての新機能やモジュールについてテストを常に書いていくことを目標にしてください。
テスト駆動開発(TDD)まで辿り着ければ素晴らしいことです。
重要な点は、新機能をデプロイする、あるいは既存コードをリファクタリングする際に、テストで達成しなければならない目標が先にわかることです。
Single concept per test
import assert from 'assert';
describe('MakeMomentJSGreatAgain', () => {
it('handles date boundaries', () => {
let date;
date = new MakeMomentJSGreatAgain('1/1/2015');
date.addDays(30);
assert.equal('1/31/2015', date);
date = new MakeMomentJSGreatAgain('2/1/2016');
date.addDays(28);
assert.equal('02/29/2016', date);
date = new MakeMomentJSGreatAgain('2/1/2015');
date.addDays(28);
assert.equal('03/01/2015', date);
});
});
import assert from 'assert';
describe('MakeMomentJSGreatAgain', () => {
it('handles 30-day months', () => {
const date = new MakeMomentJSGreatAgain('1/1/2015');
date.addDays(30);
assert.equal('1/31/2015', date);
});
it('handles leap year', () => {
const date = new MakeMomentJSGreatAgain('2/1/2016');
date.addDays(28);
assert.equal('02/29/2016', date);
});
it('handles non-leap year', () => {
const date = new MakeMomentJSGreatAgain('2/1/2015');
date.addDays(28);
assert.equal('03/01/2015', date);
});
});
Concurrency
Use Promises, not callbacks
コールバックは過去の遺物で、ネストが恐ろしいことになるから使っていけません。
ES2015/ES6ではPromiseが標準装備されたので、そちらを使いましょう。
import { get } from 'request';
import { writeFile } from 'fs';
get('https://en.wikipedia.org/wiki/Robert_Cecil_Martin', (requestErr, response) => {
if (requestErr) {
console.error(requestErr);
} else {
writeFile('article.html', response.body, (writeErr) => {
if (writeErr) {
console.error(writeErr);
} else {
console.log('File written');
}
});
}
});
import { get } from 'request';
import { writeFile } from 'fs';
get('https://en.wikipedia.org/wiki/Robert_Cecil_Martin')
.then((response) => {
return writeFile('article.html', response);
})
.then(() => {
console.log('File written');
})
.catch((err) => {
console.error(err);
});
Async/Await are even cleaner than Promises
Promiseはコールバックに対するスマートな解決手段でしたが、ES2017/ES8ではさらに優れたAsync/Awaitが提供されます。
必要なことは、関数の前にasyncって書くだけです。
それだけで、メソッドチェーンを使うこともなく順番に命令を書いていくことができます。
ES2017/ES8を使ってもいいのであれば、今後はこれを使いましょう。
import { get } from 'request-promise';
import { writeFile } from 'fs-promise';
get('https://en.wikipedia.org/wiki/Robert_Cecil_Martin')
.then((response) => {
return writeFile('article.html', response);
})
.then(() => {
console.log('File written');
})
.catch((err) => {
console.error(err);
});
import { get } from 'request-promise';
import { writeFile } from 'fs-promise';
async function getCleanCodeArticle() {
try {
const response = await get('https://en.wikipedia.org/wiki/Robert_Cecil_Martin');
await writeFile('article.html', response);
console.log('File written');
} catch(err) {
console.error(err);
}
}
Error Handling
エラーをthrowするのはよいことです。
プログラムのどこかに問題があって、関数の実行が停止され、プロセスが停止され、スタックトレースがコンソールに通知されたと、ランタイムが正常に判断したことがわかります。
Don't ignore caught errors
エラーに反応したり修正したりすることができないので、catchしたエラーを握り潰すのはよくありません。
コンソール(console.log)への出力は、他の出力の洪水に埋もれてすぐに失われてしまうので、あまりよくありません。
try/catchを書けば、そこにエラーが発生する可能性があるとわかります。
エラーが起こったときのために、対策を練るか再びthrowするか行いましょう。
try {
functionThatMightThrow();
} catch (error) {
console.log(error);
}
try {
functionThatMightThrow();
} catch (error) {
// console.logより目立つログを出す
console.error(error);
// もしくは別の対策を行う
notifyUserOfError(error);
// もしくは別の対策を行う
reportErrorToService(error);
// 別に全部行ってもかまわない
}
Don't ignore rejected promises
同じ理由によって、Proimiseのエラーも無視してはいけません。
getdata()
.then((data) => {
functionThatMightThrow(data);
})
.catch((error) => {
console.log(error);
});
getdata()
.then((data) => {
functionThatMightThrow(data);
})
.catch((error) => {
// なんかする
console.error(error);
// 別のことをする
notifyUserOfError(error);
// さらに別のこともする
reportErrorToService(error);
});
Formatting
コーディングスタイルは主観的です。
この文書に書かれている他の文と同様、絶対に従わなければならない厳しいルールはありません。
ただし、ひとつ重要な点を上げるならば、手動でフォーマットしてはいけません。
これを自動化するツールはたくさんあります。
それらのうち、どれかひとつを使用してください。
コーディングスタイルについて議論するのは時間とコストの無駄です。
自動書式設定の対象範囲にないものについては、ここなどのガイダンスを参照してください。
Use consistent capitalization
JavaScriptには型がないので、変数や関数のルールを大文字小文字で表すことがあります。
これらは主観的なルールなので、場合によって様々な取り決めが存在します。
ポイントは、一度決めたら首尾一貫してルールを採用することです。
const DAYS_IN_WEEK = 7;
const daysInMonth = 30;
const songs = ['Back In Black', 'Stairway to Heaven', 'Hey Jude'];
const Artists = ['ACDC', 'Led Zeppelin', 'The Beatles'];
function eraseDatabase() {}
function restore_database() {}
class animal {}
class Alpaca {}
const DAYS_IN_WEEK = 7;
const DAYS_IN_MONTH = 30;
const SONGS = ['Back In Black', 'Stairway to Heaven', 'Hey Jude'];
const ARTISTS = ['ACDC', 'Led Zeppelin', 'The Beatles'];
function eraseDatabase() {}
function restoreDatabase() {}
class Animal {}
class Alpaca {}
Function callers and callees should be close
関数が別の関数を呼び出すのであれば、それらはソース内の近くに置いておきましょう。
最もいいのは、呼び出される関数を呼び出す関数の直下に置くことです。
ソースを読むときは普通上から順に読んでいくので、そのように書いた方が他者が読んでも理解しやすくなります。
class PerformanceReview {
constructor(employee) {
this.employee = employee;
}
lookupPeers() {
return db.lookup(this.employee, 'peers');
}
lookupManager() {
return db.lookup(this.employee, 'manager');
}
getPeerReviews() {
const peers = this.lookupPeers();
// ...
}
perfReview() {
this.getPeerReviews();
this.getManagerReview();
this.getSelfReview();
}
getManagerReview() {
const manager = this.lookupManager();
}
getSelfReview() {
// ...
}
}
const review = new PerformanceReview(employee);
review.perfReview();
class PerformanceReview {
constructor(employee) {
this.employee = employee;
}
perfReview() {
this.getPeerReviews();
this.getManagerReview();
this.getSelfReview();
}
getPeerReviews() {
const peers = this.lookupPeers();
// ...
}
lookupPeers() {
return db.lookup(this.employee, 'peers');
}
getManagerReview() {
const manager = this.lookupManager();
}
lookupManager() {
return db.lookup(this.employee, 'manager');
}
getSelfReview() {
// ...
}
}
const review = new PerformanceReview(employee);
review.perfReview();
Comments
Only comment things that have business logic complexity.
コメントは解説を書くところであり、要件ではありません。
良いコードはコード自体が要件になっています。
function hashIt(data) {
// ハッシュ
let hash = 0;
// 文字列長
const length = data.length;
// 全文字列でループ
for (let i = 0; i < length; i++) {
// キャラクタコードを取得
const char = data.charCodeAt(i);
// ハッシュを作る
hash = ((hash << 5) - hash) + char;
// 32ビットintにする
hash &= hash;
}
}
function hashIt(data) {
let hash = 0;
const length = data.length;
for (let i = 0; i < length; i++) {
const char = data.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
// 32ビットintにする
hash &= hash;
}
}
Don't leave commented out code in your codebase
そのためにバージョン管理システムが存在するので、コードをコメントアウトして残す必要はありません。
doStuff();
// doOtherStuff();
// doSomeMoreStuff();
// doSoMuchStuff();
doStuff();
Don't have journal comments
だから履歴はバージョン管理使えっつーてるだろ。
デッドコード、コメントアウトコード、そして特にジャーナルコメントは一切不要です。
履歴取得にはgit log
を使用してください。
/**
* 2016-12-20: Removed monads, didn't understand them (RM)
* 2016-10-01: Improved using special monads (JP)
* 2016-02-03: Removed type-checking (LI)
* 2015-03-14: Added combine with type-checking (JR)
*/
function combine(a, b) {
return a + b;
}
function combine(a, b) {
return a + b;
}
Avoid positional markers
位置マーカーは大抵ただのノイズです。
適切な字下げと書式設定を用いて、関数と変数が視覚的にわかりやすくなるようにします。
////////////////////////////////////////////////////////////////////////////////
// Scope Model Instantiation
////////////////////////////////////////////////////////////////////////////////
$scope.model = {
menu: 'foo',
nav: 'bar'
};
////////////////////////////////////////////////////////////////////////////////
// Action setup
////////////////////////////////////////////////////////////////////////////////
const actions = function() {
// ...
};
$scope.model = {
menu: 'foo',
nav: 'bar'
};
const actions = function() {
// ...
};
Translation
Brazilian Portuguese
Spanish
Chinese1 / Chinese2
German
Korean
Polish
Russian1 / Russian2
Vietnamese
Japanese
Indonesia
感想
上から順に訳していって、一番下まで来たときに既に日本語訳があることに気がついて絶望した。
しかしまあ、せっかく訳したので公開することにする。
どうもサンプルコードがいまいち適切ではないものが散見される気がする。
Interfaceの無いDIPとかやる意味あるのか?
あと位置マーカーはたまにやる。
機能グループ毎に分けたいときとか困るんだよね。
クラスを分けろとかいう真っ当な抗議は聞こえません。