コードの不吉な臭い・バージョン2

Refactoring: Improving the Design of Existing Code (2nd Edition) (Addison-Wesley Signature Series (Fowler))

『Refactoring』の有名な一節に「コードの臭い」に関するものがあります。これは良くない設計のコードに見られる特徴を「臭い(bad smell)」という言葉で表現したもので、ネット上でもこれらに言及した記事を見かけます。

https://qiita.com/NagaokaKenichi/items/22972e6ba698c7f2978a

今回、第2版を読み進めていて、第1版とは異なるリストになっていることに気づきました。そこで、第1版と比較して、第2版がどうなっているか紹介します。なお、第2版で追加された臭いには見出しに「☆」を、削除された臭いには「★」をつけています。また、第1版についてはオーム社版を参照しています。

新装版 リファクタリング―既存のコードを安全に改善する― (OBJECT TECHNOLOGY SERIES)

☆奇妙な名前(Mysterious Name)

初っぱなから第1版にはない「臭い」です。「コンピュータサイエンスにおいて難しいことは2つしかない。キャッシュの破棄と名前付けである」というPhil Karltonの警句にもあるように、名前付けは難しく、それゆえに名前の変更を行うリファクタリングは頻繁に行うことになります。

When you can’t think of a good name for something, it’s often a sign of a deeper design malaise.

いい名前が思いつかないなら、設計がまずい兆候である。

Refactoring, 2nd edition

重複したコード(Duplicated Code)

同じコードが2カ所以上に表れるなら、1カ所にまとめることでコードを改善できます。

☆長すぎる関数(Long Function)

第1版では「長いメソッド」でしたが、関数に変わっています。これはサンプルコードで使用する言語(=執筆時点で最も人気のある言語)がJavaからJavaScriptに変わったことが影響していると思われます。

経験の浅いプログラマーは短い関数を嫌い、全てをmain関数に押し込みます。しかし、プログラミングのためのメンタルモデルが育ってくると、全体像を頭に入れつつ必要に応じて細部を把握することができるようになってきます。こうなると、短い関数の連なりによって構成されたプログラムの方が理解しやすくなります。また、コードの再利用性のためにも、短く役割のはっきりした関数を書くことは重要です。

短い関数を使って、保守性の高いコードを書くための鍵は名前付けです。名前が関数の役割を的確に表していれば、その関数の中身を見なくても、コードを理解することができます。

また、一つの教訓として、「何かにコメントを付けたいと思ったときは、その処理を関数化すべきである」というものがあります。

長すぎるパラメータリスト(Long Parameter List)

関数に必要なものを引数として渡すのは、悪いことではありません。グローバル変数に依存するよりは、はるかにマシです。しかし、長すぎるパラメータリストは悪いコードの兆候です。

引数リストが長い場合、そのリストを1つのオブジェクトにする(Introduce Parameter Object)といったリファクタリングが適用できます。

☆グローバルデータ(Global Data)

グローバル変数への依存が「悪いこと」であるのはよく知られていることだと思いますが、意外なことに第1版ではグローバル変数への言及はありません。これは、第1版がJavaで、グローバル変数という概念がなかったからでしょうか?

一方、JavaScriptプログラミングでは、グローバル変数を頻繁に触ります。たとえばライブラリの設定方法がグローバル変数になっていることは珍しくありません。

しかし、グローバル変数は、いったん使用が広まると、それを置き換えるのは大変です。使用は避けられないとしても、抑制的な使い方が重要です。

☆変更可能なデータ(Mutable Data)

データへの変更は、予期しないバグを生み出します。関数型プログラミングの考え方では、データ構造に変更を加える際は、値を書き換えるのではなく、変更後の値を持った新しいデータ構造を返すべきである、とされています(そのようにしかできない言語もあります)。

変更可能なデータのスコープが数行程度であれば問題ありませんが、スコープの広い変数が変更可能である場合、それが問題を引き起こすリスクは高まります。

変更の偏り(Divergent Change)

1つのモジュールが様々な理由によって変更されているのは良くない兆候です。

このような場合、そのモジュールが持つ責務が多すぎることが考えられます。フェーズの分割(Split Phase)等のリファクタリングを適用して、責務を明確にし、変更理由が1つになるようにすべきです。

変更の分散(Shotgun Surgery)

1つの目的を持った変更が様々なモジュールに分散してしまうのも、良くない兆候です。

