読みやすいコードを書くために
ソフトウェアエンジニアリングの原則を JavaScript に適用させたもの。 これはスタイルガイドではありません。JavaScript で可読性が良く、再利用でき、リファクタリング可能なソフトウェアを提供するための一つのガイドです。
変数/Variables
意味があり発音可能な変数名を利用すること
const yyyymmdstr = moment().format('YYYY/MM/DD');
const currentDate = moment().format('YYYY/MM/DD');
同じタイプの変数には同じ単語を利用すること
getUserInfo();
getClientData();
getCustomerRecord();
getUser();
検索できる名前を利用すること
私たちはコードを書くよりも読む方が多いでしょう。そのため、コードを読みやすく検索できるように書くことは重要なことです。 プログラムを理解するために有意義な名前を付けない変数によって、私たちは読み手を傷つけています。 変数を検索可能にしておいてください。buddy.jsや ESLintのようなツールは、名前が付いていない変数を識別する手助けをしてくれます。 Bad:
// What the heck is 86400000 for?
// 一体、なんのための86400000なんだい?
setTimeout(blastOff, 86400000);
// Declare them as capitalized named constants.
// それらを大文字の名前付き定数として宣言してください。
const MILLISECONDS_IN_A_DAY = 86400000;
setTimeout(blastOff, MILLISECONDS_IN_A_DAY);
説明的な変数を利用すること
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);
メンタルマップを避ける
明らかなことは暗黙的なことよりも優れています。
メンタルマップとは、認知心理学において記憶の中に構成される「あるべき姿」のイメージをさす言葉です。
const locations = ['Austin', 'New York', 'San Francisco'];
locations.forEach((l) => {
doStuff();
doSomeOtherStuff();
// ...
// ...
// ...
// Wait, what is `l` for again?
// ちょっと待って、もう一度`l`ってなんだっけ?
dispatch(l);
});
const locations = ['Austin', 'New York', 'San Francisco'];
locations.forEach((location) => {
doStuff();
doSomeOtherStuff();
// ...
// ...
// ...
dispatch(location);
});
不必要なコンテキストを加えない
もしクラスやオブジェクト名が 何かを伝えているのであれば、変数名でそのことを繰り返してはいけません。
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';
}
短絡評価や条件の代わりにデフォルト引数を利用すること
デフォルト引数は多くの場合、短絡評価よりも明確です。
ご存知の通り、これらを使った場合、関数はundefined
の引数のみにデフォルト値を提供します。
他の''
、""
、false
、null
、0
やNaN
のような"falsy"値は、デフォルト値で置き換わることはありません。
Bad:
function createMicrobrewery(name) {
const breweryName = name || 'Hipster Brew Co.';
// ...
}
Good:
function createMicrobrewery(name = 'Hipster Brew Co.') {
// ...
}
関数/Functions
関数の引数(2 つ以下が理想的)
関数の引数の数を制限することは、テストを簡単に行えるという点において非常に重要なことです。 3 つ以上あるということは、それぞれ別の引数を伴った数多くの異なるケースをテストしなければならないという、組み合わせ爆発につながります。
1 つか 2 つの場合は理想的で、3 つは可能であれば避けるべきです。 それ以上のものは統合する必要があります。通常 2 つ以上の引数を持つ場合、その関数は余りにも多くのことをやろうとしています。 そうでない場合、ほとんどの場合、上位のオブジェクトを引数とすれば十分でしょう。
JavaScript は、多くのクラスの雛形がなくとも素早くオブジェクトを作成することができるため、もし多くの引数を必要としているとわかった場合は、オブジェクトを使うことができます。
- 誰かが関数の定義を見たときに、どのプロパティが利用されているかが明らかです。
- 分割代入は、関数の中に渡された引数オブジェクトの指定されたプリミティブ型の値を複製します。これは副作用を防ぐ役割をします。注意:引数オブジェクトから再構成されたオブジェクトや配列は複製されません。
- Lint ツールが、未使用のプロパティについて警告を出すことができます。このようなことは、分割代入しない限り不可能でしょう。
function createMenu(title, body, buttonText, cancellable) {
// ...
}
function createMenu({ title, body, buttonText, cancellable }) {
// ...
}
createMenu({
title: 'Foo',
body: 'Bar',
buttonText: 'Baz',
cancellable: true,
});
関数は 1 つのことを行うこと
これはいままでのところ、ソフトウェアエンジニアリングにとってもっとも重要なルールです。 関数が 2 つ以上のことをやるときは、それを作ったり、テストしたり、理由付けすることが難しくなります。 関数をただ 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 addToDate(date, month) {
// ...
}
const date = new Date();
// It's hard to tell from the function name what is added
// 関数名からは何が追加されたのかがわかりにくい
addToDate(date, 1);
function addMonthToDate(month, date) {
// ...
}
const date = new Date();
addMonthToDate(1, date);
関数はただ 1 つの抽象化をすること
関数が 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 parseBetterJSAlternative(code) {
const tokens = tokenize(code);
const ast = lexer(tokens);
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;
}
重複したコードを削除すること
重複したコードを避けるために絶対にベストを尽くしてください。 重複したコードは、もし何かのロジックを変更しようとした場合、何か変更する場所が 1 つ以上あるという意味で悪です。
あなたがレストランを経営していて、すべてのトマト、たまねぎ、ニンニク、スパイスなどの在庫を追跡しているとします。 もし複数のリストを持っている場合、トマトが入った料理を提供したら、全てを更新しなければなりません。 もしそれが 1 つだった場合、更新する場所はたった 1 つです!
しばしば、共有点が多いにも関わらず、2 つ以上のわずかに異なる部分があるために、重複したコードを持つことがあります。 しかしその違いによって、ほとんど同じことを行う 2 つ以上の別々の関数が必要になります。 重複したコードを削除するということは、関数/モジュール/クラスを 1 つだけ利用して、これらのわずかにを異なる一連のものを処理することができる抽象化を作るということを意味します。
抽象化を正しく行うことが重要です。そのため、クラス セクションで説明されている SOLID の原則に従う必要があります。 悪い抽象化は、重複コードより悪い可能性があります、注意深くしましょう! 少し難しいことではありますが、もし良い抽象化ができるのであればそれをやってください! 自分自身を繰り返さないこと。そうしなければ、1 つの場所を 変更したいときはいつでも、複数の場所を変更することになります。
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);
});
}
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',
// User did not include 'body' key
// ユーザーは `body` キーを含めなくていい
buttonText: 'Send',
cancellable: true,
};
function createMenu(config) {
config = Object.assign(
{
title: 'Foo',
body: 'Bar',
buttonText: 'Baz',
cancellable: true,
},
config
);
// config now equals: {title: "Order", body: "Bar", buttonText: "Send", cancellable: true}
// configはこれと同じになります
// ...
}
createMenu(menuConfig);
フラグを関数の引数のように利用しない
フラグは、この関数が複数のことを行うことを利用者に伝えます。 関数は 1 つのことを行うべきです。関数が真偽値によって異なるコードの経路を経由する場合、その関数を分割してください。
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}`);
}
副作用を避ける (part 1)
関数が、値を受け取り何か他の値を返す以外のことを行う場合、副作用を引き起こします。 副作用とは、ファイルを書き込みしたり、なにかのグローバル変数を書き換えたり、誤ってあなたの全てのお金を見知らぬ人に振込みするようなものです。
時には、副作用を持つプログラムを必要とします。少し前に例で挙げた、ファイルに書き込みしなければならない場合のように。 あなたがしたいことは、どこでこれを行うかを集中させることです。 ファイルを部分的に書き換えするような、関数やクラスをいくつも持たないでください。 それを行う 1 つのサービスを持ってください。1 つ、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'];
副作用を避ける (part 2)
JavaScript では、プリミティブ型は値渡しであり、オブジェクトと配列は参照渡しです。
オブジェクトと配列の場合、もし関数がショッピングカート内の配列に変更を加える場合、(例えば、購入するために商品を加えるなど)
このcart
と同じ配列を使っている他の関数は、この追加の影響を受けます。
ユーザーが購入ボタンをクリックすると、purchase
関数を呼び出し、その関数はネットワークリクエストを生成して、そのcart
配列をサーバーへ送信します。
しかし、ネットワーク接続が悪いために、purchase
関数はリクエストを繰り返し送信し続けなければならない。
ユーザーが、ネットワークリクエストが始まる前に欲しいとは思っていない商品の"カートに追加する"ボタンをうっかりクリックしてしまった場合。
もしそれが起こって、ネットワークリクエストが始まった場合、その時、その purchase 関数は間違って追加された商品を送信してしまいます。
なぜなら、その関数はaddItemToCart
によって望んでいない商品を追加され、変更されてしまったショッピングカート配列を参照しているからです。
良い解決策は、addItemToCart
が常にcart
を複製して変更し、その変更したものを返すことでしょう。
このことは、ショッピングカートへの参照を保持している他の関数は、いかなる変更の影響も受けないことを保証します。
このアプローチに関する 2 つの注意点:
- 時に、渡されたオブジェクトを変更したい場所があるケースがありますが、このプログラミング手法を採用している場合、このケースは稀だということに気づくでしょう。そしてほとんどの場合、副作用がないようにリファクタリングすることができます。
- 巨大なオブジェクトを複製することは、パフォーマンスの面で非常にコストが高いことになります。 幸運なことに、これはこの手法においては大きな問題ではありません。なせなら、このようなプログラミングアプローチをより高速かつ、手作業でオブジェクトや配列を複製するよりもメモリ使用量を抑えることができる素晴らしいライブラリが存在するからです。
const addItemToCart = (cart, item) => {
cart.push({ item, date: Date.now() });
};
const addItemToCart = (cart, item) => {
return [...cart, { item, date: Date.now() }];
};
グローバル関数に書き込まない
グローバルを汚染することは JavaScript におけるバッドプラクティスです。
なぜなら、他のライブラリをクラッシュさせるかもしれないし、あなたの API を使っているユーザーは、プロダクション環境で例外を受け取るまで、そのことについて何もわからないからです。
例を考えてみましょう。もし、JavaScript の既存の Array 関数を拡張して、diff
という 2 つの配列間の差分をみることができる関数を追加したいとしたらどうでしょうか?
Array.prototype
に新しい関数を作成することができるかもしれないですが、他のライブラリで同じことをやろうとしているものをクラッシュさせるかもしれません。
もし、他のライブラリが、diff
を単に配列の最初と最後の差分を見つけるために利用していたとしたらどうでしょう?
このことは、なぜグローバルのArray
を単純に拡張するよりも、ES2015/ES6 のクラスを使った方がより良いかという理由です。
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));
}
}
手続き型プログラミングより関数型プログラミングを優先する
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);
条件をカプセル化する
if (fsm.state === 'fetching' && isEmpty(listNode)) {
// ...
}
function shouldShowSpinner(fsm, listNode) {
return fsm.state === 'fetching' && isEmpty(listNode);
}
if (shouldShowSpinner(fsmInstance, listNodeInstance)) {
// ...
}
否定的な条件を避ける
function isDOMNodeNotPresent(node) {
// ...
}
if (!isDOMNodeNotPresent(node)) {
// ...
}
function isDOMNodePresent(node) {
// ...
}
if (isDOMNodePresent(node)) {
// ...
}
条件文を避ける
これは一見不可能なタスクのように見えます。
最初にこれを聞いて、ほとんどの人はこう言います。「if
文なしで、何をするの?」
この答えは、多くの場合、同じタスクを実行するためにポリモーフィズム(多様性)を使ってできるよ。ということです。
2 つめの質問は大抵これです。「うーん、いいと思うんだけど、なぜそれをやりたいんだろう。。。」
この答えは、私たちが先に学んだクリーンなコードコンセプト、「関数はただ 1 つのことを行うべき」です。
あなたのクラスや関数がif
文を持っているとき、この関数は 1 つ以上のことを行なっていることを示唆しています。
たった 1 つのことをやるということを覚えておいてください。
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();
}
}
型チェックを避ける (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'));
}
型チェックを避ける (part 2)
もし、あなたが文字列、数値のような基本的なプリミティブな値を扱っていて、ポリモーフィズムを使えない、しかしまだタイプチェックが必要だと感じている場合。 TypeScript の利用を検討してみてください。 これは標準の JavaScript 構文の上に静的な型を提供するので、通常の JavaScript に変わる優れた代替品です。
通常の 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;
}
行き過ぎた最適化をしない
モダンブラウザは、ランタイムの中で多くの最適化を行います。 何度も最適化を行なっているのであれば、それは時間の無駄です。ここにどこで最適化が不足するかをみるための良い資料があります。 可能であれば、それらが修正されるまでは、それらだけを最適化の対象としてください。
// 古いブラウザにおいては、キャッシュされていない`list.length`はコストが掛かる
// なぜなら、`list.length`が再計算されるから。しかし、モダンプラウザでは最適化されている
for (let i = 0, len = list.length; i < len; i++) {
// ...
}
for (let i = 0; i < list.length; i++) {
// ...
}
使っていないコードを削除する
使っていないコードは重複したコードと同じくらい悪いだけのものです。コードベースに残しておく理由はなにもありません。 もし、呼び出されていないのであれば、それらを取り除きましょう! もし、まだ必要であってもバージョン管理の中にあるだけで安全でしょう。
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');
オブジェクとデータ構造
getters と setters を使うこと
getters と setters を使ってオブジェクト上のデータにアクセスする方が、単純にオブジェクトのプロパティをみるよりも良くなります。 「なぜ?」と尋ねるかもしれない。これはあまりまとまっていないものですが、その理由のリストです。
- もし、オブジェクトのプロパティを取得する以上のことをやろうとした場合、コードベースの全てのアクセッサを検索して変更して回る必要がない。
- 単純に
set
を行う時に、バリデーションを追加することができる。 - 内部をカプセル化する。
- 取得や設定の時に、簡単にロギングやエラーハンドリングを追加できる。
- オブジェクトのプロパティを遅延評価することができる、これをその値をサーバーから取得すると言おう。
function makeBankAccount() {
// ...
return {
balance: 0,
// ...
};
}
const account = makeBankAccount();
account.balance = 100;
function makeBankAccount() {
// これはprivate
let balance = 0;
// getter、以下でオブジェクトを返すことでpublicにします
function getBalance() {
return balance;
}
// setter、パラメータオブジェクト受け取ることでpublicにします(なんか原文が違う。。。)
function setBalance(amount) {
// ... balanceを更新するまえにバリデーションを行う
balance = amount;
}
return {
// ...
getBalance,
setBalance,
};
}
const account = makeBankAccount();
account.setBalance(100);