Awesome
clean-code-typescript ![Tweet](https://img.shields.io/twitter/url/http/shields.io.svg?style=social)
<!--
Clean Code concepts adapted for TypeScript.
-->
TypeScriptの為のクリーンコード
<!-- Inspired from [clean-code-javascript](https://github.com/ryanmcdermott/clean-code-javascript). -->clean-code-javascriptを見て閃きました。
Table of Contents
- Introduction
- Variables
- Functions
- Objects and Data Structures
- Classes
- SOLID
- Testing
- Concurrency
- Error Handling
- Formatting
- Comments
- Translations
Introduction
Robert C. Martinの書籍 Clean CodeをTypeScriptに対応させたソフトウェア工学の原則です。 翻訳書籍(amazonへのリンク) これはスタイルガイドではありません。 TypeScriptで可読性が高く、再利用可能であり、リファクタブルなソフトウェアを生産するためのガイドラインです。
すべての原則に厳密に従う必要はありません、さらに一般に合意されているものはさらに少くなります。 これらはガイドライン以上でしかありませんが、Clean Code の著者達による長年の経験を集めて文書化したものです。
ソフトウェア工学の歴史はほんの50年を少し超えた程度であり、未だに私達は多くのことを学び続けています。 ソフトウェアアーキテクチャが建築と同くらい歴史を持っていたならば、おそらく従うべき原則はより厳しくなっていたでしょう。 現時点では、このガイドラインは、あなたとあなたのチームが作成したTypeScriptコードの品質を評価するための基準として役立つでしょう。
それからもう一つ: これらを知ったからと言ってすぐに優秀なソフトウェア開発者となるわけではありませんし、長年これに従って作業を行っても間違いを犯さないわけではありません。 湿った粘土が最終的な形になるように、コードの各部分は最初のドラフト(ルール)になります。 最終的に同僚とこれをレビューする時、不完全な部分を取り除いていきます。 最初のドラフトに改善が必要となった時、自分自身を責めないでください。 代わりにコードを責めましょう!
Variables
変数
Use meaningful variable names
意味のある変数名を使う
<!-- Distinguish names in such a way that the reader knows what the differences offer. -->何を意味してるかを読み手が区別できる名前を付けましょう。
Bad:
function between<T>(a1: T, a2: T, a3: T): boolean {
return a2 <= a1 && a1 <= a3;
}
Good:
function between<T>(value: T, left: T, right: T): boolean {
return left <= value && value <= right;
}
Use pronounceable variable names
発音可能な変数名を使う
<!-- If you can’t pronounce it, you can’t discuss it without sounding like an idiot. -->あなたがそれを発音できないなら、まぬけに聞こえてまともな論議になりません。
Bad:
type DtaRcrd102 = {
genymdhms: Date;
modymdhms: Date;
pszqint: number;
}
Good:
type Customer = {
generationTimestamp: Date;
modificationTimestamp: Date;
recordId: number;
}
Use the same vocabulary for the same type of variable
同じ型の変数には同じ単語を割り当てる。
Bad:
function getUserInfo(): User;
function getUserDetails(): User;
function getUserData(): User;
Good:
function getUser(): User;
Use searchable names
検索可能な名前を使う
<!-- We will read more code than we will ever write. It's important that the code we do write is readable and searchable. By *not* naming variables that end up being meaningful for understanding our program, we hurt our readers. Make your names searchable. Tools like [TSLint](https://palantir.github.io/tslint/rules/no-magic-numbers/) can help identify unnamed constants. -->私達は書いた以上のコードを読むでしょう。 そのため書いたコードは読みやすく探しやすいコードであることが重要になってきます。 プログラムを理解するのに重要な意味がある変数に名前を付けないことによって、私達は読み手を傷つけています。 名前を付ける時は検索しやすいものにしましょう。 TSLintのようなツールは、名前のない定数を識別するのに役立ちます。
Bad:
// What the heck is 86400000 for?
// 一体何が86400000なのか?
setTimeout(restart, 86400000);
Good:
// Declare them as capitalized named constants.
// 定数の名前は大文字で宣言してください。
const MILLISECONDS_IN_A_DAY = 24 * 60 * 60 * 1000;
setTimeout(restart, MILLISECONDS_IN_A_DAY);
Use explanatory variables
説明変数を使う
Bad:
declare const users: Map<string, User>;
for (const keyValue of users) {
// iterate through users map
// user map を反復処理する
}
Good:
declare const users: Map<string, User>;
for (const [id, user] of users) {
// iterate through users map
// user map を反復処理する
}
Avoid Mental Mapping
メンタルマップを避ける
<!-- Explicit is better than implicit. *Clarity is king.* -->明示的は暗黙的より優れています。 明快さは王様です。
Bad:
const u = getUser();
const s = getSubscription();
const t = charge(u, s);
Good:
const user = getUser();
const subscription = getSubscription();
const transaction = charge(user, subscription);
Don't add unneeded context
不要な文脈を追加しない
<!-- If your class/type/object name tells you something, don't repeat that in your variable name. -->もしあなたの class/type/object の名前が何かを伝えているのなら、あなたの変数名の中でそのことを繰り返さないでください。
Bad:
type Car = {
carMake: string;
carModel: string;
carColor: string;
}
function print(car: Car): void {
console.log(`${car.carMake} ${car.carModel} (${car.carColor})`);
}
Good:
type Car = {
make: string;
model: string;
color: string;
}
function print(car: Car): void {
console.log(`${car.make} ${car.model} (${car.color})`);
}
Use default arguments instead of short circuiting or conditionals
短絡評価や条件式の代わりにデフォルト引数を使用する
<!-- Default arguments are often cleaner than short circuiting. -->デフォルト引数は、短絡評価よりもきれいなことがよくあります。
Bad:
function loadPages(count?: number) {
const loadCount = count !== undefined ? count : 10;
// ...
}
Good:
function loadPages(count: number = 10) {
// ...
}
Use enum to document the intent
目的を明文化するために列挙型(enum)を使用する
<!-- Enums can help you document the intent of the code. For example when we are concerned about values being different rather than the exact value of those. -->列挙型は、コードの目的を明文化するのに役立ちます。 例えば、私達は値の正確さよりも、その値が違うものを指していないかを心配します。
Bad:
const GENRE = {
ROMANTIC: 'romantic',
DRAMA: 'drama',
COMEDY: 'comedy',
DOCUMENTARY: 'documentary',
}
projector.configureFilm(GENRE.COMEDY);
class Projector {
// delactation of Projector
// 映写機を楽しむ
configureFilm(genre) {
switch (genre) {
case GENRE.ROMANTIC:
// some logic to be executed
// 実行されるいくつかのロジック
}
}
}
Good:
enum GENRE {
ROMANTIC,
DRAMA,
COMEDY,
DOCUMENTARY,
}
projector.configureFilm(GENRE.COMEDY);
class Projector {
// delactation of Projector
// 映写機を楽しむ
configureFilm(genre) {
switch (genre) {
case GENRE.ROMANTIC:
// some logic to be executed
// 実行されるいくつかのロジック
}
}
}
Functions
関数
Function arguments (2 or fewer ideally)
関数の引数 (理想は2つ以下)
<!-- Limiting the amount of function parameters is incredibly important because it makes testing your function easier. Having more than three leads to a combinatorial explosion where you have to test tons of different cases with each separate argument. One or two arguments is the ideal case, and three should be avoided if possible. Anything more than that should be consolidated. Usually, if you have more than two arguments then your function is trying to do too much. In cases where it's not, most of the time a higher-level object will suffice as an argument. Consider using object literals if you are finding yourself needing a lot of arguments. To make it obvious what properties the function expects, you can use the [destructuring](https://basarat.gitbooks.io/typescript/docs/destructuring.html) syntax. This has a few advantages: 1. When someone looks at the function signature, it's immediately clear what properties are being used. 2. Destructuring also clones the specified primitive values of the argument object passed into the function. This can help prevent side effects. Note: objects and arrays that are destructured from the argument object are NOT cloned. 3. TypeScript warns you about unused properties, which would be impossible without destructuring. -->関数における引数の数を制限することは非常に重要です。 なぜならそれは貴方の関数のテストをシンプルにするからです。 3つ以上になると、引数ごとの数だけ違うケースをテストしなければならず、組み合わせは爆発的に増加します。
理想的な引数の数は1〜2個であり、3つは避けるべきです。 それ以上の数になるならば結合するべきです。 普通は2つ以上の引数がある場合、関数がやりすぎています。 そうでない場合は、上位のオブジェクトを引数にすれば十分です。
たくさんの引数が必要な場合はオブジェクトリテラルの利用を検討してください。
関数がどのようなプロパティを持っているかを明示的にするた めに、destructuring構文を使うことができます。
これにはいくつかの利点があります:
-
誰かが関数の入出力を見た時に、どのプロパティが利用されているのかがすぐにわかります。
-
分割代入は、関数に渡された引数オブジェクトのプリミティブ型の値を複製します。これは副作用を防ぐのに役立ちます 注釈:引数オブジェクトから分割代入されたObjectとArrayは複製されません。
-
TypeScriptは未使用のプロパティについて警告します、これは分割代入なしでは不可能でしょう。
Bad:
function createMenu(title: string, body: string, buttonText: string, cancellable: boolean) {
// ...
}
createMenu('Foo', 'Bar', 'Baz', true);
Good:
function createMenu(options: { title: string, body: string, buttonText: string, cancellable: boolean }) {
// ...
}
createMenu({
title: 'Foo',
body: 'Bar',
buttonText: 'Baz',
cancellable: true
});
<!--
You can further improve readability by using [type aliases](https://www.typescriptlang.org/docs/handbook/advanced-types.html#type-aliases):
-->
タイプエイリアスを使うことで、さらに読みやすさ向上させることができます。
type MenuOptions = { title: string, body: string, buttonText: string, cancellable: boolean };
function createMenu(options: MenuOptions) {
// ...
}
createMenu({
title: 'Foo',
body: 'Bar',
buttonText: 'Baz',
cancellable: true
});
Functions should do one thing
関数は1つのことだけを行うべきです
<!-- This is by far the most important rule in software engineering. When functions do more than one thing, they are harder to compose, test, and reason about. When you can isolate a function to just one action, they can be refactored easily and your code will read much cleaner. If you take nothing else away from this guide other than this, you'll be ahead of many developers. -->これはソフトウェア・エンジニアリングにおいて、とても重要なルールです。 関数が1つ以上のことをするとき、それは作成してテストし、推測することをより困難にします。 関数を一つの振る舞いに分離することができれば、それらは簡単にリファクタリングすることができるようになり、あなたのコードはとても綺麗になります。 このガイドのこの項以外を何もしなかったとしても、あなたは他の多くの開発者よりも一歩先を行ってるでしょう。
Bad:
function emailClients(clients: Client[]) {
clients.forEach((client) => {
const clientRecord = database.lookup(client);
if (clientRecord.isActive()) {
email(client);
}
});
}
Good:
function emailClients(clients: Client[]) {
clients.filter(isActiveClient).forEach(email);
}
function isActiveClient(client: Client) {
const clientRecord = database.lookup(client);
return clientRecord.isActive();
}
Function names should say what they do
関数名は何をするべきか宣言するべきです。
Bad:
function addToDate(date: Date, month: number): Date {
// ...
}
const date = new Date();
// It's hard to tell from the function name what is added
// 何が追加されたかを、関数名から予測することができません。
addToDate(date, 1);
Good:
function addMonthToDate(date: Date, month: number): Date {
// ...
}
const date = new Date();
addMonthToDate(date, 1);
Functions should only be one level of abstraction
関数は一つの抽象化に留めること。
<!-- When you have more than one level of abstraction your function is usually doing too much. Splitting up functions leads to reusability and easier testing. -->あなたが1つ以上の抽象化を行っている時、関数はやりすぎています。 機能を分割すれば再利用性とテスト容易性を向上させることができます。
Bad:
function parseCode(code: string) {
const REGEXES = [ /* ... */ ];
const statements = code.split(' ');
const tokens = [];
REGEXES.forEach((regex) => {
statements.forEach((statement) => {
// ...
});
});
const ast = [];
tokens.forEach((token) => {
// lex...
});
ast.forEach((node) => {
// parse...
});
}
Good:
const REGEXES = [ /* ... */ ];
function parseCode(code: string) {
const tokens = tokenize(code);
const syntaxTree = parse(tokens);
syntaxTree.forEach((node) => {
// parse...
});
}
function tokenize(code: string): Token[] {
const statements = code.split(' ');
const tokens: Token[] = [];
REGEXES.forEach((regex) => {
statements.forEach((statement) => {
tokens.push( /* ... */ );
});
});
return tokens;
}
function parse(tokens: Token[]): SyntaxTree {
const syntaxTree: SyntaxTree[] = [];
tokens.forEach((token) => {
syntaxTree.push( /* ... */ );
});
return syntaxTree;
}
Remove duplicate code
重複したコードは消す
<!-- Do your absolute best to avoid duplicate code. Duplicate code is bad because it means that there's more than one place to alter something if you need to change some logic. Imagine if you run a restaurant and you keep track of your inventory: all your tomatoes, onions, garlic, spices, etc. If you have multiple lists that you keep this on, then all have to be updated when you serve a dish with tomatoes in them. If you only have one list, there's only one place to update! Oftentimes you have duplicate code because you have two or more slightly different things, that share a lot in common, but their differences force you to have two or more separate functions that do much of the same things. Removing duplicate code means creating an abstraction that can handle this set of different things with just one function/module/class. Getting the abstraction right is critical, that's why you should follow the [SOLID](#solid) principles. Bad abstractions can be worse than duplicate code, so be careful! Having said this, if you can make a good abstraction, do it! Don't repeat yourself, otherwise you'll find yourself updating multiple places anytime you want to change one thing. -->コードの重複を避ける事に最善を尽くしてください。 重複したコードがあるということは、ロジックの変更を行う時に複数の箇所に同じ変更をする必要があるため、よくありません。
貴方がレストランを経営していたとしましょう、そして在庫整理しているとします: トマト、玉ねぎ、ニンニク、スパイスなど、すべてあなたの物です。 貴方がこの在庫を管理するリストを複数持っていた場合、トマト料理を提供したらリストすべてを更新する必要がでてきます。 リストが1つだけなら、更新は1箇所済むでしょう!
共通点が多いが、2つ以上の小さな違いがあるために、コードが重複してしまうことはよくあります。 しかしその結果、その違いによってほとんど同じことを行う2つ以上の関数が必要になってしまいます。 重複したコードを削除するということは、一つの関数/モジュール/クラスで、これらの僅かに異なる一連のものを処理できる抽象化を作るということを意味します。
抽象化を正しく行うことが重要です。 そのため、SOLID の原則に従う必要があります。 悪い抽象化はコードの重複よりも悪い場合があるので注意してください! とは言うものの、あなたが良い抽象化をすることができるのであれば、是非行うべきです! 同じことをしないでください、そうしなければ何か一つを変更したい時、複数の場所を変更することになってしまいます。
Bad:
function showDeveloperList(developers: Developer[]) {
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: Manager[]) {
managers.forEach((manager) => {
const expectedSalary = manager.calculateExpectedSalary();
const experience = manager.getExperience();
const portfolio = manager.getMBAProjects();
const data = {
expectedSalary,
experience,
portfolio
};
render(data);
});
}
Good:
class Developer {
// ...
getExtraDetails() {
return {
githubLink: this.githubLink,
}
}
}
class Manager {
// ...
getExtraDetails() {
return {
portfolio: this.portfolio,
}
}
}
function showEmployeeList(employee: Developer | Manager) {
employee.forEach((employee) => {
const expectedSalary = employee.calculateExpectedSalary();
const experience = employee.getExperience();
const extra = employee.getExtraDetails();
const data = {
expectedSalary,
experience,
extra,
};
render(data);
});
}
<!--
You should be critical about code duplication. Sometimes there is a tradeoff between duplicated code and increased complexity by introducing unnecessary abstraction. When two implementations from two different modules look similar but live in different domains, duplication might be acceptable and preferred over extracting the common code. The extracted common code in this case introduces an indirect dependency between the two modules.
-->
あなたはコードの重複について批判的であるべきです。 不必要な抽象化を導入することによって、コードの重複と複雑さの増大との間にトレードオフがあることがあります。 2つの異なるモジュールから呼ばれている2つの実装が似ているように見えても、異なるドメインに存在する場合は重複が許容され、共通コードの抽出よりも優先される場合があります。 この時に抽出された共通コードは、2つのモジュールの間に間接的な依存関係をもたらします。
Set default objects with Object.assign or destructuring
Object.assign や 分割代入を使ってデフォルトオブジェクトを設定する
Bad:
type MenuConfig = { title?: string, body?: string, buttonText?: string, cancellable?: boolean };
function createMenu(config: MenuConfig) {
config.title = config.title || 'Foo';
config.body = config.body || 'Bar';
config.buttonText = config.buttonText || 'Baz';
config.cancellable = config.cancellable !== undefined ? config.cancellable : true;
// ...
}
createMenu({ body: 'Bar' });
Good:
type MenuConfig = { title?: string, body?: string, buttonText?: string, cancellable?: boolean };
function createMenu(config: MenuConfig) {
const menuConfig = Object.assign({
title: 'Foo',
body: 'Bar',
buttonText: 'Baz',
cancellable: true
}, config);
// ...
}
createMenu({ body: 'Bar' });
<!--
Alternatively, you can use destructuring with default values:
-->
デフォルト値を利用した分割代入を使う手もあるでしょう:
type MenuConfig = { title?: string, body?: string, buttonText?: string, cancellable?: boolean };
function createMenu({ title = 'Foo', body = 'Bar', buttonText = 'Baz', cancellable = true }: MenuConfig) {
// ...
}
createMenu({ body: 'Bar' });
<!--
To avoid any side effects and unexpected behavior by passing in explicitly the `undefined` or `null` value, you can tell the TypeScript compiler to not allow it.
See [`--strictNullChecks`](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-0.html#--strictnullchecks) option in TypeScript.
-->
明示的に undefined
や null
の値を渡して副作用や予期しない動作を避けるため、TypeScriptコンパイラにそれを許容させない設定もできます。
TypeScriptの --strictNullChecks
設定を参照
Don't use flags as function parameters
関数の引数にフラグは渡さない
<!-- Flags tell your user that this function does more than one thing. Functions should do one thing. Split out your functions if they are following different code paths based on a boolean. -->フラグは、この関数が複数の事をしていと利用者に伝えています。 関数は一つのことをするべきなので、Boolean値によって異なる処理をしている場合は関数を別にします。
Bad:
function createFile(name: string, temp: boolean) {
if (temp) {
fs.create(`./temp/${name}`);
} else {
fs.create(name);
}
}
Good:
function createTempFile(name: string) {
createFile(`./temp/${name}`);
}
function createFile(name: string) {
fs.create(name);
}
Avoid Side Effects (part 1)
副作用を避ける(その1)
<!-- A function produces a side effect if it does anything other than take a value in and return another value or values. A side effect could be writing to a file, modifying some global variable, or accidentally wiring all your money to a stranger. Now, you do need to have side effects in a program on occasion. Like the previous example, you might need to write to a file. What you want to do is to centralize where you are doing this. Don't have several functions and classes that write to a particular file. Have one service that does it. One and only one. The main point is to avoid common pitfalls like sharing state between objects without any structure, using mutable data types that can be written to by anything, and not centralizing where your side effects occur. If you can do this, you will be happier than the vast majority of other programmers. -->関数が値を受け取り何かを返す以外の事をした場合、副作用を引き起こします。 副作用とはファイルへの書き込みや、グローバル変数の変更、間違って全財産を見知らぬ他人に振り込んでしまうような事です。
前に上げた例のように、ファイルに書き込む必要があるかもしれません。 やるべきことは、それを行う場所を一つの場所に留めることです。 特定のファイルに書き込む関数やクラスが複数存在しないようにしてください。 それをする、唯一無二のサービスを作ります。
重要なのは、構造を持たずオブジェクト間で状態を共有したり、任意のものに書き込み可能な可変データ型を使ったり、副作用が発生する場所を一箇所にしないなどの、一般的な落とし穴を避けることです。 貴方がこれをできるようになれば、大多数の他のプログラマーより幸せになれるでしょう。
Bad:
// Global variable referenced by following function.
// 以下の関数で利用されるグローバル変数
let name = 'Robert C. Martin';
function toBase64() {
name = btoa(name);
}
toBase64();
// If we had another function that used this name, now it'd be a Base64 value
// 変数 name を使用した別の関数がある場合は、その中身はBase64になります。
console.log(name); // expected to print 'Robert C. Martin' but instead 'Um9iZXJ0IEMuIE1hcnRpbg=='
// 'Robert C. Martin' と表示されるはずが、'Um9iZXJ0IEMuIE1hcnRpbg=='と表示されてしまう
Good:
const name = 'Robert C. Martin';
function toBase64(text: string): string {
return btoa(text);
}
const encodedName = toBase64(name);
console.log(name);
Avoid Side Effects (part 2)
副作用を避ける(その2)
<!-- In JavaScript, primitives are passed by value and objects/arrays are passed by reference. In the case of objects and arrays, if your function makes a change in a shopping cart array, for example, by adding an item to purchase, then any other function that uses that `cart` array will be affected by this addition. That may be great, however it can be bad too. Let's imagine a bad situation: The user clicks the "Purchase", button which calls a `purchase` function that spawns a network request and sends the `cart` array to the server. Because of a bad network connection, the purchase function has to keep retrying the request. Now, what if in the meantime the user accidentally clicks "Add to Cart" button on an item they don't actually want before the network request begins? If that happens and the network request begins, then that purchase function will send the accidentally added item because it has a reference to a shopping cart array that the `addItemToCart` function modified by adding an unwanted item. A great solution would be for the `addItemToCart` to always clone the `cart`, edit it, and return the clone. This ensures that no other functions that are holding onto a reference of the shopping cart will be affected by any changes. Two caveats to mention to this approach: 1. There might be cases where you actually want to modify the input object, but when you adopt this programming practice you will find that those cases are pretty rare. Most things can be refactored to have no side effects! (see [pure function](https://en.wikipedia.org/wiki/Pure_function)) 2. Cloning big objects can be very expensive in terms of performance. Luckily, this isn't a big issue in practice because there are great libraries that allow this kind of programming approach to be fast and not as memory intensive as it would be for you to manually clone objects and arrays. -->JavaScriptではプリミティブ型は値で渡され、オブジェクトや配列は参照によって渡されます。 オブジェクトや配列の場合、商品を購入するなどしてショッピングカート配列を更新した場合、ショッピングカート配列を使用する他のすべての関数が追加の影響を受けてしまいます。 それは素晴らしく思えますが、悪いことも起こります。 悪い状況を想像してみましょう。
ユーザーが"購入"ボタンをクリックすると、ネットワークのリクエストを生成してカート配列をサーバーに送信する、purchase 関数が呼ばれます。 ネットワークの状況が悪いため、purchase 関数は要求を繰り返し続けなければいけません。 そして、ネットワークのリクエストが成功する前にユーザーがほしくないアイテムを"カートに入れる"ボタンを誤って押してしまったらどうなるでしょうか? ネットワークのリクエストが成功した時 addItemToCart 関数はカート配列への参照を持っていたため、 不要なアイテムをカート配列へ追加してしまいます、purchase 関数は間違って追加された商品を送信してしまいます。
良い解決策は addItemToCart 関数が常にカートのクローンを作成し、それを編集してさらにクローンを返すことです。 これで、ショッピングカート配列の参照を保持している他の関数が変更の影響を受けなくなります。
このアプローチにおける2つの注意点:
-
時には渡されたオブジェクト型を変更したい場合があるケースがありますが、このプログラミング手法を採用している場合にそういったケースは稀ということに気づくでしょう。 そして殆どの場合、副作用がないようなリファクタリングを行うことが可能です。 (純粋関数を参照)
-
大きなオブジェクトの複製を作成すると、パフォーマンスの面で非常に高コストになってしまう可能性があります。幸運なことに、これは実際には大きな問題ではありません。 なぜなら、手動でオブジェクトや配列を複製するのとは異なり、この種のプログラミング手法をより高速でメモリ使用量を抑えた素晴らしいライブラリがあるからです。
Bad:
function addItemToCart(cart: CartItem[], item: Item): void {
cart.push({ item, date: Date.now() });
};
Good:
function addItemToCart(cart: CartItem[], item: Item): CartItem[] {
return [...cart, { item, date: Date.now() }];
};
Don't write to global functions
グローバル関数には書き込まない
<!-- Polluting globals is a bad practice in JavaScript because you could clash with another library and the user of your API would be none-the-wiser until they get an exception in production. Let's think about an example: what if you wanted to extend JavaScript's native Array method to have a `diff` method that could show the difference between two arrays? You could write your new function to the `Array.prototype`, but it could clash with another library that tried to do the same thing. What if that other library was just using `diff` to find the difference between the first and last elements of an array? This is why it would be much better to just use classes and simply extend the `Array` global. -->グローバルを汚染するのはJavaScriptのバッドプラクティスです。
なぜなら他のライブラリをクラッシュさせるかもしれないし、あなたのAPIを使ってるユーザーは本番環境で例外が発生するまで何が起こってるか分からないからです。
例を考えてみましょう。
2つの配列の違いを差分を出すdiffメソッドを、既存JavaScriptのArrayに追加したらどうなりますか?
新しい関数を Array.prototype
に書くことはできますが、同じことをしている他のライブラリと衝突する可能性があります。
他のライブラリが単に配列の最初の要素と最後の要素の違いを見つけるために diff
を使っていたとしたらどうなるでしょうか。
これが、グローバルのArray
を拡張するより class
を使ったほうが良い理由です。
Bad:
declare global {
interface Array<T> {
diff(other: T[]): Array<T>;
}
}
if (!Array.prototype.diff) {
Array.prototype.diff = function <T>(other: T[]): T[] {
const hash = new Set(other);
return this.filter(elem => !hash.has(elem));
};
}
Good:
class MyArray<T> extends Array<T> {
diff(other: T[]): T[] {
const hash = new Set(other);
return this.filter(elem => !hash.has(elem));
};
}
Favor functional programming over imperative programming
命令形プログラミングより関数型プログラミングを好む
<!-- Favor this style of programming when you can. -->できるなら、あなたはこのプログラミングスタイルを好きになってください。
Bad:
const contributions = [
{
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 < contributions.length; i++) {
totalOutput += contributions[i].linesOfCode;
}
Good:
const contributions = [
{
name: 'Uncle Bobby',
linesOfCode: 500
}, {
name: 'Suzie Q',
linesOfCode: 1500
}, {
name: 'Jimmy Gosling',
linesOfCode: 150
}, {
name: 'Gracie Hopper',
linesOfCode: 1000
}
];
const totalOutput = contributions
.reduce((totalLines, output) => totalLines + output.linesOfCode, 0);
Encapsulate conditionals
条件をカプセル化する
Bad:
if (subscription.isTrial || account.balance > 0) {
// ...
}
Good:
function canActivateService(subscription: Subscription, account: Account) {
return subscription.isTrial || account.balance > 0
}
if (canActivateService(subscription, account)) {
// ...
}
Avoid negative conditionals
否定的な条件を避ける
Bad:
function isEmailNotUsed(email: string): boolean {
// ...
}
if (isEmailNotUsed(email)) {
// ...
}
Good:
function isEmailUsed(email): boolean {
// ...
}
if (!isEmailUsed(node)) {
// ...
}
Avoid conditionals
条件を避ける
<!-- This seems like an impossible task. Upon first hearing this, most people say, "how am I supposed to do anything without an `if` statement?" The answer is that you can use polymorphism to achieve the same task in many cases. The second question is usually, "well that's great but why would I want to do that?" The answer is a previous clean code concept we learned: a function should only do one thing. When you have classes and functions that have `if` statements, you are telling your user that your function does more than one thing. Remember, just do one thing. -->これは一見不可能な作業に見えます。 これを聞いた時、ほとんどの人は「if文を使わないとしたらどうすれば良いのか?」と言います。 この答えは、多くの場合、同じタスクをするためにポリモーフィズム(多態性・多様性)を利用するということです。 2つめの質問としてよくあるのは「素晴らしいことだと思うが、なんでそれをする必要があるのか?」といったものです。 この答えは、私達が先に学んだクリーンコードの概念「関数はただ一つのことをするべき」だからです。 貴方のクラスや関数がif文を持っている時、この関数は複数のことを示しています。 関数は唯一つのことをやるということを覚えておいてください。
Bad:
class Airplane {
private type: string;
// ...
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();
default:
throw new Error('Unknown airplane type.');
}
}
private getMaxAltitude(): number {
// ...
}
}
Good:
abstract class Airplane {
protected getMaxAltitude(): number {
// shared logic with subclasses ...
}
// ...
}
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
型チェックを避ける
<!-- TypeScript is a strict syntactical superset of JavaScript and adds optional static type checking to the language. Always prefer to specify types of variables, parameters and return values to leverage the full power of TypeScript features. It makes refactoring more easier. -->TypeScriptはJavaScriptの厳密な構文スーパセットであり、言語へ静的型チェックのオプションを追加します。 TypeScriptの機能を最大限に活用するには、常に変数、パラメータ、戻り値の型を指定する事をおすすめします。 これはリファクタリングをより簡単にします。
Bad:
function travelToTexas(vehicle: Bicycle | Car) {
if (vehicle instanceof Bicycle) {
vehicle.pedal(currentLocation, new Location('texas'));
} else if (vehicle instanceof Car) {
vehicle.drive(currentLocation, new Location('texas'));
}
}
Good:
type Vehicle = Bicycle | Car;
function travelToTexas(vehicle: Vehicle) {
vehicle.move(currentLocation, new Location('texas'));
}
Don't over-optimize
行き過ぎた最適化を避ける
<!-- Modern browsers do a lot of optimization under-the-hood at runtime. A lot of times, if you are optimizing then you are just wasting your time. There are good [resources](https://github.com/petkaantonov/bluebird/wiki/Optimization-killers) for seeing where optimization is lacking. Target those in the meantime, until they are fixed if they can be. -->今のブラウザは実行時に内部で多くの最適化を行っています。 貴方が最適化に多数の時間を費やしてるなら、あなたは時間を無駄にしています。 最適化ができていないところを見るための良い資料があります。 それらが最適化されるまでは、そこだけを最適化の対象にしてください。
Bad:
// On old browsers, each iteration with uncached `list.length` would be costly
// because of `list.length` recomputation. In modern browsers, this is optimized.
// 古いブラウザは、キャッシュされていない `list.length` を使った繰り返しはコストがかかるでしょう
// なぜなら、呼ばれるたびに `list.length` が再計算されるから。 モダンブラウザは最適化されている
for (let i = 0, len = list.length; i < len; i++) {
// ...
}
Good:
for (let i = 0; i < list.length; i++) {
// ...
}
使っていないコードは削除する
<!-- Dead code is just as bad as duplicate code. There's no reason to keep it in your codebase. If it's not being called, get rid of it! It will still be safe in your version history if you still need it. -->使われていないコードは重複したコードと同じくらい悪いものです。 あなたのコードにそれを残しておく理由はありません。 もし参照がないなら取り除きましょう!必要と感じていたとしてもバージョン管理ツールの履歴に残っているだけで十分でしょう。
Bad:
function oldRequestModule(url: string) {
// ...
}
function requestModule(url: string) {
// ...
}
const req = requestModule;
inventoryTracker('apples', req, 'www.inventory-awesome.io');
Good:
function requestModule(url: string) {
// ...
}
const req = requestModule;
inventoryTracker('apples', req, 'www.inventory-awesome.io');
Use iterators and generators
イテレーターとジェネレーターを使う
<!-- Use generators and iterables when working with collections of data used like a stream. There are some good reasons: - decouples the callee from the generator implementation in a sense that callee decides how many items to access - lazy execution, items are streamed on demand - built-in support for iterating items using the `for-of` syntax - iterables allow to implement optimized iterator patterns -->ストリームデータを持つコレクションを扱うときは、ジェネレーターとイテレーターを使ってください。 いくつかの理由があります:
- 呼び出し先でどの項目にアクセスするかを決めさせることで、呼び出し先とジェネレータの実装を切り離します
- 遅延実行、アイテムはユーザーの要求毎にストリーミングされます
for-of
構文を使った組み込みの反復処理をサポートしています- iterables により最適化されたイテレータパターンを実装することがでるようになります
Bad:
function fibonacci(n: number): number[] {
if (n === 1) return [0];
if (n === 2) return [0, 1];
const items: number[] = [0, 1];
while (items.length < n) {
items.push(items[items.length - 2] + items[items.length - 1]);
}
return items;
}
function print(n: number) {
fibonacci(n).forEach(fib => console.log(fib));
}
// Print first 10 Fibonacci numbers.
// フィボナッチ数の最初の10個をprintする
print(10);
Good:
// Generates an infinite stream of Fibonacci numbers.
// The generator doesn't keep the array of all numbers.
// フィボナッチ数の無限のストリームを生成します
// ジェネレータは全数の配列を保持しません
function* fibonacci(): IterableIterator<number> {
let [a, b] = [0, 1];
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
function print(n: number) {
let i = 0;
for (const fib of fibonacci()) {
if (i++ === n) break;
console.log(fib);
}
}
// Print first 10 Fibonacci numbers.
// フィボナッチ数の最初の10個をprintする
print(10);
<!--
There are libraries that allow working with iterables in a similar way as with native arrays, by
chaining methods like `map`, `slice`, `forEach` etc. See [itiriri](https://www.npmjs.com/package/itiriri) for
an example of advanced manipulation with iterables (or [itiriri-async](https://www.npmjs.com/package/itiriri-async) for manipulation of async iterables).
-->
map
、 slice
、 forEach
などのメソッドをチェーンさせることで、ネイティブの配列と同じようなイテラブルを扱うことを可能にするライブラリもあります。
(イテラブルを使用した高度な操作の例についてはitiririを参照、または非同期イテラブルの操作については itiriri-async を参照)
import itiriri from 'itiriri';
function* fibonacci(): IterableIterator<number> {
let [a, b] = [0, 1];
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
itiriri(fibonacci())
.take(10)
.forEach(fib => console.log(fib));
Objects and Data Structures
オブジェクトとデータ構造
Use getters and setters
getter と setter を使う
<!-- TypeScript supports getter/setter syntax. Using getters and setters to access data from objects that encapsulate behavior could be better than simply looking for a property on an object. "Why?" you might ask. Well, here's a list of reasons: - When you want to do more beyond getting an object property, you don't have to look up and change every accessor in your codebase. - Makes adding validation simple when doing a `set`. - Encapsulates the internal representation. - Easy to add logging and error handling when getting and setting. - You can lazy load your object's properties, let's say getting it from a server. -->TypeScriptはgetter/setter構文をサポートしています。 getterとsetterを使って振る舞いをカプセル化してオブジェクトにアクセスするほうが、単純なプロパティでオブジェクトにアクセスするよりも優れている可能性があります。 「何故?」と思われるかもしれませんが、以下がその理由の一覧です:
- もしオブジェクトのプロパティを取得する以上のことをしてる場合、コード内のすべてのアクセサを調べて変更する必要がありません。
- set を使うとバリデーションが追加できます。
- 内部をカプセル化できます。
- 値を取得や設定する時にログやエラー処理を追加するのが容易になります。
- オブジェクトのプロパティを遅延ロードすることができるようになります、例えばサーバから値を取得する時などです。
Bad:
type BankAccount = {
balance: number;
// ...
}
const value = 100;
const account: BankAccount = {
balance: 0,
// ...
};
if (value < 0) {
throw new Error('Cannot set negative balance.');
}
account.balance = value;
Good:
class BankAccount {
private accountBalance: number = 0;
get balance(): number {
return this.accountBalance;
}
set balance(value: number) {
if (value < 0) {
throw new Error('Cannot set negative balance.');
}
this.accountBalance = value;
}
// ...
}
// Now `BankAccount` encapsulates the validation logic.
// If one day the specifications change, and we need extra validation rule,
// we would have to alter only the `setter` implementation,
// leaving all dependent code unchanged.
// これで `BankAccount` はバリデーション処理をカプセル化しました。
// ある日仕様が変更されて、追加のバリデーションが必要になった場合にも
// `setter`の実装だけを変更すればよく
// すべての依存したコードを変更する必要はありません。
const account = new BankAccount();
account.balance = 100;
Make objects have private/protected members
オブジェクトにprivate/protectedメンバーをもたせる
<!-- TypeScript supports `public` *(default)*, `protected` and `private` accessors on class members. -->TypeScriptはクラスメンバーに対してpublic
、(default)、protected
、private
アクセサをサポートしています。
Bad:
class Circle {
radius: number;
constructor(radius: number) {
this.radius = radius;
}
perimeter() {
return 2 * Math.PI * this.radius;
}
surface() {
return Math.PI * this.radius * this.radius;
}
}
Good:
class Circle {
constructor(private readonly radius: number) {
}
perimeter() {
return 2 * Math.PI * this.radius;
}
surface() {
return Math.PI * this.radius * this.radius;
}
}
Prefer immutability
不変性を好む
<!-- TypeScript's type system allows you to mark individual properties on an interface / class as *readonly*. This allows you to work in a functional way (unexpected mutation is bad). For more advanced scenarios there is a built-in type `Readonly` that takes a type `T` and marks all of its properties as readonly using mapped types (see [mapped types](https://www.typescriptlang.org/docs/handbook/advanced-types.html#mapped-types)). -->TypeScriptの型システムではインタフェース/クラスのプロパティに readonly とマークすることができます。
これを用いると、あなたは関数型で書くことができるようになります。
(予想外の変更は良くないものです)
より高度なシナリオでは、組み込み型の Readonly
があります。これはT
型 と map型を併用してすべてのプロパティを読み取り専用としてマークします。
(マップ型を参照)
Bad:
interface Config {
host: string;
port: string;
db: string;
}
Good:
interface Config {
readonly host: string;
readonly port: string;
readonly db: string;
}
<!--
Case of Array, you can create a read-only array by using `ReadonlyArray<T>`.
do not allow changes such as `push()` and `fill()`, but can use features such as `concat()` and `slice()` that do not change the value.
-->
Arrayの場合にはReadonlyArray<T>
を使って読み取り専用の配列を作ることができます。
push()
や fill()
のような変更はできませんが、値を変えない concat()
や slice()
と言った機能は使うことができます。
Bad:
const array: number[] = [ 1, 3, 5 ];
array = []; // error
array.push(100); // array will updated
Good:
const array: ReadonlyArray<number> = [ 1, 3, 5 ];
array = []; // error
array.push(100); // error
<!--
Declaring read-only arguments in [TypeScript 3.4 is a bit easier](https://github.com/microsoft/TypeScript/wiki/What's-new-in-TypeScript#improvements-for-readonlyarray-and-readonly-tuples).
-->
TypeScript 3.4で読み取り専用引数を宣言するのが少し簡単になりました。
function hoge(args: readonly string[]) {
args.push(1); // error
}
<!--
Prefer [const assertions](https://github.com/microsoft/TypeScript/wiki/What's-new-in-TypeScript#const-assertions) for literal values.
-->
リテラル値にはconst assertionsを好んで使うと良いでしょう。
Bad:
const config = {
hello: 'world'
};
config.hello = 'world'; // value is changed
const array = [ 1, 3, 5 ];
array[0] = 10; // value is changed
// writable objects is returned
function readonlyData(value: number) {
return { value };
}
const result = readonlyData(100);
result.value = 200; // value is changed
Good:
// read-only object
const config = {
hello: 'world'
} as const;
config.hello = 'world'; // error
// read-only array
const array = [ 1, 3, 5 ] as const;
array[0] = 10; // error
// You can return read-only objects
function readonlyData(value: number) {
return { value } as const;
}
const result = readonlyData(100);
result.value = 200; // error
type vs. interface
タイプ VS インタフェース
<!-- Use type when you might need a union or intersection. Use interface when you want `extends` or `implements`. There is no strict rule however, use the one that works for you. For a more detailed explanation refer to this [answer](https://stackoverflow.com/questions/37233735/typescript-interfaces-vs-types/54101543#54101543) about the differences between `type` and `interface` in TypeScript. -->union や intersection が必要な場合は type を使用してください。
extends や implements がほしいときにはinterfaceを使います。
厳密なルールはありませんが、その時に合ったものを使ってください。
TypeScriptの type
と interface
の違いについてのより詳細な説明はこちらの 回答 を参照してください。
Bad:
interface EmailConfig {
// ...
}
interface DbConfig {
// ...
}
interface Config {
// ...
}
//...
type Shape = {
// ...
}
Good:
type EmailConfig = {
// ...
}
type DbConfig = {
// ...
}
type Config = EmailConfig | DbConfig;
// ...
interface Shape {
// ...
}
class Circle implements Shape {
// ...
}
class Square implements Shape {
// ...
}
Classes
Classes should be small
クラスは小さくあるべき
<!-- The class' size is measured by its responsibility. Following the *Single Responsibility principle* a class should be small. -->責任の範囲によってクラスの大きさを定めます。単一責任の原則 に従ってクラスは小さくなければいけません。
Bad:
class Dashboard {
getLanguage(): string { /* ... */ }
setLanguage(language: string): void { /* ... */ }
showProgress(): void { /* ... */ }
hideProgress(): void { /* ... */ }
isDirty(): boolean { /* ... */ }
disable(): void { /* ... */ }
enable(): void { /* ... */ }
addSubscription(subscription: Subscription): void { /* ... */ }
removeSubscription(subscription: Subscription): void { /* ... */ }
addUser(user: User): void { /* ... */ }
removeUser(user: User): void { /* ... */ }
goToHomePage(): void { /* ... */ }
updateProfile(details: UserDetails): void { /* ... */ }
getVersion(): string { /* ... */ }
// ...
}
Good:
class Dashboard {
disable(): void { /* ... */ }
enable(): void { /* ... */ }
getVersion(): string { /* ... */ }
}
// split the responsibilities by moving the remaining methods to other classes
// 他のクラスにメソッドを移動して責任を分割する
// ...
High cohesion and low coupling
高凝集度と疎結合性
<!-- Cohesion defines the degree to which class members are related to each other. Ideally, all fields within a class should be used by each method. We then say that the class is *maximally cohesive*. In practice, this however is not always possible, nor even advisable. You should however prefer cohesion to be high. -->凝集度は、クラスメンバーが互いにどの程度関連しているかを定義します。理想的には、プログラム内の全てのフィールドがクラスの各メソッドを使用できることです。 このような状態を、クラスは 凝集度が高い と言います。ただ、実際これは常に可能ではありませんが、可能な限り凝集度を高くすることが賢明といえます。
<!-- Coupling refers to how related or dependent are two classes toward each other. Classes are said to be low coupled if changes in one of them doesn't affect the other one. -->結合度とは、2つのクラスが互いにどの程度依存しているかを指します。片方の変更がもう片方に影響しない場合、互いのクラスは疎結合と言われます。
<!-- Good software design has **high cohesion** and **low coupling**. -->優れたソフトウェア設計は、 高い凝集度 と 低い結合度 を内包しています。
Bad:
class UserManager {
// Bad: each private variable is used by one or another group of methods.
// It makes clear evidence that the class is holding more than a single responsibility.
// If I need only to create the service to get the transactions for a user,
// I'm still forced to pass and instance of `emailSender`.
// 悪い: 各プライベート変数は、メソッドの1つまたは別のグループによって使用されます。
// それは、クラスが1つ以上の責任を保持しているという明確な証拠を表しています。
// ユーザーのトランザクションを取得するためだけにサービスを作成する必要がある場合は、
// `emailSender` のインスタンスを渡すことを余儀なくされています。
constructor(
private readonly db: Database,
private readonly emailSender: EmailSender) {
}
async getUser(id: number): Promise<User> {
return await db.users.findOne({ id });
}
async getTransactions(userId: number): Promise<Transaction[]> {
return await db.transactions.find({ userId });
}
async sendGreeting(): Promise<void> {
await emailSender.send('Welcome!');
}
async sendNotification(text: string): Promise<void> {
await emailSender.send(text);
}
async sendNewsletter(): Promise<void> {
// ...
}
}
Good:
class UserService {
constructor(private readonly db: Database) {
}
async getUser(id: number): Promise<User> {
return await this.db.users.findOne({ id });
}
async getTransactions(userId: number): Promise<Transaction[]> {
return await this.db.transactions.find({ userId });
}
}
class UserNotifier {
constructor(private readonly emailSender: EmailSender) {
}
async sendGreeting(): Promise<void> {
await this.emailSender.send('Welcome!');
}
async sendNotification(text: string): Promise<void> {
await this.emailSender.send(text);
}
async sendNewsletter(): Promise<void> {
// ...
}
}
Prefer composition over inheritance
インヘリタンス(継承)よりコンポジション(合成集約)を好む
<!-- As stated famously in [Design Patterns](https://en.wikipedia.org/wiki/Design_Patterns) by the Gang of Four, you should *prefer composition over inheritance* where you can. There are lots of good reasons to use inheritance and lots of good reasons to use composition. The main point for this maxim is that if your mind instinctively goes for inheritance, try to think if composition could model your problem better. In some cases it can. -->有名なGang of Four(四人組)によるデザインパターンのように、 可能な場所では継承よりも合成集約を優先するべきです。 継承を利用する良い理由も合成集約を利用する良い理由もたくさんあります。 この章の要点は、もしあなたが本能的に継承を使うように心が動くのであれば、合成集約がその問題をよりよくモデル化できるかどうか考えてみてください。 いくつかの場合、それができます。
<!-- You might be wondering then, "when should I use inheritance?" It depends on your problem at hand, but this is a decent list of when inheritance makes more sense than composition: -->あなたは、「いつ継承を使うべきか?」について疑問に思うかもしれません。 それは、あなたの持つ問題次第です。ただ、これは、継承が合成集約よりも理にかなってる場合の一覧です。
<!-- 1. Your inheritance represents an "is-a" relationship and not a "has-a" relationship (Human->Animal vs. User->UserDetails). 2. You can reuse code from the base classes (Humans can move like all animals). 3. You want to make global changes to derived classes by changing a base class. (Change the caloric expenditure of all animals when they move). -->-
継承が「has-a」ではなくて「is-a」を表している場合(人間は動物である と 人は属性情報を含んでいる)
-
基底クラスからコードを再利用できる(人は動物のように動くことができる)
-
基底クラスを変更することで、派生クラスを全体的に変更したい(全ての動物の移動中の消費カロリーを変更する)
Bad:
class Employee {
constructor(
private readonly name: string,
private readonly email: string) {
}
// ...
}
// Bad because Employees "have" tax data. EmployeeTaxData is not a type of Employee
// よくない。なぜなら、従業員(Employee)は税情報を持っている。しかし、従業員税情報(EmployeeTaxData)は従業員ではない
class EmployeeTaxData extends Employee {
constructor(
name: string,
email: string,
private readonly ssn: string,
private readonly salary: number) {
super(name, email);
}
// ...
}
Good:
class Employee {
private taxData: EmployeeTaxData;
constructor(
private readonly name: string,
private readonly email: string) {
}
setTaxData(ssn: string, salary: number): Employee {
this.taxData = new EmployeeTaxData(ssn, salary);
return this;
}
// ...
}
class EmployeeTaxData {
constructor(
public readonly ssn: string,
public readonly salary: number) {
}
// ...
}
Use method chaining
メソッドチェーンを利用すること
<!-- This pattern is very useful and commonly used in many libraries. It allows your code to be expressive, and less verbose. For that reason, use method chaining and take a look at how clean your code will be. -->このパターンは非常に便利なので、多くのライブラリでよく使われています。これにより、貴方のコードは表現力豊かで、冗長ではなくなります。というわけで、メソッドチェーンを使って、あなたのコードがどれくらい綺麗になるか見てください。
Bad:
class QueryBuilder {
private collection: string;
private pageNumber: number = 1;
private itemsPerPage: number = 100;
private orderByFields: string[] = [];
from(collection: string): void {
this.collection = collection;
}
page(number: number, itemsPerPage: number = 100): void {
this.pageNumber = number;
this.itemsPerPage = itemsPerPage;
}
orderBy(...fields: string[]): void {
this.orderByFields = fields;
}
build(): Query {
// ...
}
}
// ...
const queryBuilder = new QueryBuilder();
queryBuilder.from('users');
queryBuilder.page(1, 100);
queryBuilder.orderBy('firstName', 'lastName');
const query = queryBuilder.build();
Good:
class QueryBuilder {
private collection: string;
private pageNumber: number = 1;
private itemsPerPage: number = 100;
private orderByFields: string[] = [];
from(collection: string): this {
this.collection = collection;
return this;
}
page(number: number, itemsPerPage: number = 100): this {
this.pageNumber = number;
this.itemsPerPage = itemsPerPage;
return this;
}
orderBy(...fields: string[]): this {
this.orderByFields = fields;
return this;
}
build(): Query {
// ...
}
}
// ...
const query = new QueryBuilder()
.from('users')
.page(1, 100)
.orderBy('firstName', 'lastName')
.build();
SOLID
Single Responsibility Principle (SRP)
単一責任の原則 (SRP)
<!-- 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. -->Clean Code では「クラス変更される理由は1つ以上あってはならない」と述べられています。 飛行機へ乗る時にスーツケースを一つしか持てないように、多くの機能をクラスに詰め込むのは魅力的に見えます。 これに関する問題は、あなたのクラスが概念的にまとまりがなく、変化を許容する理由をたくさん持ってしまうということです。 クラスを変更する理由を最小限に留めることが重要になります。 ここが重要な理由は、1つのクラスにたくさんの機能がある場合に、その一部を変更した時、変更箇所がコードベース内の他の依存モジュールにどう影響するのかを理解するのが難しくなってしまうからです。
Bad:
class UserSettings {
constructor(private readonly user: User) {
}
changeSettings(settings: UserSettings) {
if (this.verifyCredentials()) {
// ...
}
}
verifyCredentials() {
// ...
}
}
Good:
class UserAuth {
constructor(private readonly user: User) {
}
verifyCredentials() {
// ...
}
}
class UserSettings {
private readonly auth: UserAuth;
constructor(private readonly user: User) {
this.auth = new UserAuth(user);
}
changeSettings(settings: UserSettings) {
if (this.auth.verifyCredentials()) {
// ...
}
}
}
Open/Closed Principle (OCP)
オープン・クローズドの原則 (OCP)
<!-- 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. -->Bertrand Meyer 氏曰く、「ソフトウェアの関連(クラス、モジュール、関数など)は拡張のために開かれているべきですが、変更のためには閉じられているべき」と言われています。 これはどういう意味かというと、この原則は基本的に、既存のコードを変更せずにユーザーが新しい機能を追加できるようにするべきと言う意味です。
Bad:
class AjaxAdapter extends Adapter {
constructor() {
super();
}
// ...
}
class NodeAdapter extends Adapter {
constructor() {
super();
}
// ...
}
class HttpRequester {
constructor(private readonly adapter: Adapter) {
}
async fetch<T>(url: string): Promise<T> {
if (this.adapter instanceof AjaxAdapter) {
const response = await makeAjaxCall<T>(url);
// transform response and return
} else if (this.adapter instanceof NodeAdapter) {
const response = await makeHttpCall<T>(url);
// transform response and return
}
}
}
function makeAjaxCall<T>(url: string): Promise<T> {
// request and return promise
}
function makeHttpCall<T>(url: string): Promise<T> {
// request and return promise
}
Good:
abstract class Adapter {
abstract async request<T>(url: string): Promise<T>;
// code shared to subclasses ...
}
class AjaxAdapter extends Adapter {
constructor() {
super();
}
async request<T>(url: string): Promise<T>{
// request and return promise
}
// ...
}
class NodeAdapter extends Adapter {
constructor() {
super();
}
async request<T>(url: string): Promise<T>{
// request and return promise
}
// ...
}
class HttpRequester {
constructor(private readonly adapter: Adapter) {
}
async fetch<T>(url: string): Promise<T> {
const response = await this.adapter.request<T>(url);
// transform response and return
}
}
Liskov Substitution Principle (LSP)
リスコフの置換原則 (LSP)
<!-- 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 parent 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. -->これは非常に単純な概念と言うには恐ろしいものです。 S型がT型の派生型の場合、T型のオブジェクトはプログラムの特性(正確さや処理など)を変更すること無く、S型で置き換えることができる(つまり、S型のオブジェクトをT型のオブジェクトと置き換えることができる)というやっかいな定義です。
これに対する良い説明は、親クラスと子クラスがある場合に、親クラスと子クラスは異なる振る舞いを起こさず、互換性を持っているということです。 まだ難しいと思うので、古典的な正方形と長方形の例を見てみましょう。 数学的には正方形は長方形ですが、継承による「is-a」関係を使いモデル化すると、問題が発生しやすくなります。
Bad:
class Rectangle {
constructor(
protected width: number = 0,
protected height: number = 0) {
}
setColor(color: string): this {
// ...
}
render(area: number) {
// ...
}
setWidth(width: number): this {
this.width = width;
return this;
}
setHeight(height: number): this {
this.height = height;
return this;
}
getArea(): number {
return this.width * this.height;
}
}
class Square extends Rectangle {
setWidth(width: number): this {
this.width = width;
this.height = width;
return this;
}
setHeight(height: number): this {
this.width = height;
this.height = height;
return this;
}
}
function renderLargeRectangles(rectangles: Rectangle[]) {
rectangles.forEach((rectangle) => {
const area = rectangle
.setWidth(4)
.setHeight(5)
.getArea(); // BAD: Returns 25 for Square. Should be 20.
rectangle.render(area);
});
}
const rectangles = [new Rectangle(), new Rectangle(), new Square()];
renderLargeRectangles(rectangles);
Good:
abstract class Shape {
setColor(color: string): this {
// ...
}
render(area: number) {
// ...
}
abstract getArea(): number;
}
class Rectangle extends Shape {
constructor(
private readonly width = 0,
private readonly height = 0) {
super();
}
getArea(): number {
return this.width * this.height;
}
}
class Square extends Shape {
constructor(private readonly length: number) {
super();
}
getArea(): number {
return this.length * this.length;
}
}
function renderLargeShapes(shapes: Shape[]) {
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)
インタフェース分離の原則 (ISP)
<!-- ISP states that "Clients should not be forced to depend upon interfaces that they do not use.". This principle is very much related to the Single Responsibility Principle. What it really means is that you should always design your abstractions in a way that the clients that are using the exposed methods do not get the whole pie instead. That also include imposing the clients with the burden of implementing methods that they don’t actually need. -->ISPでは「クライアントは彼らが利用していないインタフェースへの依存を強要してはならない」と述べられています。 この原則は、単一責任原則と非常に密接な関係にあります。 これが本当に意味することは、公開されたメソッドを使用しているクライアントが、すべてのパイを取得しないよう抽象化を常に設計する必要があるという事です。 これは実際には必要としないメソッドを実装するという負荷をクライアント側に課す事も含まれます。
Bad:
interface SmartPrinter {
print();
fax();
scan();
}
class AllInOnePrinter implements SmartPrinter {
print() {
// ...
}
fax() {
// ...
}
scan() {
// ...
}
}
class EconomicPrinter implements SmartPrinter {
print() {
// ...
}
fax() {
throw new Error('Fax not supported.');
}
scan() {
throw new Error('Scan not supported.');
}
}
Good:
interface Printer {
print();
}
interface Fax {
fax();
}
interface Scanner {
scan();
}
class AllInOnePrinter implements Printer, Fax, Scanner {
print() {
// ...
}
fax() {
// ...
}
scan() {
// ...
}
}
class EconomicPrinter implements Printer {
print() {
// ...
}
}
Dependency Inversion Principle (DIP)
依存関係逆転の原則 (DIP)
<!-- This principle states two essential things: 1. High-level modules should not depend on low-level modules. Both should depend on abstractions. 2. Abstractions should not depend upon details. Details should depend on abstractions. This can be hard to understand at first, but if you've worked with Angular, 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. DIP is usually achieved by a using an inversion of control (IoC) container. An example of a powerful IoC container for TypeScript is [InversifyJs](https://www.npmjs.com/package/inversify) -->この原則では2つの重要な事を述べています:
-
上位モジュールは下位モジュールに依存してはいけません、どちらも抽象化に依存するべきです。
-
抽象化は実装に依存してはいけません、実装は抽象化に依存するべきです。
最初にこれを理解するのは難しいかもしれませんが Angularで作業したことがある場合はこの原則が依存性注入(DI)の形で実装されるのを見たことがあるでしょう。 それは同一の概念ではありませんが、DIPは高レベルのモジュールが低レベルのモジュールの詳細を知るが、それらが設定されるのを防ぎます。 これはDIで成し遂げることが出来ます。 これはモジュール間の結合を減らすことが大きなメリットになります。 密結合はコードのリファクタリングを困難にするので、非常に良くない開発パターンです。
Bad:
import { readFile as readFileCb } from 'fs';
import { promisify } from 'util';
const readFile = promisify(readFileCb);
type ReportData = {
// ..
}
class XmlFormatter {
parse<T>(content: string): T {
// Converts an XML string to an object T
// XML文字列をTオブジェクトに変換します。
}
}
class ReportReader {
// BAD: We have created a dependency on a specific request implementation.
// We should just have ReportReader depend on a parse method: `parse`
// 悪い:特定のリクエストに依存する実装を作りました。
// ReportReaderを parseメソッドに依存させています:`parse`
private readonly formatter = new XmlFormatter();
async read(path: string): Promise<ReportData> {
const text = await readFile(path, 'UTF8');
return this.formatter.parse<ReportData>(text);
}
}
// ...
const reader = new ReportReader();
await report = await reader.read('report.xml');
Good:
import { readFile as readFileCb } from 'fs';
import { promisify } from 'util';
const readFile = promisify(readFileCb);
type ReportData = {
// ..
}
interface Formatter {
parse<T>(content: string): T;
}
class XmlFormatter implements Formatter {
parse<T>(content: string): T {
// Converts an XML string to an object T
// XML文字列をTオブジェクトに変換します。
}
}
class JsonFormatter implements Formatter {
parse<T>(content: string): T {
// Converts a JSON string to an object T
// json文字列をTオブジェクトに変換します。
}
}
class ReportReader {
constructor(private readonly formatter: Formatter) {
}
async read(path: string): Promise<ReportData> {
const text = await readFile(path, 'UTF8');
return this.formatter.parse<ReportData>(text);
}
}
// ...
const reader = new ReportReader(new XmlFormatter());
await report = await reader.read('report.xml');
// or if we had to read a json report
// JSONレポートを読む場合
const reader = new ReportReader(new JsonFormatter());
await report = await reader.read('report.json');
Testing
テスト
<!-- 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](https://github.com/gotwarlost/istanbul). There's no excuse to not write tests. There are [plenty of good JS test frameworks](http://jstherightway.org/#testing-tools) with typings support for TypeScript, 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. -->テストはリリースよりも重要です。 あなたのテストをしていないか、不十分であった場合、あなたがリリースするたびに何も壊していないという確信を得ることは無いでしょう。 何が適切な量を構成するのは貴方のチーム次第ですが、カバレッジ100%(すべてのステートメントと分岐を含め)にするということは、非常に高い信頼性とともに、開発者の安心を持つことが出来ます。 つまり、優れたテストフレームワークとカバレッジツールを使う必要があります。
テストを書かない理由はありません。 TypeScriptの型をサポートする優れたJSテストフレームワークはたくさんあるので、チームが好むものを見つけてください。 自分のチームに合ったものが見つかったら導入するすべての新しい機能やモジュールに対して常にテストを書くことを目標にします。 もし貴方がテスト駆動開発(TDD)であるなら素晴らしいでしょう。 しかし、重要なのは機能を動かす前や既存の機能をリファクタする前にカバレッジの目標を達成していることを確実にすることです。
The three laws of TDD
<!-- 1. You are not allowed to write any production code unless it is to make a failing unit test pass. 2. You are not allowed to write any more of a unit test than is sufficient to fail; and compilation failures are failures. 3. You are not allowed to write any more production code than is sufficient to pass the one failing unit test. -->TDD3つの法則
-
失敗した単体テストに合格しない限り、実コードを書くことはできません。
-
失敗させるためにしか単体テストは書いてはいけません。コンパイルエラーは失敗に数えます。
-
単体テストを1つだけ成功させる以上に、 プロダクトコードを書いてはならない。
F.I.R.S.T. rules
F.I.R.S.T. の規則
<!-- Clean tests should follow the rules: - **Fast** tests should be fast because we want to run them frequently. - **Independent** tests should not depend on each other. They should provide same output whether run independently or all together in any order. - **Repeatable** tests should be repeatable in any environment and there should be no excuse for why they fail. - **Self-Validating** a test should answer with either *Passed* or *Failed*. You don't need to compare log files to answer if a test passed. - **Timely** unit tests should be written before the production code. If you write tests after the production code, you might find writing tests too hard. -->綺麗なテストは以下の規則に従います:
-
Fast テストは頻繁に実行したいので早いはずです。
-
Independent テストは互いに依存してはいけません。それらは独立して実行されても任意の順番でまとめて実行されても同じ結果を返します。
-
Repeatable テストはどのような環境でも繰り返し実行可能であるべきで、何故失敗するかについての言い訳をしてはいけません。
-
Self-Validating テストは 合格 と 不合格 のどちらかしかありえません。テストに合格した場合にログファイルを比較して回答したりしないでください。
-
Timely 単体テストはプロダクトコードの前に書かれるべきです。プロダクトコードの後にテストを書くとテストを書くのは難しくなる可能性があります。
Single concept per test
テスト毎に単純なコンセプトを持つ
<!-- Tests should also follow the *Single Responsibility Principle*. Make only one assert per unit test. -->テストは 単一責任原則 に従うべきです。単体テスト毎にアサーションを1つ持ちましょう。
Bad:
import { assert } from 'chai';
describe('AwesomeDate', () => {
it('handles date boundaries', () => {
let date: AwesomeDate;
date = new AwesomeDate('1/1/2015');
assert.equal('1/31/2015', date.addDays(30));
date = new AwesomeDate('2/1/2016');
assert.equal('2/29/2016', date.addDays(28));
date = new AwesomeDate('2/1/2015');
assert.equal('3/1/2015', date.addDays(28));
});
});
Good:
import { assert } from 'chai';
describe('AwesomeDate', () => {
it('handles 30-day months', () => {
const date = new AwesomeDate('1/1/2015');
assert.equal('1/31/2015', date.addDays(30));
});
it('handles leap year', () => {
const date = new AwesomeDate('2/1/2016');
assert.equal('2/29/2016', date.addDays(28));
});
it('handles non-leap year', () => {
const date = new AwesomeDate('2/1/2015');
assert.equal('3/1/2015', date.addDays(28));
});
});
The name of the test should reveal its intention
テストの名前は何をしてるか、理解しやすくしましょう。
<!-- When a test fail, its name is the first indication of what may have gone wrong. -->テストが失敗した場合に、何が間違ってたか気づけるような名前が重要です。
Bad:
describe('Calendar', () => {
it('2/29/2020', () => {
// ...
});
it('throws', () => {
// ...
});
});
Good:
describe('Calendar', () => {
it('should handle leap year', () => {
// ...
});
it('should throw when format is invalid', () => {
// ...
});
});
Concurrency
並行性
Prefer promises vs callbacks
コールバックではなく Promise を使う
<!-- Callbacks aren't clean, and they cause excessive amounts of nesting *(the callback hell)*. There are utilities that transform existing functions using the callback style to a version that returns promises (for Node.js see [`util.promisify`](https://nodejs.org/dist/latest-v8.x/docs/api/util.html#util_util_promisify_original), for general purpose see [pify](https://www.npmjs.com/package/pify), [es6-promisify](https://www.npmjs.com/package/es6-promisify)) -->コールバックは綺麗では無く、過度のネスト(コールバック地獄)を引き起こします。
コールバックスタイルの既存関数をPromiseスタイルに変更するユーティリティがあります。
Node.js に関しては util.promisify
、一般的な目的には pify, es6-promisify
Bad:
import { get } from 'request';
import { writeFile } from 'fs';
function downloadPage(url: string, saveTo: string, callback: (error: Error, content?: string) => void) {
get(url, (error, response) => {
if (error) {
callback(error);
} else {
writeFile(saveTo, response.body, (error) => {
if (error) {
callback(error);
} else {
callback(null, response.body);
}
});
}
});
}
downloadPage('https://en.wikipedia.org/wiki/Robert_Cecil_Martin', 'article.html', (error, content) => {
if (error) {
console.error(error);
} else {
console.log(content);
}
});
Good:
import { get } from 'request';
import { writeFile } from 'fs';
import { promisify } from 'util';
const write = promisify(writeFile);
function downloadPage(url: string, saveTo: string): Promise<string> {
return get(url)
.then(response => write(saveTo, response));
}
downloadPage('https://en.wikipedia.org/wiki/Robert_Cecil_Martin', 'article.html')
.then(content => console.log(content))
.catch(error => console.error(error));
<!--
Promises supports a few helper methods that help make code more conscise:
| Pattern | Description |
| ------------------------ | ----------------------------------------- |
| `Promise.resolve(value)` | Convert a value into a resolved promise. |
| `Promise.reject(error)` | Convert an error into a rejected promise. |
| `Promise.all(promises)` |Returns a new promise which is fulfilled with an array of fulfillment values for the passed promises or rejects with the reason of the first promise that rejects. |
| `Promise.race(promises)`|Returns a new promise which is fulfilled/rejected with the result/error of the first settled promise from the array of passed promises. |
`Promise.all` is especially useful when there is a need to run tasks in parallel. `Promise.race` makes it easier to implement things like timeouts for promises.
-->
Promisesは、コードをより簡潔にするためのヘルパーメソッドをいくつかサポートします。
Pattern | Description |
---|---|
Promise.resolve(value) | 値をpromiseの resolve に変換する |
Promise.reject(error) | エラーをpromiseの reject に変換する |
Promise.all(promises) | 渡されたプロミスの配列がすべて正常に完了するか、一部でrejectとなった場合 Promise を完了させます。 |
Promise.race(promises) | 渡されたプロミスの配列の最初に resolve/rejectされた結果で新しいプロミスを返します。 |
Promise.all
はタスクを並行して実行する必要がある時に特に便利です。
Promise.race
はPromiseにタイムアウトのようなものを実装する時に、それを簡単に実現できます。
Async/Await are even cleaner than Promises
Async/Await はPromisesよりも更に綺麗です
<!-- With `async`/`await` syntax you can write code that is far cleaner and more understandable than chained promises. Within a function prefixed with `async` keyword you have a way to tell the JavaScript runtime to pause the execution of code on the `await` keyword (when used on a promise). -->async
/await
構文を使うと、promiseチェーンを作るよりはるかに綺麗で理解しやすいコードを書くことが出来ます。
async
キーワードを接頭辞とした関数内では (promiseが使用されている場合) await
キーワードでコードの実行を一時停止するようjavascriptランタイムに指示することが出来ます。
Bad:
import { get } from 'request';
import { writeFile } from 'fs';
import { promisify } from 'util';
const write = util.promisify(writeFile);
function downloadPage(url: string, saveTo: string): Promise<string> {
return get(url).then(response => write(saveTo, response));
}
downloadPage('https://en.wikipedia.org/wiki/Robert_Cecil_Martin', 'article.html')
.then(content => console.log(content))
.catch(error => console.error(error));
Good:
import { get } from 'request';
import { writeFile } from 'fs';
import { promisify } from 'util';
const write = promisify(writeFile);
async function downloadPage(url: string, saveTo: string): Promise<string> {
const response = await get(url);
await write(saveTo, response);
return response;
}
// somewhere in an async function
try {
const content = await downloadPage('https://en.wikipedia.org/wiki/Robert_Cecil_Martin', 'article.html');
console.log(content);
} catch (error) {
console.error(error);
}
Error Handling
エラー処理
<!-- 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. -->例外が発生するのは良いことです! これはプログラムの何かが正常に動作しなかった事をランタイムが正常に判別出来たことを意味します。 そして現在のスタックで関数の実行を停止して(Node内の)プロセスを強制終了し、コンソールにスタックトレースを表示して通知します。
Always use Error for throwing or rejecting
ThrowやRejectの時にErrorを使う
<!-- JavaScript as well as TypeScript allow you to `throw` any object. A Promise can also be rejected with any reason object. It is advisable to use the `throw` syntax with an `Error` type. This is because your error might be caught in higher level code with a `catch` syntax. It would be very confusing to catch a string message there and would make [debugging more painful](https://basarat.gitbooks.io/typescript/docs/types/exceptions.html#always-use-error). For the same reason you should reject promises with `Error` types. -->JavaScriptとTypeScriptは任意のオブジェクトを throw
できます。
Promiseの場合は reject
することができます。
Error
型と throw
構文を使うようにしましょう。
これは貴方のエラーが catch
構文を使ってより高いレベルのコードで補足されるかもしれないからです。
その時に文字列でメッセージを出すのはとても混乱を招き、
デバッグをより厄介にするでしょう。
同じ理由で Error
型の reject
を行うべきです。
Bad:
function calculateTotal(items: Item[]): number {
throw 'Not implemented.';
}
function get(): Promise<Item[]> {
return Promise.reject('Not implemented.');
}
Good:
function calculateTotal(items: Item[]): number {
throw new Error('Not implemented.');
}
function get(): Promise<Item[]> {
return Promise.reject(new Error('Not implemented.'));
}
// or equivalent to:
async function get(): Promise<Item[]> {
throw new Error('Not implemented.');
}
<!--
The benefit of using `Error` types is that it is supported by the syntax `try/catch/finally` and implicitly all errors have the `stack` property which
is very powerful for debugging.
There are also another alternatives, not to use the `throw` syntax and instead always return custom error objects. TypeScript makes this even easier.
Consider following example:
-->
Error
型を使う利点は、それが try/catch/finally
構文によってサポートされており、暗黙的に stack
プロパティを持つためデバッグにおいて非常に強力だからです。
throw
構文を使わずに常にカスタムエラーオブジェクトを返すという方法もあります。
TypeScriptはそれを更に簡単にします。
次の例を参考にしてみてください:
type Result<R> = { isError: false, value: R };
type Failure<E> = { isError: true, error: E };
type Failable<R, E> = Result<R> | Failure<E>;
function calculateTotal(items: Item[]): Failable<number, 'empty'> {
if (items.length === 0) {
return { isError: true, error: 'empty' };
}
// ...
return { isError: false, value: 42 };
}
<!--
For the detailed explanation of this idea refer to the [original post](https://medium.com/@dhruvrajvanshi/making-exceptions-type-safe-in-typescript-c4d200ee78e9).
-->
このアイディアの詳細については元の記事を参照してください。
Don't ignore caught errors
例外を握りつぶさない
<!-- 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 (`console.log`) 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 `try/catch` 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);
}
// or even worse
try {
functionThatMightThrow();
} catch (error) {
// ignore error
}
Good:
import { logger } from './logging'
try {
functionThatMightThrow();
} catch (error) {
logger.log(error);
}
Don't ignore rejected promises
reject された promise を無視しない
<!-- For the same reason you shouldn't ignore caught errors from `try/catch`. -->同じ理由で、try/catch
された例外を無視してはいけません。
Bad:
getUser()
.then((user: User) => {
return sendEmail(user.email, 'Welcome!');
})
.catch((error) => {
console.log(error);
});
Good:
import { logger } from './logging'
getUser()
.then((user: User) => {
return sendEmail(user.email, 'Welcome!');
})
.catch((error) => {
logger.log(error);
});
// or using the async/await syntax:
// もしくは async/await 構文を使う
try {
const user = await getUser();
await sendEmail(user.email, 'Welcome!');
} catch (error) {
logger.log(error);
}
Formatting
フォーマット
<!-- 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. The general rule to follow is *keep consistent formatting rules*. For TypeScript there is a powerful tool called [TSLint](https://palantir.github.io/tslint/). It's a static analysis tool that can help you improve dramatically the readability and maintainability of your code. There are ready to use TSLint configurations that you can reference in your projects: -->フォーマットは主観的です。 ここにある多くの規則のように、貴方が従わなければならない厳格なルールではありません。 重要な点は、フォーマットについて 論議しないこと です。 これを自動化するツールはたくさんあります。 それを使ってくさい。 エンジニアがフォーマットについて論議するのは時間とお金の無駄です。 従うべき一般的な規則は 一貫したフォーマットルールを守る ことです。
TypeScriptにはTSLintという強力なツールがあります。 コードの読みやすさと保守性を劇的に向上させるのに役立つ静的分析ツールです。 プロジェクトですぐに使えるTSLint構成です:
-
TSLint Config Standard - standard style rules
-
TSLint Config Airbnb - Airbnb style guide
-
TSLint Clean Code - TSLint rules inspired by the Clean Code: A Handbook of Agile Software Craftsmanship
-
TSLint react - lint rules related to React & JSX
-
TSLint + Prettier - lint rules for Prettier code formatter
-
ESLint rules for TSLint - ESLint rules for TypeScript
-
Immutable - rules to disable mutation in TypeScript
TypeScriptスタイルガイドとコーディング規約も参照してください。
Use consistent capitalization
一貫性を持った大文字を使用すること
<!-- 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() {}
type animal = { /* ... */ }
type Container = { /* ... */ }
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() {}
type Animal = { /* ... */ }
type Container = { /* ... */ }
<!--
Prefer using `PascalCase` for class, interface, type and namespace names.
Prefer using `camelCase` for variables, functions and class members.
-->
クラス、インタフェース、タイプ、名前空間には PascalCase
の利用が好ましいでしょう。
変数、関数、クラスメンバーには camelCase
が好ましいでしょう。
Function callers and callees should be close
関数の呼び出し元と定義は近いはずです
<!-- 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(private readonly employee: Employee) {
}
private lookupPeers() {
return db.lookup(this.employee.id, 'peers');
}
private lookupManager() {
return db.lookup(this.employee, 'manager');
}
private getPeerReviews() {
const peers = this.lookupPeers();
// ...
}
review() {
this.getPeerReviews();
this.getManagerReview();
this.getSelfReview();
// ...
}
private getManagerReview() {
const manager = this.lookupManager();
}
private getSelfReview() {
// ...
}
}
const review = new PerformanceReview(employee);
review.review();
Good:
class PerformanceReview {
constructor(private readonly employee: Employee) {
}
review() {
this.getPeerReviews();
this.getManagerReview();
this.getSelfReview();
// ...
}
private getPeerReviews() {
const peers = this.lookupPeers();
// ...
}
private lookupPeers() {
return db.lookup(this.employee.id, 'peers');
}
private getManagerReview() {
const manager = this.lookupManager();
}
private lookupManager() {
return db.lookup(this.employee, 'manager');
}
private getSelfReview() {
// ...
}
}
const review = new PerformanceReview(employee);
review.review();
Organize imports
import文を整理する
<!-- With clean and easy to read import statements you can quickly see the dependencies of current code. Make sure you apply following good practices for `import` statements: - Import statements should be alphabetized and grouped. - Unused imports should be removed. - Named imports must be alphabetized (i.e. `import {A, B, C} from 'foo';`) - Import sources must be alphabetized within groups, i.e.: `import * as foo from 'a'; import * as bar from 'b';` - Groups of imports are delineated by blank lines. - Groups must respect following order: - Polyfills (i.e. `import 'reflect-metadata';`) - Node builtin modules (i.e. `import fs from 'fs';`) - external modules (i.e. `import { query } from 'itiriri';`) - internal modules (i.e `import { UserService } from 'src/services/userService';`) - modules from a parent directory (i.e. `import foo from '../foo'; import qux from '../../foo/qux';`) - modules from the same or a sibling's directory (i.e. `import bar from './bar'; import baz from './bar/baz';`) -->わかりやすく読みやすいimport文を使用すると、現在のコードの依存関係をすばやく確認できます。
import
構文には以下の良いプラクティスを必ず適用してください。
- インポート文はアルファベット順に並べ、グループ化する必要があります。
- 未使用のインポートは削除する必要があります。
- 名前付きインポートはアルファベット順にする必要があります(例:
import {A, B, C} from 'foo';
) - インポート元はグループ内でアルファベット順になっている必要があります。 例:
import * as foo from 'a'; import * as bar from 'b';
- importのグループは空行で区切られています。
- importのグループは以下の順になるようにする:
- ポリフィル(
import 'reflect-metadata';
) - NodeJSの組み込みモジュール(例:
import fs from 'fs';
) - 外部モジュール(例:
import { query } from 'itiriri';
) - 内部モジュール(例:
import { UserService } from 'src/services/userService';
) - 親ディレクトリからのモジュール(例:
import { UserService } from 'src/services/userService';
) - 同じディレクトリや兄弟ディレクトリからのモジュール(例:
import bar from './bar'; import baz from './bar/baz';
)
- ポリフィル(
Bad:
import { TypeDefinition } from '../types/typeDefinition';
import { AttributeTypes } from '../model/attribute';
import { ApiCredentials, Adapters } from './common/api/authorization';
import fs from 'fs';
import { ConfigPlugin } from './plugins/config/configPlugin';
import { BindingScopeEnum, Container } from 'inversify';
import 'reflect-metadata';
Good:
import 'reflect-metadata';
import fs from 'fs';
import { BindingScopeEnum, Container } from 'inversify';
import { AttributeTypes } from '../model/attribute';
import { TypeDefinition } from '../types/typeDefinition';
import { ApiCredentials, Adapters } from './common/api/authorization';
import { ConfigPlugin } from './plugins/config/configPlugin';
Use typescript aliases
タイプスクリプトエイリアスを使用する
<!-- Create prettier imports by defining the paths and baseUrl properties in the compilerOptions section in the `tsconfig.json` This will avoid long relative paths when doing imports. -->tsconfig.json
のcompilerOptions
セクションで、path
とbaseUrl
プロパティを定義するとより綺麗なimportを書くことができます。
これにより、import時に長い相対パスの使用を避けることができます。
Bad:
import { UserService } from '../../../services/UserService';
Good:
import { UserService } from '@services/UserService';
// tsconfig.json
...
"compilerOptions": {
...
"baseUrl": "src",
"paths": {
"@services": ["services/*"]
}
...
}
...
Comments
コメント
<!-- The use of a comments is an indication of failure to express without them. Code should be the only source of truth. > Don’t comment bad code—rewrite it. > — *Brian W. Kernighan and P. J. Plaugher* -->コメントを使用するのは、それなしでは表現できなかったことを表します。 コードが唯一の真実であるべきです。
悪いコードにコメントをしていないで書き換えてください。 — Brian W. Kernighan and P. J. Plaugher
Prefer self explanatory code instead of comments
コメントではなく、自己説明的なコードを好む
<!-- Comments are an apology, not a requirement. Good code *mostly* documents itself. -->コメントは弁明であり、必須ではありません。 良いコードはほとんどの場合文章のようになっています。
Bad:
// Check if subscription is active.
// subscriptionが有効かを確認
if (subscription.endDate > Date.now) { }
Good:
const isSubscriptionActive = subscription.endDate > Date.now;
if (isSubscriptionActive) { /* ... */ }
Don't leave commented out code in your codebase
コメントアウトされたコードを残さない
<!-- Version control exists for a reason. Leave old code in your history. -->バージョン管理ツールが存在する理由です、古いコードは履歴に残しましょう。
Bad:
type User = {
name: string;
email: string;
// age: number;
// jobPosition: string;
}
Good:
type User = {
name: string;
email: string;
}
Don't have journal comments
日付を持ったコメントはいりません
<!-- Remember, use version control! There's no need for dead code, commented code, and especially journal comments. Use `git log` to get history! -->バージョン管理ツールを使うことを覚えましょう!
使ってないコード、コメントアウトされたコード、日記のようなコメントは不要です。
gitの履歴を取得するのに git log
を使ってください。
Bad:
/**
* 2016-12-20: Removed monads, didn't understand them (RM)
* 2016-10-01: Improved using special monads (JP)
* 2016-02-03: Added type-checking (LI)
* 2015-03-14: Implemented combine (JR)
*/
function combine(a: number, b: number): number {
return a + b;
}
Good:
function combine(a: number, b: number): number {
return a + b;
}
Avoid positional markers
位置取りの印として使うのをさける
<!-- 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. Most IDE support code folding feature that allows you to collapse/expand blocks of code (see Visual Studio Code [folding regions](https://code.visualstudio.com/updates/v1_17#_folding-regions)). -->これは単純にノイズです。 関数と変数名を適切なインデント、フォーマットで使用して、コードの視覚的な構造を持ちましょう。 ほとんどのIDEはコードの折りたたみ機能をサポートしているので、コードブロックを折りたたむ/展開することができます。(Visual Studio Code folding regionsを参照).
Bad:
////////////////////////////////////////////////////////////////////////////////
// Client class
////////////////////////////////////////////////////////////////////////////////
class Client {
id: number;
name: string;
address: Address;
contact: Contact;
////////////////////////////////////////////////////////////////////////////////
// public methods
////////////////////////////////////////////////////////////////////////////////
public describe(): string {
// ...
}
////////////////////////////////////////////////////////////////////////////////
// private methods
////////////////////////////////////////////////////////////////////////////////
private describeAddress(): string {
// ...
}
private describeContact(): string {
// ...
}
};
Good:
class Client {
id: number;
name: string;
address: Address;
contact: Contact;
public describe(): string {
// ...
}
private describeAddress(): string {
// ...
}
private describeContact(): string {
// ...
}
};
TODO comments
<!-- When you find yourself that you need to leave notes in the code for some later improvements, do that using `// TODO` comments. Most IDE have special support for those kind of comments so that you can quickly go over the entire list of todos. Keep in mind however that a *TODO* comment is not an excuse for bad code. -->後で改善が必要と思われるコードがあった場合 // TODO
コメントを使ってください。
ほとんどの IDEはこれらの種類のコメントを特別にサポートしているため、あなたはTODOリストをすばやく探すことが出来ます。
ただし TODO コメントを悪いコードの言い訳にしないでください。
Bad:
function getActiveSubscriptions(): Promise<Subscription[]> {
// ensure `dueDate` is indexed.
return db.subscriptions.find({ dueDate: { $lte: new Date() } });
}
Good:
function getActiveSubscriptions(): Promise<Subscription[]> {
// TODO: ensure `dueDate` is indexed.
return db.subscriptions.find({ dueDate: { $lte: new Date() } });
}
Translations
翻訳
<!-- This is also available in other languages: -->他の言語でも見ることができます:
Brazilian Portuguese: vitorfreitas/clean-code-typescript
Chinese:
Japanese: MSakamaki/clean-code-typescript
他の言語の翻訳作業も進行中です:
Korean
翻訳が完了すれば参考文献として追加されます。
詳細と進捗状況はこのissuesを見てください。 この資料をあなたの言語に翻訳をすることは、Clean Code コミュニティーにとって何ものにも代えがたい貢献となるでしょう。