この場合、分散した責務を集約したモジュールを作成し、変更箇所が分散しないようにすべきです。

特性の横恋慕(Feature Envy)

あるモジュールの関数が、別モジュールのデータや関数とばかりコミュニケーションを取っている場合は、その関数を別モジュールに移動させたほうが良いかもしれません。

データの群れ(Data Clumps)

いつも連れ立って使用されるデータは、1つのクラスにまとめた方がいいかもしれません。たとえば、ユーザの名前とメールアドレスは、別々の変数として管理するのではなく、Userクラスのフィールドにするのが良いでしょう。

基本データ型への執着(Primitive Obsession)

整数や文字列といった基本データ型のみを使用してプログラムを書くことも、経験の浅いプログラマーに見られる兆候です。

たとえば、郵便番号を単なる文字列型に格納することがあります。しかし、郵便番号の扱いには独特のドメインロジックがあり、単純な文字列以上の存在とみなすことができます。このような場合は、基本データ型をオブジェクトで置き換えることを検討すべきです。

☆繰り返されるswitch文(Repeated Switches)

オブジェクト指向言語では、条件分岐をポリモーフィズムで置き換え可能です。第1版の出版当時は、このことを強調するために、switch文を使うことは悪い兆候とみなしていました。

現在では、ポリモーフィズムは広く使われるようになり、一方でswtich文にパターンマッチのような洗練された機能を持たせた言語も現れています。

このような状況を鑑みて、「switch文それ自体は悪ではなく、同じswitch文が何度も出てくるのが悪い兆候である」とトーンダウンしています。

☆ループ(Loops)

第1版の出版当時において、Javaにはループの代替となる機能はありませんでした。しかし、現在では、Javaも含め様々な言語で、ループのより良い代替が存在します。

JavaScriptの場合、多くのケースで、ループを配列のfilter/map/reduceなどに置き換えることが可能です。

☆怠け者(Lazy Element)

第1版では「怠け者クラス(Lazy Class)」でしたが、より汎用的な名前に変わっています。
役に立たないモジュールは削除すべきである、ということです。

リファクタリングの結果不要になるようなケースもありますし、変更を予期して作った間接化のレイヤーが不要だとわかることもあります。

疑わしき一般化(Speculative Generality)

将来の変更に備えるという目的でコードを書いたが、実際には変更が発生せず、無駄に柔軟な(その分、複雑な)コードになっていることがあります。

このような場合は、そのコードをよりシンプルな形に変更した方がよいかもしれません。

一時的属性(Temporary Field)

特定の状況でしか必要とされないフィールドは、別のクラスに切り出した方がよいかもしれません。

メッセージの連鎖(Message Chains)

メソッドチェーンには注意が必要です。呼び出し側がチェーンの途中で呼び出されるオブジェクトに強く依存することになるためです。

第三者のメソッドを呼び出すようなメソッドチェーンは、デメテルの法則にも違反します。

このような場合、委譲の隠蔽(Hide Delegate)を使用して、チェーンを隠蔽すべきです。

仲介人(Middle Man)

隠蔽が行き過ぎて、クラスのほとんどのメソッドが別のオブジェクトに委譲をするだけになっているような場合は、仲介人の削除(Remove Middleman)を適用し、移譲先のオブジェクトに直接メッセージを送るようにすべきです。

☆インサイダー取引(Insider Trading)

モジュール間でのデータの受け渡しはモジュールの結合度を高めます。ある程度のデータの受け渡しは必要不可欠ですが、過度にデータを交換しているオブジェクトには注意が必要です。

このような場合、お互いが共通して使用するデータを持つ別のクラスを用意した方がよいかもしれません。

また、継承にも注意が必要です。サブクラスはスーパークラスのことを知りすぎることが少なくありません。継承を委譲に置き換えることで、過度のデータ交換を抑制できます。

巨大なクラス(Large Class)

巨大すぎるクラスは大量のフィールドを抱えていることが多いです。

クラスの抽出(Extract Class)を適用し、フィールドをひとまとめにしましょう。スーパークラスの抽出(Extract Superclass)や種別コードのサブクラスへの置き換え(Replace Type Code with Subclasses)が使えるかもしれません。

クラスのインタフェース不一致(Alternative Classes with Different Interfaces)

あるモジュールから別のモジュールへの置き換えを可能にするには、インタフェースが同じである必要があります。代替可能なはずのモジュールのインタフェースが異なっている場合は、関数宣言の変更(Change Function Declaration)等を使用して、インタフェースを揃えましょう。

データクラス(Data Class)

フィールドとゲッター・セッター以外に何も持たないようなクラスは、単なるデータの保持者です。

データクラスは、必要な振る舞いが誤った場所に書かれている兆候です。そのデータクラスを使用しているコードを、データクラス自身に持たせるようにできないか検討してみましょう。

この原則には例外があり、関数呼び出しの結果を表すオブジェクトなどは独自の振る舞いを持たないクラスとなることがあります。

相続拒否(Refused Bequest)

サブクラスがスーパークラスの一部しか利用していない場合、その継承関係が本当に必要か検討すべきです。

メソッドやフィールドの階層を変更したり、継承を委譲に置き換えたりといったリファクタリングを適用する余地があります。

コメント(Comments)

コメント自体は悪いものではありませんが、悪いコードを説明するためにコメントを手厚く書いているような場合があります。

このような場合、関数の抽出などを通して、コメントが不要なコードに改善することができます。

★パラレル継承(Parallel Inheritance Hierarchies)

第2版では削除。複雑な継承関係は廃止すべき。

★未熟なクラスライブラリ(Incomplete Library Class)

第2版では削除。基盤ライブラリの設計がまずいと、それに立脚したコードを書くのが大変になる。

感想

「イミュータブルなデータは不吉な臭いである」といった、第1版の出版後に広まった知見も反映されていて、現代風にしっかりアップデートされていると思いました。第1版を読んだことがある方も、第2版を読めば新しい発見があると思います。

『ハッカーと画家』を読んだ

ハッカーと画家 コンピュータ時代の創造者たち

『ハッカーと画家』は、ベンチャーキャピタル・Y Combinatorの創設者であり、著名なLISPハッカーでもあるPaul Grahamのエッセイ集。

原文は http://www.paulgraham.com/articles.html で公開されており、ほとんどの部分は日本語訳されたものが公開されている(下記リンクにまとめられている)。

https://matome.naver.jp/odai/2133690674362908101

ハッカーと画家』をはじめ、『オタクが人気者になれない理由』など、半分くらいはすでにオンラインで読んだことのあるエッセイだった。

しかし、通して読むとそれなりにつながりがあるようにも感じられる。デザインの話とか、言語の生産性の話とか。

良いデザインをもった優れたプロダクトを作り出すためには試行錯誤が重要で、そのためにはアイデアを形にする速度が重要である。そうすると、プログラムを書くたびにコンパイルが必要になるような言語よりも、インタプリタ型の言語の方が生産性が高い、といった話。

もっとも、技術的なディテールはやや古びて感じられるのも事実。たとえば、本書が書かれた頃には「古臭い」存在であった静的型付け言語は、プログラムの大規模化やコンパイラ・IDEの進化もあり、復権している。

また、Webアプリケーションがデスクトップアプリケーションを駆逐するという話も、その後のスマートフォンとモバイルアプリの隆盛によって、過去の話になっている感がある。

しかし、現在、分野によってはアプリのダウントレンドとWebの復権がみられるようになってきているように、テクノロジーの世界でも歴史は繰り返す。あと5〜10年もすると、動的型付けの言語が復権しているかもしれないし、LISPがふたたび注目を浴びているかもしれない。10年後くらいにまた読み返すと面白そう、と思った。

Nuxt.jsビギナーズガイドを読んだ

Nuxt.jsビギナーズガイド―Vue.js ベースのフレームワークによるシングルページアプリケーション開発

『Nuxt.jsビギナーズガイド』は、Vue.jsをベースにしたWebアプリケーションフレームワーク、Nuxt.jsの入門書です。仕事でVue.jsを使っているものの、Nuxt.jsの導入が必要かどうか迷っていたので、読んでみました。

Nuxt.jsは、React.js用のサーバサイドレンダリング(SSR)ライブラリであるNext.jsに影響を受けたフレームワークです。Next.jsとは異なり、Nuxt.jsは単にVue.jsのSSR機能を提供するだけでなく、VueRouterやVuexとも統合されています。

