ソフトウェア工学の原則、ロバートC.マーティンの著書「クリーンコード」から、 JavaScriptに適応しました。これはスタイルガイドではありません。これは、JavaScriptで読み取り可能で、再利用可能で、リファクタリング可能なソフトウェアを作成するためのガイドです。
ここにあるすべての原則に厳密に従う必要はなく、さらに少なくなります 普遍的に合意されました。これらはガイドラインであり、それ以上のものではありませんが、 クリーンコードの作者による長年の集合的な経験にわたって成文化されたもの。
私たちのソフトウェアエンジニアリングの技術は50年強前のものであり、私たちは まだたくさん学んでいます。ソフトウェアアーキテクチャがアーキテクチャと同じくらい古い場合 それ自体、多分それから私たちは従うべきより難しい規則を持つでしょう。今のところ、これらをしましょう ガイドラインは、の品質を評価するための試金石として機能します あなたとあなたのチームが作成する JavaScript コード。
もう1つ:これらを知っていてもすぐにはより良いソフトウェアになるわけではありません 開発者、そして長年彼らと協力することはあなたが作らないという意味ではありません 間違い。すべてのコードは、湿った粘土のように、最初のドラフトとして始まります 最終的な形に形作られました。最後に、次のときに欠陥を彫り落とします 私たちは仲間と一緒にそれをレビューします。必要な最初のドラフトで自分を殴らないでください 改善。代わりにコードを打ち負かしてください!
悪い:
const yyyymmdstr = moment().format("YYYY/MM/DD");
よし:
const currentDate = moment().format("YYYY/MM/DD");
悪い:
getUserInfo();
getClientData();
getCustomerRecord();
よし:
getUser();
これまでに書くよりも多くのコードを読みます。私たちが 書き込みは読み取り可能で検索可能です。最終的に変数に名前を付けないことによって 私たちのプログラムを理解するために意味があるので、私たちは読者を傷つけます。 名前を検索可能にします。buddy.js や ESLint などのツールは、名前のない定数を識別するのに役立ちます。
悪い:
// What the heck is 86400000 for?
setTimeout(blastOff, 86400000);
よし:
// Declare them as capitalized named constants.
const MILLISECONDS_PER_DAY = 60 * 60 * 24 * 1000; //86400000;
setTimeout(blastOff, MILLISECONDS_PER_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?
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, color) {
car.carColor = color;
}
よし:
const Car = {
make: "Honda",
model: "Accord",
color: "Blue"
};
function paintCar(car, color) {
car.color = color;
}
デフォルトのパラメータは、多くの場合、短絡よりもクリーンです。次のことに注意してください それらを使用すると、関数は引数のデフォルト値のみを提供します。、、などの他の「ファルシー」値は、デフォルト値に置き換えられません。
undefined
''
""
false
null
0
NaN
悪い:
function createMicrobrewery(name) {
const breweryName = name || "Hipster Brew Co.";
// ...
}
よし:
function createMicrobrewery(name = "Hipster Brew Co.") {
// ...
}
関数パラメータの量を制限することは、次の理由で非常に重要です。 関数のテストが簡単になります。3つ以上を持つことは、 さまざまなケースを大量にテストする必要がある組み合わせ爆発 それぞれの別々の引数。
1つまたは2つの引数が理想的なケースであり、可能であれば3つの引数は避ける必要があります。 それ以上のものはすべて統合する必要があります。通常、あなたが持っている場合 2つ以上の引数がある場合、あなたの関数はあまりにも多くのことをしようとしています。場合 そうでない場合、ほとんどの場合、上位レベルのオブジェクトで十分です。 引数。
JavaScriptを使用すると、多くのクラスなしでその場でオブジェクトを作成できます 定型文、あなたは自分自身が必要だと思っているならオブジェクトを使うことができます たくさんの議論。
関数が期待するプロパティを明確にするために、ES2015 / ES6を使用できます 構文の破壊。これにはいくつかの利点があります。
悪い:
function createMenu(title, body, buttonText, cancellable) {
// ...
}
createMenu("Foo", "Bar", "Baz", true);
よし:
function createMenu({ title, body, buttonText, cancellable }) {
// ...
}
createMenu({
title: "Foo",
body: "Bar",
buttonText: "Baz",
cancellable: true
});
これは、ソフトウェアエンジニアリングにおいて群を抜いて最も重要なルールです。関数が機能する場合 複数のことを行うと、構成、テスト、および推論が難しくなります。 関数を 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);
複数の抽象化レベルがある場合、関数は通常 やりすぎです。機能を分割することで、再利用性と使いやすさにつながります テスティング。
悪い:
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 syntaxTree = parse(tokens);
syntaxTree.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 parse(tokens) {
const syntaxTree = [];
tokens.forEach(token => {
syntaxTree.push(/* ... */);
});
return syntaxTree;
}
コードの重複を避けるために最善を尽くしてください。重複コードは悪いです 変更する必要がある場合に何かを変更する場所が複数あることを意味します いくつかのロジック。
あなたがレストランを経営していて、あなたがあなたの在庫を追跡していると想像してみてください:すべてのあなたの トマト、玉ねぎ、ニンニク、スパイスなど複数のリストがある場合 あなたはこれをオンに保ちます、そしてあなたが料理を出すときすべてが更新されなければなりません それらの中のトマト。リストが 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);
});
}
悪い:
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
buttonText: "Send",
cancellable: true
};
function createMenu(config) {
let finalConfig = Object.assign(
{
title: "Foo",
body: "Bar",
buttonText: "Baz",
cancellable: true
},
config
);
return finalConfig
// config now equals: {title: "Order", body: "Bar", buttonText: "Send", cancellable: true}
// ...
}
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}`);
}
関数は、値を取る以外のことをすると副作用を生成します 別の値を返します。副作用は、ファイルへの書き込みである可能性があります。 グローバル変数を変更するか、誤ってすべてのお金を 他人。
今、あなたは時々プログラムで副作用を持つ必要があります。前のように たとえば、ファイルに書き込む必要がある場合があります。あなたがしたいことは これを行う場所を一元化します。複数の関数やクラスを持たない 特定のファイルに書き込むこと。それを行うサービスを1つ用意してください。唯一無二。
主なポイントは、オブジェクト間で状態を共有するなどの一般的な落とし穴を回避することです 構造なしで、何にでも書き込むことができる可変データ型を使用して、 副作用が発生する場所を一元化しません。あなたがこれを行うことができれば、あなたは 他の大多数のプログラマーよりも幸せになりましょう。
悪い:
// Global variable referenced by following function.
// If we had another function that used this name, now it'd be an array and it could break it.
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'];
JavaScript では、変更できない (不変) 値と変更可能な値があります。 (変更可能)。オブジェクトと配列は2種類の変更可能な値なので重要です 関数にパラメーターとして渡されるときに慎重に処理します。ある JavaScript 関数は、オブジェクトのプロパティを変更したり、 他の場所で簡単にバグを引き起こす可能性のある配列。
を表す配列パラメータを受け取る関数があるとします。 ショッピングカート。関数がそのショッピングカート配列に変更を加えた場合 - たとえば、購入するアイテムを追加することで、次に他の関数を追加します。 同じ配列を使用すると、この追加の影響を受けます。それは 素晴らしいですが、悪い可能性もあります。悪い状況を想像してみましょう:
cart
ユーザーは、次の関数を呼び出す「購入」ボタンをクリックします。 ネットワーク要求を生成し、アレイをサーバーに送信します。というのは ネットワーク接続が悪い場合、関数は再試行を続ける必要があります 依頼。さて、その間にユーザーが誤って「カートに追加」をクリックした場合はどうなりますか? ネットワーク要求が始まる前に、実際には必要のないアイテムのボタンですか? それが起こり、ネットワーク要求が開始された場合、その購入機能 配列が変更されたため、誤って追加されたアイテムを送信します。
purchase
cart
purchase
cart
優れた解決策は、関数が常にクローンを作成し、編集し、クローンを返すことです。これにより、まだ機能している 古いショッピング カートを使用しても、変更の影響を受けません。
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 のネイティブ配列メソッドを拡張して、次のようなメソッドを持つことができます。 2つの配列の違いを表示しますか?あなたはあなたの新しい関数を書くことができます に、しかしそれは試みた別のライブラリと衝突する可能性があります 同じことをする。その他のライブラリが検索に使用していた場合はどうなりますか 配列の最初と最後の要素の違いは?これが理由です ES2015 / ES6クラスを使用し、単にグローバルを拡張する方がはるかに優れています。
diff
Array.prototype
diff
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));
}
}
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.reduce(
(totalLines, output) => totalLines + output.linesOfCode,
0
);
悪い:
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)) {
// ...
}
これは不可能な作業のようです。これを最初に聞いたとき、ほとんどの人はこう言います。 「声明なしで何かをすることになっているのですか?」答えは、 多くの場合、ポリモーフィズムを使用して同じタスクを実行できます。2番目 質問は通常、「それは素晴らしいことですが、なぜ私はそれをしたいのですか?」です。ザ 答えは私たちが学んだ以前のクリーンコードの概念です:関数は唯一のことをするべきです 一つ。ステートメントを持つクラスと関数がある場合は、 あなたの関数が複数のことをすることをあなたのユーザーに伝えています。思い出す ただ一つのことをしてください。
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();
}
}
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"));
}
文字列や整数などの基本的なプリミティブ値を操作している場合は、 ポリモーフィズムを使用することはできませんが、それでも型チェックの必要性を感じています。 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;
}
最新のブラウザは、実行時に内部で多くの最適化を行います。たくさんの 時間、あなたが最適化しているなら、あなたはあなたの時間を無駄にしているだけです。良いものがあります 最適化が不足している場所を確認するためのリソース。その間、それらまでターゲットにします 可能であれば修正されます。
悪い:
// On old browsers, each iteration with uncached `list.length` would be costly
// because of `list.length` recomputation. In modern browsers, this is optimized.
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");
ゲッターとセッターを使用してオブジェクトのデータにアクセスすることは、単に オブジェクトのプロパティを探しています。「なぜ?」とあなたは尋ねるかもしれません。さて、ここに 理由の整理されていないリスト:
set
悪い:
function makeBankAccount() {
// ...
return {
balance: 0
// ...
};
}
const account = makeBankAccount();
account.balance = 100;
よし:
function makeBankAccount() {
// this one is private
let balance = 0;
// a "getter", made public via the returned object below
function getBalance() {
return balance;
}
// a "setter", made public via the returned object below
function setBalance(amount) {
// ... validate before updating the balance
balance = amount;
}
return {
// ...
getBalance,
setBalance
};
}
const account = makeBankAccount();
account.setBalance(100);
これは、クロージャ(ES5以下)によって実現できます。
悪い:
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
読みやすいクラスの継承、構築、メソッドを取得することは非常に困難です 古典的な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() {
/* ... */
}
}
このパターンはJavaScriptで非常に便利であり、次のような多くのライブラリで見られます。 jQueryとLodashとして。これにより、コードを表現力豊かにし、冗長性を低くすることができます。 そのため、メソッドチェーンを使用して、コードがどれほどクリーンであるかを見てみましょう となります。あなたのクラス関数では、単にすべての関数の最後に戻る、 さらにクラスメソッドをチェーンすることもできます。
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;
// NOTE: Returning this for chaining
return this;
}
setModel(model) {
this.model = model;
// NOTE: Returning this for chaining
return this;
}
setColor(color) {
this.color = color;
// NOTE: Returning this for chaining
return this;
}
save() {
console.log(this.make, this.model, this.color);
// NOTE: Returning this for chaining
return this;
}
}
const car = new Car("Ford", "F-150", "red").setColor("pink").save();
4人のギャングによるデザインパターンで有名に述べられているように、 可能な場合は、継承よりも構成を優先する必要があります。たくさんあります 継承を使用する正当な理由と、コンポジションを使用する多くの正当な理由。 この格言の主なポイントは、あなたの心が本能的に 継承、構成があなたの問題をよりよくモデル化できるかどうかを考えてみてください。いくつかでは できるケース。
それでは、「いつ継承を使うべきか」と疑問に思われるかもしれません。それ 目前の問題に依存しますが、これは継承時のまともなリストです 構成よりも理にかなっています:
悪い:
class Employee {
constructor(name, email) {
this.name = name;
this.email = email;
}
// ...
}
// Bad because Employees "have" tax data. EmployeeTaxData is not a type of 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);
}
// ...
}
As stated in Clean Code, "There should never be more than one reason for a class to change". It's tempting to jam-pack a class with a lot of functionality, like when you can only take one suitcase on your flight. The issue with this is that your class won't be conceptually cohesive and it will give it many reasons to change. Minimizing the amount of times you need to change a class is important. It's important because if too much functionality is in one class and you modify a piece of it, it can be difficult to understand how that will affect other dependent modules in your codebase.
Bad:
class UserSettings {
constructor(user) {
this.user = user;
}
changeSettings(settings) {
if (this.verifyCredentials()) {
// ...
}
}
verifyCredentials() {
// ...
}
}
Good:
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()) {
// ...
}
}
}
As stated by Bertrand Meyer, "software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification." What does that mean though? This principle basically states that you should allow users to add new functionalities without changing existing code.
Bad:
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 === "nodeAdapter") {
return makeHttpCall(url).then(response => {
// transform response and return
});
}
}
}
function makeAjaxCall(url) {
// request and return promise
}
function makeHttpCall(url) {
// request and return promise
}
Good:
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
});
}
}
This is a scary term for a very simple concept. It's formally defined as "If S is a subtype of T, then objects of type T may be replaced with objects of type S (i.e., objects of type S may substitute objects of type T) without altering any of the desirable properties of that program (correctness, task performed, etc.)." That's an even scarier definition.
The best explanation for this is if you have a parent class and a child class, then the base class and child class can be used interchangeably without getting incorrect results. This might still be confusing, so let's take a look at the classic Square-Rectangle example. Mathematically, a square is a rectangle, but if you model it using the "is-a" relationship via inheritance, you quickly get into trouble.
Bad:
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(); // BAD: Returns 25 for Square. Should be 20.
rectangle.render(area);
});
}
const rectangles = [new Rectangle(), new Rectangle(), new Square()];
renderLargeRectangles(rectangles);
Good:
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);
JavaScript doesn't have interfaces so this principle doesn't apply as strictly as others. However, it's important and relevant even with JavaScript's lack of type system.
ISP states that "Clients should not be forced to depend upon interfaces that they do not use." Interfaces are implicit contracts in JavaScript because of duck typing.
A good example to look at that demonstrates this principle in JavaScript is for classes that require large settings objects. Not requiring clients to setup huge amounts of options is beneficial, because most of the time they won't need all of the settings. Making them optional helps prevent having a "fat interface".
Bad:
class DOMTraverser {
constructor(settings) {
this.settings = settings;
this.setup();
}
setup() {
this.rootNode = this.settings.rootNode;
this.settings.animationModule.setup();
}
traverse() {
// ...
}
}
const $ = new DOMTraverser({
rootNode: document.getElementsByTagName("body"),
animationModule() {} // Most of the time, we won't need to animate when traversing.
// ...
});
Good:
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() {}
}
});
This principle states two essential things:
This can be hard to understand at first, but if you've worked with AngularJS, you've seen an implementation of this principle in the form of Dependency Injection (DI). While they are not identical concepts, DIP keeps high-level modules from knowing the details of its low-level modules and setting them up. It can accomplish this through DI. A huge benefit of this is that it reduces the coupling between modules. Coupling is a very bad development pattern because it makes your code hard to refactor.
As stated previously, JavaScript doesn't have interfaces so the abstractions that are depended upon are implicit contracts. That is to say, the methods and properties that an object/class exposes to another object/class. In the example below, the implicit contract is that any Request module for an will have a method.
InventoryTracker
requestItems
Bad:
class InventoryRequester {
constructor() {
this.REQ_METHODS = ["HTTP"];
}
requestItem(item) {
// ...
}
}
class InventoryTracker {
constructor(items) {
this.items = items;
// BAD: We have created a dependency on a specific request implementation.
// We should just have requestItems depend on a request method: `request`
this.requester = new InventoryRequester();
}
requestItems() {
this.items.forEach(item => {
this.requester.requestItem(item);
});
}
}
const inventoryTracker = new InventoryTracker(["apples", "bananas"]);
inventoryTracker.requestItems();
Good:
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) {
// ...
}
}
// By constructing our dependencies externally and injecting them, we can easily
// substitute our request module for a fancy new one that uses WebSockets.
const inventoryTracker = new InventoryTracker(
["apples", "bananas"],
new InventoryRequesterV2()
);
inventoryTracker.requestItems();
Testing is more important than shipping. If you have no tests or an inadequate amount, then every time you ship code you won't be sure that you didn't break anything. Deciding on what constitutes an adequate amount is up to your team, but having 100% coverage (all statements and branches) is how you achieve very high confidence and developer peace of mind. This means that in addition to having a great testing framework, you also need to use a good coverage tool.
There's no excuse to not write tests. There are plenty of good JS test frameworks, so find one that your team prefers. When you find one that works for your team, then aim to always write tests for every new feature/module you introduce. If your preferred method is Test Driven Development (TDD), that is great, but the main point is to just make sure you are reaching your coverage goals before launching any feature, or refactoring an existing one.
Bad:
import assert from "assert";
describe("MomentJS", () => {
it("handles date boundaries", () => {
let date;
date = new MomentJS("1/1/2015");
date.addDays(30);
assert.equal("1/31/2015", date);
date = new MomentJS("2/1/2016");
date.addDays(28);
assert.equal("02/29/2016", date);
date = new MomentJS("2/1/2015");
date.addDays(28);
assert.equal("03/01/2015", date);
});
});
Good:
import assert from "assert";
describe("MomentJS", () => {
it("handles 30-day months", () => {
const date = new MomentJS("1/1/2015");
date.addDays(30);
assert.equal("1/31/2015", date);
});
it("handles leap year", () => {
const date = new MomentJS("2/1/2016");
date.addDays(28);
assert.equal("02/29/2016", date);
});
it("handles non-leap year", () => {
const date = new MomentJS("2/1/2015");
date.addDays(28);
assert.equal("03/01/2015", date);
});
});
Callbacks aren't clean, and they cause excessive amounts of nesting. With ES2015/ES6, Promises are a built-in global type. Use them!
Bad:
import { get } from "request";
import { writeFile } from "fs";
get(
"https://en.wikipedia.org/wiki/Robert_Cecil_Martin",
(requestErr, response, body) => {
if (requestErr) {
console.error(requestErr);
} else {
writeFile("article.html", body, writeErr => {
if (writeErr) {
console.error(writeErr);
} else {
console.log("File written");
}
});
}
}
);
Good:
import { get } from "request-promise";
import { writeFile } from "fs-extra";
get("https://en.wikipedia.org/wiki/Robert_Cecil_Martin")
.then(body => {
return writeFile("article.html", body);
})
.then(() => {
console.log("File written");
})
.catch(err => {
console.error(err);
});
Promises are a very clean alternative to callbacks, but ES2017/ES8 brings async and await which offer an even cleaner solution. All you need is a function that is prefixed in an keyword, and then you can write your logic imperatively without a chain of functions. Use this if you can take advantage of ES2017/ES8 features today!
async
then
Bad:
import { get } from "request-promise";
import { writeFile } from "fs-extra";
get("https://en.wikipedia.org/wiki/Robert_Cecil_Martin")
.then(body => {
return writeFile("article.html", body);
})
.then(() => {
console.log("File written");
})
.catch(err => {
console.error(err);
});
Good:
import { get } from "request-promise";
import { writeFile } from "fs-extra";
async function getCleanCodeArticle() {
try {
const body = await get(
"https://en.wikipedia.org/wiki/Robert_Cecil_Martin"
);
await writeFile("article.html", body);
console.log("File written");
} catch (err) {
console.error(err);
}
}
getCleanCodeArticle()
Thrown errors are a good thing! They mean the runtime has successfully identified when something in your program has gone wrong and it's letting you know by stopping function execution on the current stack, killing the process (in Node), and notifying you in the console with a stack trace.
Doing nothing with a caught error doesn't give you the ability to ever fix or react to said error. Logging the error to the console () isn't much better as often times it can get lost in a sea of things printed to the console. If you wrap any bit of code in a it means you think an error may occur there and therefore you should have a plan, or create a code path, for when it occurs.
console.log
try/catch
Bad:
try {
functionThatMightThrow();
} catch (error) {
console.log(error);
}
Good:
try {
functionThatMightThrow();
} catch (error) {
// One option (more noisy than console.log):
console.error(error);
// Another option:
notifyUserOfError(error);
// Another option:
reportErrorToService(error);
// OR do all three!
}
For the same reason you shouldn't ignore caught errors from .
try/catch
Bad:
getdata()
.then(data => {
functionThatMightThrow(data);
})
.catch(error => {
console.log(error);
});
Good:
getdata()
.then(data => {
functionThatMightThrow(data);
})
.catch(error => {
// One option (more noisy than console.log):
console.error(error);
// Another option:
notifyUserOfError(error);
// Another option:
reportErrorToService(error);
// OR do all three!
});
Formatting is subjective. Like many rules herein, there is no hard and fast rule that you must follow. The main point is DO NOT ARGUE over formatting. There are tons of tools to automate this. Use one! It's a waste of time and money for engineers to argue over formatting.
For things that don't fall under the purview of automatic formatting (indentation, tabs vs. spaces, double vs. single quotes, etc.) look here for some guidance.
JavaScript is untyped, so capitalization tells you a lot about your variables, functions, etc. These rules are subjective, so your team can choose whatever they want. The point is, no matter what you all choose, just be consistent.
Bad:
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 {}
Good:
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 {}
If a function calls another, keep those functions vertically close in the source file. Ideally, keep the caller right above the callee. We tend to read code from top-to-bottom, like a newspaper. Because of this, make your code read that way.
Bad:
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();
Good:
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 are an apology, not a requirement. Good code mostly documents itself.
Bad:
function hashIt(data) {
// The hash
let hash = 0;
// Length of string
const length = data.length;
// Loop through every character in data
for (let i = 0; i < length; i++) {
// Get character code.
const char = data.charCodeAt(i);
// Make the hash
hash = (hash << 5) - hash + char;
// Convert to 32-bit integer
hash &= hash;
}
}
Good:
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;
// Convert to 32-bit integer
hash &= hash;
}
}
Version control exists for a reason. Leave old code in your history.
Bad:
doStuff();
// doOtherStuff();
// doSomeMoreStuff();
// doSoMuchStuff();
Good:
doStuff();
Remember, use version control! There's no need for dead code, commented code, and especially journal comments. Use to get history!
git log
Bad:
/**
* 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;
}
Good:
function combine(a, b) {
return a + b;
}
They usually just add noise. Let the functions and variable names along with the proper indentation and formatting give the visual structure to your code.
Bad:
////////////////////////////////////////////////////////////////////////////////
// Scope Model Instantiation
////////////////////////////////////////////////////////////////////////////////
$scope.model = {
menu: "foo",
nav: "bar"
};
////////////////////////////////////////////////////////////////////////////////
// Action setup
////////////////////////////////////////////////////////////////////////////////
const actions = function() {
// ...
};
Good:
$scope.model = {
menu: "foo",
nav: "bar"
};
const actions = function() {
// ...
};
これは他の言語でも利用可能です: