関数
関数の引数(理想的には2つ以下)
関数パラメータの数を制限することは非常に重要です。なぜなら、引数が3つを超えると組み合わせ爆発が発生し、個々の引数に対して大量の異なるケースをテストする必要が生じるからです。
1つまたは2つの引数が理想的で、3つ以上は可能な限り避けるべきです。引数が多くなる場合は通常、高レベルのオブジェクトを引数として渡すことで解決できます。この場合、関数が複数の責務を持っている可能性が高いため、機能分割を検討してください。
JavaScriptではクラスのボイラープレートを必要とせずオブジェクトを動的に生成できるため、多数の引数が必要な場合にはオブジェクトの使用を検討してください。
関数が期待するプロパティを明確化するには、ES2015/ES6の分割代入構文が有効です。主な利点は以下の通りです:
- 関数シグネチャを見た際に、使用されるプロパティが即座に把握可能
- 名前付きパラメータを模倣できる
- 分割代入は引数オブジェクトの指定されたプリミティブ型の値をクローンします(ただしオブジェクトと配列はクローンされません)。これにより副作用の防止に役立ちます。
- 未使用プロパティの検出をLinterで可能にします
悪い例:
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
});関数は単一責任を果たすべき
これはソフトウェア工学で最も重要な原則です。複数の処理を行う関数は構成・テスト・理解が困難になります。関数を単一アクションに分離することで、リファクタリングが容易になりコードの可読性が向上します。このガイドからこの原則だけを実践しても、多くの開発者を優れたコード品質を実現できます。
悪い例:
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;
}重複コードの排除
ロジック変更時の修正箇所を最小化するため、重複コードは厳格に回避してください。レストランの在庫管理で例えると、トマトの在庫情報が複数リストに分散している場合、トマトを使った料理を提供する際にすべてのリストを更新する必要があります!
重複コードは類似した処理の微妙な差異によって発生しがちです。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);
});
}Object.assignによるデフォルトオブジェクト設定
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);フラグは関数が複数責務を持つことを示唆します。真偽値に基づいて処理が分岐する場合は関数を分割してください。
悪い例:
良い例:
function createFile(name, temp) {
if (temp) {
fs.create(`./temp/${name}`);
} else {
fs.create(name);
}
}副作用の回避(パート1)
function createFile(name) {
fs.create(name);
}
function createTempFile(name) {
createFile(`./temp/${name}`);
}副作用とはファイルへの書き込み、グローバル変数の変更など、入力値変換以外の処理を指します。副作用を伴う処理は単一のサービスに一元化し、状態共有や可変データ型の不適切な使用を回避することが重要です。
悪い例:
良い例:
副作用の回避(パート2)
JavaScriptではオブジェクトや配列などの可変値の扱いに注意が必要です。addItemToCart関数で常にカートのクローンを作成・編集・返却する方法は、予期せぬ変更を防ぐ有効な手段です(処理速度の面で大きなオブジェクトのクローン作成は優れたライブラリの利用を推奨)。
// 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'];良い例:
⬆ トップに戻る
グローバル関数への書き込みを回避
Array.prototype拡張などグローバルオブジェクトの汚染はライブラリ衝突のリスクを高めます。代わりにES6クラスを使用してグローバルArrayを拡張する方法が安全です。
悪い例:
良い例:
命令型プログラミングより関数型プログラミングを優先
JavaScriptはHaskellのような純粋な関数型言語ではありませんが、関数型のアプローチを採用することでコードの清潔さとテスト容易性が向上します。
悪い例:
const addItemToCart = (cart, item) => {
cart.push({ item, date: Date.now() });
};良い例:
const addItemToCart = (cart, item) => {
return [...cart, { item, date: Date.now() }];
};条件分岐のカプセル化
悪い例:
良い例:
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));
}
}否定条件を避ける
悪い例:
良い例:
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)) {
// ...
}型チェックの回避(パート1)
JavaScriptの型自由特性を活用し、一貫性のあるAPI設計を行うことで型チェックの必要性を低減できます。
悪い例:
function isDOMNodeNotPresent(node) {
// ...
}
if (!isDOMNodeNotPresent(node)) {
// ...
}良い例:
function isDOMNodePresent(node) {
// ...
}
if (isDOMNodePresent(node)) {
// ...
}型チェックの回避(パート2)
プリミティブ値の型チェックが必要な場合はTypeScriptの採用を検討してください。静的型付けにより可読性を維持しながら型安全性を向上できます。
悪い例:
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();
}
}過剰な最適化を避ける
現代のブラウザは高度な最適化を自動で実施します。パフォーマンス改善が必要な場合は信頼性の高いリソースを参照し、実際に計測されたボトルネックに焦点を当ててください!
悪い例:
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"));
}デッドコードの削除
使用されていないコードはバージョン管理システムに任せ、コードベースから確実に削除してください。デッドコードは保守性を低下させ、誤動作の原因となります!
悪い例:
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");