本書の中でも特に良かったのは、Nuxt.jsを使うべき場合と使うべきでない場合について言及されている点です。Nuxt.jsは日本国内でも採用事例が増えていて、「Vue使うならNuxtだよね」という雰囲気も出てきています。が、自分たちのアプリケーションの要件に合っているかは、よく考えて導入する必要があります。SSRにはNode.jsサーバが必要になりますし、サーバでもブラウザでも実行できるJavaScript(ユニバーサルJavaScript)を書くには、ブラウザ動くJavaScriptを書くよりも高度なスキルが必要になります。

もう一つ良い点は、本の薄さの割にサンプルコードが豊富なことです。サンプルでは、シンプルなアプリケーションを1つと、Firebaseをバックエンドにした実践的なアプリケーションを1つ実装しています。これらを通して、実際の開発のイメージを固めることができます。

全200ページ程度で読みやすいですし、Nuxt.js初心者の知りたいポイントが要領よくまとまっているので、おすすめです。

なお、『Nuxt.jsビギナーズガイド』はVue.jsを使ったことがあることを前提にしているので、Vue.jsの初心者の人は、まず『Vue.js入門』に目を通すことをおすすめします。

Vue.js入門 基礎から実践アプリケーション開発まで

Secrets of the JavaScript Ninja 2nd 読書メモ 第6章 未来のための関数:ジェネレータとプロミス

Secrets of the Javascript Ninja

ジェネレータ(Generators)は特殊な種類の関数である。通常の関数が1つの値しか返さないのに対して、ジェネレータは、実行を断続的に中止しながら、複数の値を生み出すことができる。

プロミス(Promises)は、JavaScriptに組み込みのオブジェクトであり、非同期処理の取り扱いに適している。

ジェネレータ関数

ジェネレータ関数は以下のように定義する。functionキーワードの直後に*(アスタリスク)がついているのがジェネレータ関数の 目印である。ジェネレータ関数の中ではyieldというキーワードを使って値を生成できる。

function* WeaponGenerator() {
  yield 'Katana';
  yield 'Wakizashi';
  yield 'Kusarigama';
}

for (let weapon of WeaponGenerator()) {
  console.log(weapon); // Katana, Wakizashi, Kusarigama
}

イテレータオブジェクトでジェネレータを制御する

ジェネレータの呼び出しはジェネレータ関数の本文の実行を意味しない。その代わりに、イテレータオブジェクトが作成される。このオブジェクトを通してジェネレータとコミュニケーションをとることができる。

function* WeaponGenerator() {
  yield 'Katana';
  yield 'Wakizashi';
}

const weaponIterator = WeaponGenerator();

const weapon1 = weaponIterator.next();
console.log(weapon1); // { value: 'Katana', done: false }

const weapon2 = weaponIterator.next();
console.log(weapon2); // { value: 'Wakizashi', done: false }

const weapon3 = weaponIterator.next();
console.log(weapon3); // { value: undefined, done: true }

イテレータはジェネレータの実行をコントロールするために使用される。イテレータオブジェクトはnextメソッドを持っていて、このメソッドを通してジェネレータに値を要求できる。

const weapon = weaponIterator.next();

この呼び出しに対して、ジェネレータはyieldに達するまでコードを実行する。yieldに達すると、その時点での結果を返して、実行を中断する。この結果には、その時点での値(value)と、ジェネレータが完了したか(done)が含まれる。

現在の値の生成が終わると、ジェネレータはそこで中断する。

イテレータの繰り返し

イテレータのnextメソッドを使うことで、ジェネレータに新しい値を要求できる。また、ジェネレータが次の値を持っているか知るには、結果として取得できるオブジェクトのdoneプロパティを使用する。これらを利用すると、古典的なwhileループでイテレータから繰り返し値を取得できる。

function* WeaponGenerator() {
  yield 'Katana';
  yield 'Wakizashi';
}

const weaponsIterator = WeaponGenerator();
let item;
while (!(item = weaponsIterator.next()).done) {
  console.log(item.value); // Katana, Wakizashi
}

for-ofループは、このループのシンタックスシュガーである。

for (let item of WeaponGenerator()) {
  console.log(item);
}

別のジェネレーターを生成する

ジェネレータの実行を別のジェネレータに任せたい場合もある。

function* NinjaGenerator() {
  yield 'Hattori';
  yield 'Yoshi';
}

function* WarriorGenerator() {
  yield 'Sun tzu';
  yield* NinjaGenerator();
  yield 'Genghis Khan';
}

for (let warrior of WarriorGenerator()) {
  console.log(warrior); // Sun tzu, Hattori, Yoshi, Genghis Khan

yeild*演算子をイテレータに使うことで、別のジェネレータから値を生成できる。

ジェネレータとやりとりする

ジェネレータから値を生み出すだけでなく、ジェネレータに値を送ることもできる。

ジェネレータ関数の引数としてデータを送る

function* NinjaGenerator(action) {
  // 値を生成することで、ジェネレータは中間の計算結果を返すことができる
  // イテレータのnextメソッドを引数付きで呼ぶと、
  // ジェネレータにデータを送り返すことができる
  const imposter = yield `Hattori ${action}`;
  // nextに送られた値がyieldされた式の値になる
  console.log(`imposter: ${imposter}`);
  yield `Yoshi (${imposter}) ${action}`;
}

const ninjaIterator = NinjaGenerator('skulk');
const result1 = ninjaIterator.next();
console.log(result1.value); // Hattori skulk
const result2 = ninjaIterator.next('Hanzo'); // HanzoをNinjaGeneratorに
console.log(result2.value); // Yoshi (Hanzo) skulk

ジェネレータに例外を投げる

function* NinjaGenerator() {
  try {
    yield 'Hattori';
    console.error("The expected exception didn't occur");
  } catch (e) {
    console.log('Aha! We caught an exception');
  }
}

const ninjaIterator = NinjaGenerator();
const result1 = ninjaIterator.next();
console.log(result1); // Hattori

ninjaIterator.throw('Catch this!'); // Aha! We caught an exception

ジェネレータの内部を探求する

ジェネレータは小さなプログラムのようなものであり、複数の状態を遷移するステートマシンでもある。

  • 開始前(Suspended start):ジェネレータが作成された直後の状態
  • 実行中(Executing):ジェネレータのコードが実行されている状態
  • 生成停止中(Suspended yield):実行中に、yield式に到達し、処理が中断している状態
  • 完了(Completed):returnに到達するかコードを最後まで実行し、値の生成が完了した状態
function* NinjaGenerator() {
  yield 'Hattori';
  yield 'Yoshi';
}

const ninjaIterator = NinjaGenerator();
// 新しいジェネレータを作成する(Suspended start)

const result1 = ninjaIterator.next();
// ジェネレータの実行を開始し、Executingに移行する
// yield 'Hattori'; までを実行し、Suspended yield状態になる
// 新しいオブジェクト { value: 'Hattori', done: false } を返す

const result2 = ninjaIterator.next();
// ジェネレータの実行を再度開始し、Executingに移行する
// yield 'Yoshi'; までを実行し、Suspended yield状態になる
// 新しいオブジェクト { value: 'Yoshi', done: false } を返す

const result3 = ninjaIterator.next();
// ジェネレータの実行を再度開始し、Executingに移行する
// 実行するコードがないのでCompleted状態に移行する
// 新しいオブジェクト { value: undefined, done: true } を返す

ジェネレータの実行コンテキストを追跡する

function* NinjaGenerator(action) {
  yield `Hattori ${action}`;
  return `Yoshi ${action}`;
}

// ninjaIterator変数はNinjaGeneratorの実行コンテキストを参照する
const ninjaIterator = NinjaGenerator('skulk');

// 通常の関数と異なり、next()メソッドは実行コンテキストを生成しない
// その代わりに、NinjaGeneratorの実行コンテキストで処理を続行する
const result1 = ninjaIterator.next();
const result2 = ninjaIterator.next();

Promiseとやりとりする

Promiseは、ES2015で導入された、非同期処理を扱いやすくするための概念である。今は存在せず、将来手に入る値のプレースホルダーである。

// Promiseオブジェクトのコンストラクタを呼び出し、
// resolve(成功時に実行する関数)とreject(失敗時に実行する関数)をとる
// コールバック関数(executor)を渡す
const ninjaPromise = new Promise((resolve, reject) => {
  resolve('Hattori');
  // 失敗時の処理は reject(new Error('An error ...')) のように書く
});

// Promiseのthenメソッドを使って、成功時のコールバック関数を渡す
ninjaPromise.then(ninja => {
  console.log(ninja); // Hattori
});

Promiseに渡したコールバック関数(executor)は即時実行される。

単純なコールバックの問題を理解する

私たちは、長く時間のかかる処理によってユーザの操作を妨げないために、非同期処理を行っている。非同期処理のための最も初歩的な道具はコールバック関数である。

getJSON('data/ninjas.json', function() {
  // 処理
});

このようなタスクの際にはエラーの発生する可能性がある。コールバック関数の問題は、try-catchのような言語の組み込み機能を使えない点だ。以下のコードではエラーはキャッチできない。

try {
  getJSON('data/ninjas.json', function() {
    // 処理
  });
} catch(e) {
  // エラー処理
}

コールバック関数を呼び出すコードはイベントループの同じステップにはないことが普通だたら、例外処理でキャッチはできず、エラーは失われる。

そのため、Node.jsの世界では、コールバック関数にerrとdataの2つの引数が渡されるような慣習が確立されている。errにnullでない値が渡された場合はエラーが発生しているということだ。

コールバック関数の第一の問題点はエラーハンドリングの難しさである。

また、長い処理の後には、取得した結果を使って何か処理を行うことが多い。この結果、別の長い処理が開始されることもある。

getJSON('data/ninjas.json', function(err, ninjas) {
  getJSON(ninjas[0].location, function(err, locationInfo) {
    sendOrder(locationInfo, function(err, status) {
      // statusについての処理
    });
  });
});

このように、連続したステップを実施するのが難しいことが、コールバック関数の第二の問題点である。

さらに、最終的な結果を得るためのステップが、依存関係にない場合もある。このような場合、実行時間を最適化するためには、並行して処理を行うのが望ましい。しかし、並行した処理をコールバック関数で実装するのはかなりトリッキーである(書籍にはコード例があるが、長いので割愛)。コールバック関数の第三の問題点は、並行処理が難しいことである。

コールバック関数は、try-catchやループのような言語の組み込み機能と併せて使うことが難しいのも欠点である。

Promiseを使ってみる

Promiseは非同期に実行するタスクの結果のプレースホルダーである。まだ手に入れておらず、将来入手できる値を意味する。Promiseはいくつかの状態をもつ。

Promiseは、Pending状態から始まる。この状態をUnresolvedと呼ぶこともある。プログラムの実行が進み、Promiseのresolve関数が呼ばれると、PromiseはFullfilled状態に移行する。このとき、望んだ値を手に入れることができる。

一方、Promiseのreject関数が呼ばれるか、例外が発生し、それがPromiseの中でハンドルされなかった場合、PromiseはRejected状態に移行する。この場合、エラーの内容を取得できる。

FullfilledおよびRejectedをまとめてResolved状態とも呼ぶ。一度Resolvedに移行すると、Unresolvedに戻ることはない。

console.log('At code start');

const ninjaDelayedPromise = new Promise((resolve, reject) => {
  console.log('ninjaDelayedPromise executor');
  // 500ミリ秒後にresolve
  setTimeout(() => {
    console.log('Resolving ninjaDelayedPromise');
    resolve('Hattori');
  }, 500);
});

console.log('After creating ninjaDelayedPromise');

ninjaDelayedPromise.then(ninja => {
  console.log(`ninjaDelayedPromise resolve handled with ${ninja}`);
});

const ninjaImmediatePromise = new Promise((resolve, reject) => {
  console.log('ninjaImmediatePromise executor. Immediate resolve.');
  resolve('Yoshi');
});

ninjaImmediatePromise.then(ninja => {
  console.log(`ninjaImmediatePromise resolve handled with ${ninja}`);
});

console.log('At code end');

// ↑の実行結果は↓のようになる。Promiseのresolveは即時呼び出しではなく、次のイベントループの実行まで遅延する。
// At code start
// ninjaDelayedPromise executor
// After creating ninjaDelayedPromise
// ninjaImmediatePromise executor. Immediate resolve.
// At code end
// ninjaImmediatePromise resolve handled with Yoshi
// Resolving ninjaDelayedPromise
// ninjaDelayedPromise resolve handled with Hattori

Promiseの却下(reject)

Promiseの実行を失敗させる(rejectする)方法は2つある。明示的に reject メソッドを実行するか、ハンドルされない例外を発生させるかである。

const explicit = new Promise((resolve, reject) => {
  reject('明示的なreject');
});
explicit.then(() => console.log('実行されないはず'))
  .catch(error => console.error(error));

const implicit = new Promise((resolve, reject) => {
  // 未定義変数の書き換えによる例外が発生する
  undeclaredVariable += 1;
});
implicit.then(() => console.log('実行されないはず'))
  .catch(error => console.error(error));

このように、Promiseの中で発生したエラーを一括して処理する仕組みが存在するのがPromiseの大きな利点である。

Promiseの連鎖

then を実行すると新しいPromiseが返る。そのため、 then を繰り返すことでPromiseを連鎖させることができる。

fetch('ninjas.json')
  .then(response => response.json()) // レスポンスをJSON.parse()する
  .then(ninjas => console.log(ninjas)) // 取得したデータを出力
  .catch(error => console.error(error));

Promiseの連鎖の中で発生したエラーは、末尾の catch で一括でキャッチできる。

複数のPromise

依存関係のない複数の非同期処理を並行して実行することもできる。

Promise.all([
  fetch('ninjas.json'),
  fetch('mapInfo.json'),
  fetch('plan.json'),
]).then(results => {
  const ninjas = results[0];
  const mapInfo = results[1];
  const plan = results[2];
  console.log(ninjas, mapInfo, plan);
}).catch(console.error);

Promise.all メソッドはPromiseの配列をとり、全てのPromiseが成功したら返る新しいPromiseを作成する。いずれか1つのPromiseが失敗した場合は、Promiseは失敗する。

いくつかの非同期タスクのうち、最初に完了したものの結果だけを取得したい場合は、 Promsie.raceが利用できる。

Promise.race([
  fetch('yoshi.json),
  fetch('hattori.json),
  fetch('hanzo.json),
]).then(ninja => {
  console.log(ninja);
});

ジェネレータとPromiseを組み合わせる

非同期処理を行うコードをジェネレータの中に置くことで、シーケンシャルな非同期処理をエレガントに書くことができる。

function async(generator) {
  const iterator = generator();
  const handle = iteratorResult => {
    if (iteratorResult.done) return;

    const iteratorValue = iteratorResult.value;
    if (iteratorValue instanceof Promise) {
      iteratorValue.then(res => handle(iterator.next(res));
                   .catch(err => iterator.throw(err));
    }
  };

  try {
    handle(iterator.next());
  } catch (e) {
    iterator.throw(e);
  }
}

async(function* () {
  try {
    const ninjas = yield fetch('ninjas.json);
    const missions = yield fetch(ninjas[0].missionsUrl);
    console.log(missions);
fetch(missions[0].detailsUrl);
  } catch(e) {
    console.error(e);
  }
});

async関数

JavaScriptのasync関数は、前述したPromiseとジェネレータの組み合わせを言語機能化したものである。

(async function() {
  try {
    const ninjas = await fetch('ninja.json');
    const mission = await fetch(ninjas[0].missionsUrl);
    console.log(missions);
  } catch(e) {
    console.error(e);
  }
})();

asyncキーワードを使って非同期関数であることを宣言すると、この中ではawaitキーワードを使って、Promiseがresolveされるのを待つことができる。

『超速! Webページ速度改善ガイド』を読んだ

超速! Webページ速度改善ガイド ── 使いやすさは「速さ」から始まる (WEB+DB PRESS plus)

ここ最近、仕事でWebサイトのパフォーマンス改善をしているので、最新のベストプラクティスを押さえるために読みました。

パフォーマンス改善については、『ハイパフォーマンスWebサイト』という古典的名著があり、ここ最近の本としては『Webフロントエンド ハイパフォーマンスチューニング』などがあります。

パフォーマンス改善には、計測(ボトルネックの特定)=>改善というステップが存在します。本書は、計測方法や改善の方策が、具体的にわかりやすく解説されているのが特徴です。また、章立てが 基礎知識 => 調査と改善 で統一されているため、効率よく知識を身につけることができます。

Chrome DevTools はパフォーマンスの改善には欠かせないツールですが、高機能なぶんどのような機能があるか把握するのが大変です。本書では、ネットワーク・スクリプティング・ペインティング・メモリ等、様々なシーンでDevToolsを使って計測をする方法が解説されています。

また、改善方法も、小さなサンプルコードが載っているため、すぐに導入しやすいです。

Webサイトのパフォーマンスに悩んでいる人が、最初に読む本としてオススメ。