「みんなのデータ構造」でプログラミングで使うデータ構造を学ぶ

「みんなのデータ構造」(Amazonリンク)とは、コンピュータ・サイエンスの基礎となるデータ構造の教科書「Open Data Structure」の日本語訳です。Introduction to Algorithmsといったアルゴリズムの名著への橋渡しになるような、実用的なテーマが丁寧に説明されています。

この本でデータ構造を学ぶ意義は、訳者まえがきで以下のように説かれています。

  1. ソフトウェアのほとんどはシンプルなデータ構造の組み合わせでできている。
  2. 「みんなのデータ構造の内容がだいたいわかれば、いいエンジニアになれる。

また、わからない部分は読み飛ばしていいとも書かれています。さらに嬉しいことに、この書籍の中でも実務や学術研究で頻繁に登場する内容がピックアップされています。

書籍のサンプルコードはC++ですが、何か1つプログラミング言語を知っていれば問題なく読み進めることができます。

解説パンダくん
解説パンダくん

弁護士ドットコムでは、エンジニア有志で本書の輪読会をしています。この本の内容をマスターして、競技プログラミングに挑戦するぞ!

SLList・DLListの感想・考察

  • Arrayでは、先端、末尾の要素に対する追加add、削除removeはO(1)。しかし、それ以外のindexに対する要素の追加、削除はO(1 + min{i, n-i})
  • SLListやDLListでは、先端head、末尾tailのノードの参照を保持する
  • SLListはStackとQueueインターフェース(push, pop, add, remove)が実装できる(計算量はO(1))
  • SLListは直前のノードの参照を保持していないが、DLListは各ノードが直前のノードの参照を保持する
    • SLListは単方向。先端から末尾に辿るだけ
    • DLListでは、先端のノードの直前と末尾のノードの次のノードはdummyノードとするため、循環できる
  • DLListでは、要素の取得get、変更set、追加add、削除removeの計算量はO(1 + min{i, n-i})である
    • しかし、対象となるノードの参照を保持している場合に限り、計算量はO(1)となる
  • SLListや、DLListは動的に要素を追加/削除する場面で適したデータ構造
  • 一方、DLListは前後のノードの参照を持つため、メモリ効率が良くない。SLListの短所を改善するものの、空間計算量にデメリットが生じる。

コンピュータ・サイエンスの基礎を学びたい方や、競技プログラミングにチャレンジする方に「みんなのデータ構造」(Amazonリンク)はおすすめです。

連結リストとは何か

連結リストには、SLList(singly-linked list、単方向連結リスト)とDLList(doubly-linked list、双方向連結リスト)がある。

SLListは、Queue(add、remove)、Stack(push、pop)の操作をO(1)で実装できる。 DLListは、Queue、Stack、Dequeの操作をO(1)で実装できる。

配列(Array)と比較すると、連結リストの短所はget(i)、set(i, x)が全ての要素に対して定数時間ではなくなる。長所は、ノードへの参照uがあれば、uの削除やuの隣へのノードの挿入が定数時間で実行できること。

SLList: 単方向連結リスト

SLListは、StackとQueueインターフェースを実装する。push(x)、pop()、add(x)、remove()の実行時間はいずれもO(1)である。

ノードを定義する

各ノードuは、データu.xと参照u.nextを保持している。列の末尾のノードwにおいては、w.next = nullである。

class Node {
public:
	T x;
	Node *next;
	Node(T x0) {
		x = 0;
		next = NULL;
	}
};

先頭と末尾のノードを変数に格納する。

Node *head;
Node *tail;
int n;

push() - O(1)

先頭に要素を追加する。

T push(T x) {
	Node *u = new Node(x);
	u->next = head;
	head = u;
	if (n == 0) {
		tail = u;
	}
	n++;
	return u;
}

pop() - O(1)

先頭の要素を取り出す。

T pop() {
	if (n == 0) return null;
	T x = head->x;
	Node *u = head;
	head = head->next;
	delete u;
	n--;
	if (n == 0) tail = NULL;
	return x;
}

remove() - O(1)

先頭の要素を取り出す。popと実装は同じ。

T remove() {
	if (n == 0) return null;
	T x = head->x;
	Node *u = head;
	head = head->next;
	delete u;
	n--;
	if (n == 0) tail = NULL;
	return x;
}

add() - O(1)

末尾に要素を追加する。

bool add(T x) {
	Node *u = new Node(x);

	if (n == 0) {
		head = u;
	} else {
		tail->next = u;
	}

	tail = u;
  n++;
	return true;
}

DLList: 双方向連結リスト

DLListは、Listインターフェースを実装する。get(i)、set(i, x)、add(i, x)、remove(i)の実行時間はいずれもO(1 + min{i, n-i})である。

ノードを定義する

前後のノードの参照を持つ。

struct Node {
	T x;
	Node *prev, *next;
}

先頭のノードの前、末尾のノードの次にはdummyノードを設置する。

Node dummy;
int n;
DLList() {
	dummy.next = &dummy;
	dummy.prev = &dummy;
	n = 0;
}

i番目のノードを取得する。計算量はO(min{i, n-i})

Node* getNode(int i) {
	Node* p;

	if (i < n/2) {
		p = dummy.next;
		for (int j = 0; j < i; j++)
			p = p->next;
	} else {
		p = &dummy;
		for (int j = n; j > i; j--)
			p = p->prev;
	}

	return (p);
}

get()/set() - O(1 + min{i, n-i})

getNode(i)を利用する。

T get(int i) {
	return getNode(i)->x;
}
T set(i, x) {
	Node* u = getNode(i);
	T y = u->x;
	u->x = x;
	return y;
}

add(i, x)/remove(i) - O(1 + min{i, n-i})

ノードwの直前にノードuを追加する。

Node* addBefore(Node *w, T x) {
	Node *u = new Node(x);
	u->prev = w->prev;
	u->next = w;
	u->prev->next = u;
	u->next->prev = u;
	n++;
	return u;
}

addは、addBeforeとgetNodeを組み合わせる。

void add(int i, T x) {
	addBefore(getNode(i), x);
}

ノードwを削除する。

void remove(Node *w) {
	w->prev->next = w->next;
	w->next->prev = w->prev;
	delete w;
	n--;
}

次にremove(i)を実装する。

T remove(int i) {
	Node *w = getNode(i);
	T x = w->x;
	remove(w);
	return x;
}
Algorithmみんなのデータ構造
プログラミングをするパンダ
プログラミングをするパンダ (@Panda_Program)
Software Engineer

毎日コードを書くという習慣

jQueryの作者John Resig氏による「Write Code Every Day」というブログを読んだ。この記事は氏のブログの紹介と、自分もかつて半年ほどやっていたので軽い振り返りだ。

氏は、初めサイドプロジェクトで休日が消費され、しかもいいコードが書けなかった時の喪失感やプレッシャーを抱えていたそうだ。

ある日、Jennifer Dewalt氏が180日間もの間、毎日何らかのサイトを作るというプロジェクトを完遂したのに触発され、John Resig氏は毎日コードを書いてみようと決心した。

自分に4つのルールを課す

毎日コードを書くという取り組みのために、4つのルールを自分に課したとのこと。

  1. コードを毎日書く。ドキュメント、ブログやコードを追加すること
  2. 有用なコードを書く。インデントの修正やリファクタは加算しない
  3. 12時までに書くこと
  4. コードはGithubにアップしてOSSとすること

なるほど。これなら続きそうだ。3, 4は任意だが、夜遅くに雑なコードを書かないようにコードの質を意識するために入れたそうだ。

どのような効用があったか

この習慣を5ヶ月続けて、得た知見はざっとこんな感じらしい。

  • 最小限で動くコードを書くようになった(Minimum viable code)
  • 毎日進捗を生んでいるという感触を得たため、不安がなくなったthe anxiety started to melt awayという表現が面白い)
  • 閃きの頻度が増えた(散歩やシャワーを浴びている時でも問題解決のいい方法を思いついた。これは週末にサイドプロジェクトをしていた時にはなかったそうだ)
  • 毎日サイドプロジェクトのコードを読んでいるので、コンテキストスイッチのコストが減った
  • 5ヶ月でいくつかWebサイトを作り、大量のnode moduleを作成した

中でも、**「過去数カ月間で自分が書いたコードの量が(あまりにも多すぎて)信じられない」**という感想が印象的だ。

驚くべきことに、John Resig氏の「毎日コードを書く」習慣は2013/11/23から2017/4/17まで3年半もの間続いている。氏のGithubアカウントのコミット履歴がその証拠だ。その意志の力に多大なるリスペクトを送りたい。

かつて自分も半年間やってみた

**実は私もかつて半年ほど「毎日コミットをする」ということをやってみたことがある。**その活動内容をまとめようとブログの下書きを作ってからもう1年がたってしまった。そのうち最後まで書いてして公開しよう。下書きを読み返して特に興味深いと感じた箇所を以下に抜粋する。

半年間のアウトプット内容

以下は、2018/10-2019/5の半年間のアウトプットの内容だ。プログラマ1年目の終わりから2年目の途中までだ。

Elmで作ったポートフォリオサイトは今でも自分のお気に入りだ。デザインは当時同じチームだったデザイナーさんにアドバイスを貰った。また、Tailwind CSSを2019年の頭にはもう使っており、その便利さは十分に理解していた。Tailwind CSSの存在は会社の先輩(もう退職してしまった)に教えてもらったのだ。最近Tailwind CSSの記事を書いた。

また、XServerで運用していたWordPressのブログサイトをGatsbyJS + Netlifyに載せ替えたのもこの頃だった。

解説パンダくん
解説パンダくん

WordPressのDBに格納された本文をGatsbyJS用にMarkdownに変換するのは大変だったよ。

そのほかはLispを触ったり、React Nativeを触ってみたり、Rust、Go言語を触ってみたりと、色々やっていたんだなと振り返っていて思う。

この時期(プログラマ1年目〜2年目半ば)は、業務で扱うPHP以外に何を学べばいいのかわからなかったため、興味のあるものに手当たり次第手を出していた。そうすることで、「自分は停滞しているのではないか」という不安を解消していた。

PHPだけでは生き残れないと思って個人開発でReactを使った結果、今では実務でReact/Next.jsを使ったフロントエンドの開発を任せて貰えるまでに至った。社内でも「パンダさんはフロント寄りの人だと思っている」というコメントをメンバーやマネージャーから貰っていた。上記の中では一番大きなリターンをもたらしてくれたトライだった。

また、このブログを立ち上げたのもこの時期だ。

6つのルールを自分に課す

このときルールを自分に課していた。

それは「GitHubに毎日コミットして、コミット履歴を埋める」というものです。方法はGitHubの自分のレポジトリに1日最低1コミットするだけ。ただし、あくまで自分の学習のためなので、以下のルールを設けました。

・ 1日最低1コミットでOK
・ プログラミング言語のコードを記述する
・ 仕事で書いたコードのコミットは除く
・ 設定ファイルの変更やREADMEはカウントしてOK
・ コードのフォーマットや改行などはカウントしない
・ チュートリアルは1章につき1コミット

1コミットでOKにしたのは、「人は少しのことなら格段に行動しやすくなる」という心理学の考え方を元にしているからです。人間は何かを始めることに対しては億劫になりがちですが、一度始めてしまえば容易に継続できます。

このルールはよかった。実際、机に座ってエディタを開いたら、コードを書き足したりリファクタリングする意欲が湧いてくる。そうなれば、あとは設計して手を動かすだけだ。

朝は「今日は何を書こうか」という意識から始まる。つまりそれは、「どのような課題を解決しようか」という問いと同じだ。問題意識を無意識下に追いやると、通勤中でも食事中でも脳はbackground taskとして処理を続ける。そして答えは右脳が教えてくれる。

また、この頃から、人が読まなくても自分が後で読むとわからないようなコードを書かないように意識していた。明日の自分はもはや他人である。ここからアプリケーションの設計に意識が向き始めた。

**そして設計方面の学習を続けた結果、最近MRのレビューでCleanな設計の視点からコメントを書いた時に、たまたまテックリードが自分のコメントを見てくれていて「とてもいいコメントをしますね」とフィードバックを貰えた。**これは努力の方向性が間違っていなかったことが確認できて安心したし、何より褒めて貰えたことが素直に嬉しかった。

毎日コミットするという習慣は半年で辞めてしまったが、今でも自分にリターンをもたらしてくれている。

まとめ

今日はブログを毎日書いて11日目だ。**正直、ブログに比べるとコードを書くほうがよっぽど楽だ。**コードはコンピュータに対する命令であり、プログラミングはコンピュータの力を引き出すことである。コーディングの上達はPCを自分がうまく操作できることであり、成長の実感があった。

しかし例えばブログがバズったり、SEOで流入数が増えたからといって、プログラミングスキルが上達したことにはならない(もちろん、モチベーションの維持と向上には繋がる)。

**なのになぜ自分が毎日ブログを書いているか。それは、かつて学んだこと、手元で試したことを再利用のために書き留めて置きたいからである。**個人開発で試したことを実務で本番環境に取り入れたことが何度もある。その時は、自分のGithubのレポジトリを参照して、「ああ、こう書いてたな」と思い出してた。

それをブログという、誰でも読みやすい形にリライトしているのだ。毎日ブログを書くのはキツいものがある(3〜6時間は取られる)一方、書きたいネタはまだたくさんある。React、Next.jsの使い心地、本の感想、チーム開発について、アジャイルと現場、プロダクトに対するオーナーシップなど。ただ、いつまで続くだろうか。それは自分にもわからない。

たとえ毎日ブログを書くのをやめたとしても、次は毎日コードを書く習慣を復活させているかな。

関連記事: Clean Coderに挙げられている「ソフトウェアのプロが備えるべき最低限のこと」

Essay
プログラミングをするパンダ
プログラミングをするパンダ (@Panda_Program)
Software Engineer

追記: 本記事がesa.ioの公式Twitterに取り上げられました!

esaに書いた記事をNext.jsで公開する

Next.jsのバージョン9.3から、ビルド時に外部ソースからデータを取得するgetStaticPropsというAPIが公開されました。

ブログは静的なコンテンツです。ブログの内容はユーザーに応じて動的に変わるということはありません。そして、getStaticPropsは静的なページを構築するために最適なAPIです。

そこで、esaにmarkdownで書いた記事をNext.jsで表示するサイトを構築しました。

実際にサイトにアクセスして記事を開いてみてください。爆速で遷移するのが体験できます。Lighthouseの成績もバツグンです。(blog-starterをベースに利用したため、コンテンツはそのレポジトリの内容を踏襲しています)

Lighthouseで測定した結果。スコアはperformanceの98以外、全て100

デモサイトのコードはGitHubにアップしています。

Next.jsのblog starterを利用する

準備として、Next.jsの公式が用意しているblog-starterを利用しました。

blog starterではmarkdownファイルをgray-matterでHTMLに変換しています。今回は、markdownのソースをローカルファイルではなくesaのAPIのレスポンスに変更するように書き換えます。

esaの設定をする

APIキーを作成する

まずはHTTPリクエストでesaの記事を取得するために、APIキーを作成しましょう。

esaにアクセスして、setting > applicationを開きましょう。

esaの管理画面

そして、「Generate new token」をクリックして、Personal access tokensを作成します。

esaの管理画面

tokenに名前をつけましょう。また、今回は読み取りだけなので、read権限のみを付与します。

esaの管理画面

このキーはAPIから記事を取得する際に利用します。一度しか表示されないので、コピーして別の箇所に保存します。

記事を作成する

esaで記事を作成しましょう。いつものように記事書くことに加え、記事の冒頭にslugや画像など、メタ情報を記述していきます。

esaの記事の編集画面

GitHubに記事内容のサンプルを掲載しています。

(追記: 将来的にはメタ情報を追加できるようになるそうです。機能追加を期待して待ちましょう😊)

Next.jsのビルド時にesaから記事を取得する

記事取得のAPIをコールする

esaへのAPIリクエストは以下のように作成できます。先ほど取得したAPIキーとチーム名は環境変数に設定します。

const API_TOKEN = process.env.NEXT_EXAMPLE_CMS_ESA_API_TOKEN
const TEAM = process.env.NEXT_EXAMPLE_CMS_ESA_TEAM
const endpoint = `https://api.esa.io/v1/teams/${TEAM}/posts`

async function fetchAPI(path) {
  const res = await fetch(`${endpoint}${path}`, {
    headers: {'Authorization': `Bearer ${API_TOKEN}`}
  })

  if (!res.ok) {
    console.error(await res.text())
    throw new Error('Failed to fetch API')
  }

  const json = await res.json()
  if (json.errors) {
    console.error(json.errors)
    throw new Error('Failed to fetch API')
  }

  return json
}

レイヤードアーキテクチャでは、このfetchAPIがData Access層に相当します。

Next.jsはバージョン9.4から.envファイルに環境変数を書き込むだけで、process.envを通じて値を利用できるようになりました。このため、環境変数は.envを作成して値をそちらに記載します。

NEXT_EXAMPLE_CMS_ESA_API_TOKEN=
NEXT_EXAMPLE_CMS_ESA_TEAM=
NEXT_EXAMPLE_CMS_ESA_CATEGORY=

基本的なリクエストは上記ですが、クエリパラメータで返却される記事を絞ることができます。主に使うクエリは下記です。

keyvalue役割
sortcreated並べ替えのキーを作成日にする
orderdesc降順で並べる
qwip:falseshipされた記事を取得する
qon:category名category名に完全一致する記事を取得する

その他のクエリパラメータはesaの公式サイトで確認できます。

SSGをするためにgetStaticPropsを利用する

SSGとは、Static Site Generationのことです。これは静的なページをビルド時に作成することに特徴があります。

SSRはリクエストを受けてサーバーでコードを実行しますが、SSGのページは静的なファイルのみを返します。このため、レスポンスにかかる時間を短縮できます。簡単にいうと、表示速度が阿部寛のページ並みになるということです。

ページ、pages/posts/[slug].jsgetStaticPropsを利用します。ビルド時にブログのslugが確定しているため、ブログ記事のURLは決まっています。例えば、/posts/hello-worldなど、記事のslug名ごとのパスで記事にアクセスできます。一方、/posts/foo-barなど、slugに存在しないパスを入力すると404が返ってきます。

これを実現するために、getStaticPathsを使って、slugごとにページを作成しましょう。

export async function getStaticProps({params}) {
  // slugをkeyに記事のコンテンツをesaから取得する
  const post = await getPostBySlug(params.slug, [
    'title',
    'date',
    'slug',
    'author',
    'content',
    'ogImage',
    'coverImage',
  ])
  const content = await markdownToHtml(post.content || '')

  return {
    props: {
      post: {
        ...post,
        content,
      },
    },
  }
}

export async function getStaticPaths() {
  // 全ての記事のslugを取得する
  const posts = await getAllPosts(['slug'])

  return {
    paths: posts.map((posts) => ({
      // paramsはgetStaticPropsに渡される
      params: {
        // [slug].jsなので、プロパティはslugにする。
        // もし[id].jsなら、{params: {id: post.slug}} にする
        slug: posts.slug,
      }
    })),
    fallback: false,
  }
}

Data Mapping層を設けて、esaから取得するデータを変換する

getStaticPaths, getStaticProps内で呼び出している関数getAllPostsgetPostBySlugは下記のように実装しています。

export async function getPostBySlug(slug, fields = []) {
  const data = await fetchAllPosts()
  const posts = data.posts.map((post) => mapPost(post, fields))
  return posts.filter((post) => post.slug === slug)[0]
}

export async function getAllPosts(fields = []) {
  const data = await fetchAllPosts()
  return data.posts.map((post) => mapPost(post, fields))
}

設計面の話すると、今回はAPIがデータソースであるため、ブログアプリケーションで必要なデータとは異なるデータが返ってきます。そこで、Reactで使いやすいデータに整形する必要があります。そのデータマッピングをするためにmapPost関数を作成しています。

また、esaの特徴として、記事のIDはincrementalな数字です。このesaの仕様のままでは、記事のアドレスは/posts/1/posts/777になってしまいます。

しかし、記事のアドレスは無機質な数字より英文の記事名の方がSEO対策になります。例えば、このブログでも記事のslugは必ず英語にしています(この記事は/posts/nextjs-with-cms-esa)。そして、このslugはesaのAPIレスポンスには含まれていません。このため、記事のメタデータの中にslugを自分で設定できるようにしています。

このように、データの保存元とデータの利用先で必要なデータの形式が異なることはソフトウェアでは一般的です。そこで、下記のようにData Mapping層を設けています。

ここでは引数fieldsでkeyを指定し、必要な情報だけを取得できます。

function mapPost(post, fields) {
  // gray-matterでmarkdownからメタデータと本文を分離する
  const {data, content} = matter(post.body_md)
  const items = {}

  fields.forEach((field) => {
    switch (field) {
      case 'slug':
        if (typeof data.slug === 'undefined') {
          throw new Error('Slug is not set.')
        }
        items[field] = data.slug
        break
      case 'content':
        items[field] = content
        break
      default:
        if (data[field]) {
          items[field] = data[field]
        }
        break
    }
  })

  return items
}

データをコンポーネントに注入する

propsは上記のgetStaticPropsから渡されます。

export default function Post({post, preview}) {
  const router = useRouter()
  if (!router.isFallback && !post?.slug) {
    return <ErrorPage statusCode={404}/>
  }
  return (
    <Layout preview={preview}>
      <Container>
        <Header/>
        {router.isFallback ? (
          <PostTitle>Loading…</PostTitle>
        ) : (
          <>
            <article className="mb-32">
              <Head>
                <title>
                  {post.title} | Next.js Blog Example with {CMS_NAME}
                </title>
                <meta property="og:image" content={post.ogImage.url}/>
              </Head>
              <PostHeader
                title={post.title}
                coverImage={post.coverImage}
                date={post.date}
                author={post.author}
              />
              <PostBody content={post.content}/>
            </article>
          </>
        )}
      </Container>
    </Layout>
  )
}

ちなみに、コンポーネントはblog-starterから1行の変更なく使い回すことができました。

データの取得元をmarkdownからesaのAPIに変更しただけなので、「コンポーネントが必要なデータさえ渡せばviewに変更を加えることなく表示内容を替えることができる」というのはviewとロジックが分離しているクリーンなコードである証左ですね。

ローカルでビルドして記事取得を確認する

手元でビルドしてみましょう。

$ npm run build

> blog-starter@1.0.0 build /Users/matthew/sample_program/react/next-blog-esa
> next build

info  - Loaded env from .env
Creating an optimized production build

Compiled successfully.

Automatically optimizing pages

Page                                Size     First Load JS
 /                               1.17 kB        70.2 kB
   /_app                           288 B            58 kB
 /404                            2.55 kB        60.5 kB
 /posts/[slug]                   2.03 kB        71.1 kB
 css/f6f82ffa2b321e5ac3c5.css  167 B
 /posts/dynamic-routing
 /posts/hello-world
 /posts/preview
+ First Load JS shared by all       58 kB
 static/pages/_app.js            288 B
 chunks/commons.ceeeee.js        10.7 kB
 chunks/framework.e84fa6.js      40 kB
 runtime/main.2d0e0e.js          6.28 kB
 runtime/webpack.c21266.js       746 B
 css/d7c40193fbab5fdba323.css    2.46 kB

λ  (Server)  server-side renders at runtime (uses getInitialProps or getServerSideProps)
  (Static)  automatically rendered as static HTML (uses no initial props)
  (SSG)     automatically generated as static HTML + JSON (uses getStaticProps)

下記の箇所が注目ポイントです。

└ ● /posts/[slug]                   2.03 kB        71.1 kB
    └ css/f6f82ffa2b321e5ac3c5.css  167 B
    ├ /posts/dynamic-routing
    ├ /posts/hello-world
    └ /posts/preview

は、SSGで作成されたページであることを表します。ファイルは/pages/[slug].jsしか用意していないのに、getStaticPathsで取得したslugに応じてdynamic-routinghello-worldpreviewの3つのページが作成されています。これがSSGの特徴です。

getStaticPaths返り値のfallbackをfalseにしているので、ここに存在しないパス/posts/foo-barなどはコンテンツが存在せず、404になります。

VercelでデプロイしてJAMStackな構成にする

Next.jsを手軽にデプロイするならVercelがベストです。Vercelは、Next.jsを開発しているVercel社のサービスです。無料でサーバレスなアプリをデプロイできることが特徴です。

ちなみに、サービスとしてのVercelの旧名はNowで、会社としてのVercelの旧名はZEITでした。$21M、つまり2,100万ドル(約22億円)を調達したタイミングでサービスと社名が一致するように改名されました。

コマンドをインストールする

まずはVercelをインストールします。

$ npm i --global vercel@latest

環境変数を設定する

マイページから環境変数を設定します。

vercelのsetting画面

環境変数は、環境ごとに設定できます。

vercelを使ってデプロイする

環境変数の設定が完了したら、プロジェクトルートで下記のコマンドを実行します。これがデプロイコマンドです。

$ vercel
Vercel CLI 19.0.1
🔍  Inspect: https://vercel.com/panda-program/next-blog-esa/hqm0kdiw1 [6s]
  Preview: https://next-blog-esa.panda-program.now.sh [copied to clipboard] [44s]
📝  To deploy to production (next-blog-esa.now.sh), run `now --prod`

たった1コマンドでデプロイできました。https://next-blog-esa.panda-program.now.shはPreview用のURLです。Vercelのデプロイ方針は、develop, staging, productionではなく、Develop, Preview, Ship(Production)です。このPreview用のURLはデプロイするたびに作成されます。

本番用にデプロイするには--prodオプションを追加します。

$ vercel --prod
Vercel CLI 19.0.1
🔍  Inspect: https://vercel.com/panda-program/next-blog-esa/d6363ukyc [7s]
  Production: https://next-blog-esa.now.sh/ [copied to clipboard] [42s]

コマンドの結果に表示されているように、プロダクションのURLはhttps://next-blog-esa.now.sh/です。

ビルドのログを確認する

上記のInspectのURLにアクセスすると、deploy単位で状態を把握できます。

vercelのinspectページ

ビルドのログは画面から見ることができます。

vercelのビルドのログ表示画面

以上でデプロイは完了です!

デプロイに成功すると、Vercelはサイトのキャプチャも表示してくれるので、トップページはわざわざ毎回アクセスする手間が省けます。このような配慮も嬉しいですね。

vercelの管理画面でサイトのプレビューが表示されている

Lighthouseのスコアも完璧。爆速ブログの完成です!

Lighthouseで測定した結果。スコアはperformanceの98以外、全て100

触ってみてわかったCMSとしてesaを使うメリットとデメリット

実際にesaを使ってブログを構築してみました。メリットとデメリットを比べてみましょう。

esaをCMSとするメリット

まず、メリットは以下のようなものです。

・内容がリアルタイムで反映される
・画像準備が楽
・非エンジニアでも記事を入稿できる

esaのエディタはmarkdownをリアルタイムでパースしてくれます。Qiitaのようにmarkdownの変換結果がすぐにわかります。また、画像はドラッグ&ドロップでesaのS3アップロードできます。Gatsbyでは自分でGitレポジトリに画像を配置する必要があるので、手間が省けるのは便利です。

さらに、もし会社でブログを作るなら、markdownを書くために非エンジニアがレポジトリをcloneしてcommitしてpushするのは非現実的です。CUIではなくてもGUIで、GitHubのサイト上で直接ファイルの作成・編集はできます。ただ、そもそも入稿者が増えるたびにGitHubアカウントを作成して、権限を設定するのは煩雑です。

その点、会社で全員がesaで記事をread/writeできるなら、入稿者からは慣れたインターフェースで入稿できるのでとても楽です。

esaをCMSにする時のネックになること

デメリットは以下のことが挙げられるかと思います。

・メタ情報を自分で追加する必要がある
・previewモードが未実装
・3ヶ月目から有料になる

esaのレスポンスは以下のようなものです。ブログとして公開するために欲しいdescriptionやアイキャッチ用のimageといったメタ情報が存在しないのです。

{
    "posts": [
        {
            "number": 9,
            "name": "sample post",
            "full_name": "sample post",
            "wip": false,
            "body_md": "body",
            "body_html": "<p>body</p>",
            "created_at": "2020-05-23T19:43:05+09:00",
            "message": "Create post.",
            "kind": "stock",
            "comments_count": 0,
            "tasks_count": 0,
            "done_tasks_count": 0,
            "url": "https://next.esa.io/posts/9",
            "updated_at": "2020-05-23T19:43:05+09:00",
            "tags": [],
            "category": "blog/nextjs",
            "revision_number": 1,
            "created_by": {
                "name": "パンダ",
                "screen_name": "panda",
                "icon": "https://img.esa.io/uploads/production/users/44031/icon/thumb_m_a793b9b0f4e39c59f914e25ba447f485.jpg"
            },
            "updated_by": {
                "name": "パンダ",
                "screen_name": "panda",
                "icon": "https://img.esa.io/uploads/production/users/44031/icon/thumb_m_a793b9b0f4e39c59f914e25ba447f485.jpg"
            },
            "stargazers_count": 0,
            "watchers_count": 1,
            "star": false,
            "watch": true,
            "sharing_urls": null
        }
    ],
    "prev_page": null,
    "next_page": null,
    "total_count": 3,
    "page": 1,
    "per_page": 20,
    "max_per_page": 100
}

そもそもesaの記事にはdescriptionなどは存在しないからなのですが、ブログとして公開するためには必要な要素です。

そのため、メタ情報をesaの記事に記述する必要があります。また、Next.jsの9.4から実装されたPreviewモードは、esaにAPIがないため利用できません(ただ、previewAPIがなくともesaのエディタのpreviewで十分だとは思っています)。

最後に、esaは無料のサービスではないので、2ヶ月の無料期間が終われば、以降は月500円の課金が必要になります。便利なサービスに対価を払うことは、そのサービスが存続するためには絶対に必要なことです。一方、徹底的に無料でブログを作りたい方にはその点がネックになるでしょう。

まとめ

いかがでしたでしょうか。Next.jsとesaでJAMStackな爆速ブログを作る方法を紹介しました。

Gatsbyではすでにesaをデータソースとするプラグインがありますが、Next.jsでは見当たらなかったので自分で作ってみました。

ユースケースとしては、「企業でesaを情報共有ツールとして使っている。そして、非エンジニアも入稿したい」という時にesaをCMSとして使うのがベストだと思います。

esaのカテゴリを指定して記事を取得できるので、例えばブログ記事は/public/blogに書くというようにブログ専用のカテゴリを作って運用すると良いでしょう。

会社で使うのではなくても、esaを個人で利用している人は少なくないです。一人でも多くの方がブログを公開して、知識を共有することでプログラミング界隈が盛り上がることを願っています。

なお、トップの画像はVercel社のOpen Graph Image as a Serviceというサービスを利用して作成しています。

ReactNext.jsJAMStackVercel
プログラミングをするパンダ
プログラミングをするパンダ (@Panda_Program)
Software Engineer

VercelでGatsbyJS用のキャッシュの設定をする

**GatsbyJSとは、React.js製の静的サイトジェネレータです。SSRをすることでビルド時に最適化された静的ファイルを生成するため、サイトの表示速度が爆速になります。**ブログで使われているケースが多いです。

GatsbyJSについては「GatsbyJSで実現する、高速&実用的なサイト構築」という本が詳しいです。GatsbyJSの構成やブログの作り方などが解説されています。

**この記事ではGatsbyJSで作成したのサイトに適切なキャッシュの設定方法を紹介します。**まずGatsbyJSで推奨されているキャッシュの種類は2種類あることを紹介し、コンテンツごとに最適なキャッシュ方法を紹介します。

関連記事

GatsbyJS公式が推奨するキャッシュの設定を読む

GatsbyJS公式サイトはキャッシュの推奨設定を公開しています。これを読むとGatsbyJSにおけるキャッシュの設定はファイルに応じて2種類あることがわかります。

1. cache-control: public, max-age=0, must-revalidate;
2. cache-control: public, max-age=31536000, immutable;

1のキャッシュヘッダーは、頻繁に更新があるコンテンツに対して付与します。max-age=0なのでキャッシュはせず、さらにmust-revalidateで必ずサーバー側でキャッシュの検証をします。なお、max-age=0, must-revalidateno-cacheと書き換えることもできます。

2は、max-age=31536000とあるので、下記の計算式から1年間キャッシュを保存することがわかります。

解説パンダくん
解説パンダくん

計算式は 31,536,000(秒)= 60(秒) × 60(分) × 24(時) × 365(日) だよ。

GatsbyJSはビルド時にjsやCSSのファイル名にhashを付与するので、ビルドごとに一意なファイルを生成します。

例えばapp.jsというファイルはapp-[content-hash].jsというファイル名に変換されます。このため、以前ビルドしたコンテンツをブラウザが誤って読み込むことはなく、キャッシュを破棄させるキャッシュバスティングせずともコンテンツを常に最新に保てます。

以下ではなぜそのキャッシュを適用するのか、GatsbyJSで生成したコンテンツの内容を読みながらキャッシュの種類ごとに詳しく見ていきます。

「cache-control: public, max-age=0, must-revalidate」を適用するファイル一覧

キャッシュヘッダーにcache-control: public, max-age=0, must-revalidate;を適用するものは、HTML、app-data.json、/page-data/配下のJSONです。

実際にファイルの中身を見れば、なぜキャッシュしないことが推奨されているか理解ができます。まず、HTMLは頻繁に更新されるので、最新版が必要であることは理解できると思います。

/page-data/のpage-data.jsonを読む

トップページで使うJSONファイル、page-data.jsonを見てみましょう。

/page-data/index/page-data.json
{
  "componentChunkName": "component---src-templates-index-template-js",
  "path": "/",
  "result": {
    "data": {
      "allMarkdownRemark": {
        "edges": [
          {
            "node": {
              "fields": {
                "slug": "/posts/gatsbyjs-cache",
                "categorySlug": "/category/gatsbyjs/"
              },
              "frontmatter": {
                "title": "GatsbyJS公式推奨のキャッシュ設定を理解する",
                "date": "2020/07/05",
                "category": "Vercel",
                "description": "GatsbyJSとは、React.js製の静的サイトジェネレータです..."
              }
            }
          },
          {
            "node": {
              "fields": {
                "slug": "/posts/nextjs-slack",
                "categorySlug": "/category/next-js/"
              },
              "frontmatter": {
                "title": "Next.jsからSlackに通知を送る",
                "date": "2020/07/04",
                "category": "Next.js",
                "description": "この記事ではNext.jsからSlackに通知を送る方法を紹介します..."
              }
            }
          }
          // ...
        ]
      }
    }
  }
}

ハイライトを当てている箇所は、本記事の情報です。

**/page-data/index/page-data.jsonはトップページで表示している記事の一覧です。**確かにこのファイルをキャッシュしてしまうと、新規記事を追加しても以前サイトにアクセスした人は最新記事へのリンクが表示されなくなってしまいますね。

postspagesといった個別の記事についても上記と同様にmarkdownRemark(本文情報)とfrontmatter(メタ情報)がJSONに格納されています。このため、常に最新の記事を配信するためにはpage-data.jsonをキャッシュしてはいけないのです。

/page-data/のapp-data.jsonを読む

次にapp-data.jsonを見てみましょう。

/page-data/app-data.json
{"webpackCompilationHash":"ef93e02a0d2ee7cef376"}

webpackCompilationHashは、ブラウザが読み込んでいるサイトのバージョンと実際にデプロイされた最新のバージョンが一致していることを確認するために使われます。

ユーザーが表示するサイトを常に最新に保つためには、これもキャッシュしてはいけない内容ですね。

「cache-control: public, max-age=31536000, immutable」を適用するファイルタイプ一覧

キャッシュヘッダーにcache-control: public, max-age=31536000, immutable;を適用するものは、JavaScript、CSS、/static/配下の静的ファイルです。

webpack.stats.jsonを読んでJS、CSSのファイル名を確認する

JavaScriptとCSSは、ビルドのたびに一意のハッシュが付与されると先ほど書きました。webpack.stats.jsonを読むとビルドされたJS、CSSのファイル名を確認できます。

/webpack.stats.json
{
  "namedChunkGroups": {
    "app": {
      "assets": [
        "webpack-runtime-43c21f6c6453f3e9506e.js",
        "webpack-runtime-43c21f6c6453f3e9506e.js.map",
        "styles.595bac0ca0e2ea7b429b.css",
        "styles-0dd9b16d06f2e4f550cc.js",
        "styles-0dd9b16d06f2e4f550cc.js.map",
        "framework-84c9287a5714d2d8ce36.js",
        "framework-84c9287a5714d2d8ce36.js.map",
        "532a2f07-f5ad30ee5092265c5f96.js",
        "532a2f07-f5ad30ee5092265c5f96.js.map",
        "app-c2875e4f24d448537cff.js",
        "app-c2875e4f24d448537cff.js.map"
      ],
    },
    // ...
  }
}

filename-[content-hash].jsの形式になっていますね。ファイル内容に変更がある場合にのみ新規のハッシュが付与されます。その場合、コンテンツはサーバーから読み込まれます。

**反対に、ファイルに変更がない場合はハッシュは変更されません。この時、ブラウザはキャッシュからコンテンツをロードします。**これは画像(png, jpg, webp)やフォント(woff、ttf)などの静的ファイルも同じです。

このため、JS、CSS、静的ファイルのキャッシュ期間は1年間が最適なのです。

/sw.jsのサービスワーカーだけはJSの例外

JavaScriptの中でもサービスワーカー/sw.jsだけはcache-control: public, max-age=0, must-revalidateを設定します。これは新しいバージョンのサイトが利用可能かどうかをリクエストのたびに確認するためです。

/sw.jsgatsby-plugin-offlineというプラグインを利用している場合にのみ生成されます。

まとめ

GatsbyJS公式が推奨しているキャッシュは、ざっくりいうと「キャッシュする」「キャッシュしない」の二択ですね。

また、キャッシュはLighthouseやCore Web Vitalsのスコアを上げるために有効です。つまりSEO対策にもなるんです。

最適なキャッシュを設定して、GatsbyJS製のサイトの表示速度をさらに爆速にしましょう。

GatsbyJS
プログラミングをするパンダ
プログラミングをするパンダ (@Panda_Program)
Software Engineer

この記事は、弁護士ドットコム Advent Calendar 2019 - Qiitaの2日目の記事です。

TL;DR

  • TDDの実践方法を実際にコードを書いて解説します
  • TDDの「レッド・グリーン・リファクタリング」のリズムを学ぼう
  • 何度もテストを実行して、プログラムに対する不安を取り除こう

TDDのサイクル

TDDはテスト技法ではなく設計手法

TDD Boot Camp Sendai 9thに参加しました。TDDの伝道師和田さん(@t_wada)を講師に迎え、有志たちで開かれた勉強会でした。

解説パンダくん
解説パンダくん

和田さんは「テスト駆動開発」(TDD本。Kent Beck著)の翻訳者だよ。ちなみに、Kent Beckはアジャイルの源流となるエクストリーム・プログラミングの提唱者で、今はFacebookで働いているよ。

午前中は和田さんによるTDDに関する講演とライブコーディング。午後は参加者同士のペアプロで出題されたお題を実装していく活気あるイベントでした。

イベントを通じてTDDはテストファーストのことだと考えていた自分は目を見開かされました。TDDは単にテストファーストでプログラムを実装することではなく、実装(ソフトウェア)が期待しない動作をすることに対する不安を取り除くための一連の手法だったのです。

TDD本にもこのように書かれています。

皮肉なことに、TDDはテスト技法ではない。TDDは分析技法であり、設計技法であり、実際には開発のすべてのアクティビティを構造化する技法なのだ。『テスト駆動開発』p.278(著 Kent Beck, 訳 和田 卓人)

最初は単にテストファーストだと思っていた自分は、TDDの基本動作がとても洗練されていることに感銘を受けました。

そこでプログラミングコンテストAtCoderのABC(AtCoder Beginner Contest)の問題を題材にして、TDDの手法を紹介していきます。

TDD実践の流れ

そもそもTDDは、**「動作する綺麗なコード」**を書くことを目標にして、以下のステップで実装を進めていく手法です。

1. 目標をTODOリストとして書き出す
2. TODOリストから一つピックアップし、テストを書く
3. テストコードを実行して失敗させる(レッド)
4. 実装コードを書く
5. できる限り最短でテストが通るコードを実装する(グリーン)
6. コードの重複を除去する(リファクタリング)
7. 次のTODOを選び、2に進む

この一連のステップを進めていくと、以下のルールを自然と守ることができます。

  • テストが落ちているときにのみ実装コードを追加する
  • 実装コードは、テストが通っている時にのみ書き換える

これにより、ソフトウェアは開発者がテストという形で意図した動作をするようになり、開発者は「ソフトウェアが期待していない挙動をすることに対する不安」から解放されることになります。

TDDの題材にAtCoderを選んだ理由

TDDを試すに当たり、AtCoderの問題を解くことが適していると考える理由は、以下の3点です。

  • 問題文がそのまま仕様であること
  • 入力と出力が明確であること
  • 入出力のパターンが複数例用意されていること

AtCoderにおいて、競技プログラミング初心者向けのABCというコンテストでは難易度順にA問題からF問題の6問が出題されます。

ここでは、難しいアルゴリズムを使わなくても一般的なエンジニアであれば回答ができるB問題を解いていきます。

以前構築したAtCoderをPHPで解くためのレポジトリを使用します(この環境の準備過程は「競技プログラミングAtCoderを快適に解くための環境構築をする」に記載しています)。

問題(仕様)を読み解く

ABC 第141回 B - Tap Danceの問題文は以下の通りです。

高橋君はタップダンスをすることにしました。タップダンスの動きは文字列 S で表され、S の各文字は L, R, U, D のいずれかです。 各文字は足を置く位置を表しており、1 文字目から順番に踏んでいきます。 S が以下の 2 条件を満たすとき、またその時に限り、S を「踏みやすい」文字列といいます。 ・ 奇数文字目がすべて R, U, D のいずれか。 ・ 偶数文字目がすべて L, U, D のいずれか。 S が「踏みやすい」文字列なら Yes を、そうでなければ No を出力してください。

この問題の制約は以下の通りです。

S は長さ 1 以上 100 以下の文字列 S の各文字は L, R, U, D のいずれか

つまり、この問題文は以下の仕様に読み換えることができます。

「1から100まで長さの、L, R, U, Dがランダムに並んだ文字列が入力される。この時、奇数文字目がR・U・Dのいずれかであり、かつ偶数文字目がL、U、DのいずれかであればYesを出力する。そうでなければNoを出力する」

例えば、入力値がRUDLUDRである場合、偶数文字目、機数文字目が「踏みやすい」という条件を満たすので、出力はYesになります。

TODOリストを作成する

TDDはここから始まります。まずTODOリストを作成していきましょう。

1から100まで長さの、L,R,U,Dがランダムに並んだ文字列が入力される時、入力された文字列について、以下のように仕様を分解してTODOリストを作成します。

ただし、今回はYesかNoを出力するのではなく、YesかNoを返却するという実装をしていきます。

  • L,R,U,Dのいずれかの文字を渡した時、YesまたはNoを返す
    • 文字Rを渡した時、Yesを返す
    • 文字Uを渡した時、Yesを返す
    • 文字Dを渡した時、Yesを返す
    • 文字Lを渡した時、Noを返す

また、「奇数文字目がR・U・Dのいずれかであり、かつ偶数文字目がL、U、DのいずれかであればYesを返す」という条件を以下のように読み替えましょう。

「奇数文字目の少なくとも1つはL、または偶数文字目の少なくとも1つがRであれば、Noを返すことができる」

このため、TODOリストに、下記の項目を追加します。

  • 奇数文字目の少なくとも1つがLである場合、Noを返す
    • 文字列LRRを渡した時、Noを返す
  • 偶数文字目の少なくとも1つがRである場合、Noを返す
    • 文字列LRRを渡した時、Noを返す

早速実装していきましょう。

1周目 - 仮実装

まずは簡単に実装できる「Rを入力すると、Yesを返す」という項目を選びます。

レッド

テストを記述します。文字列Rを入力して文字列Yesを返すため、「Sampleクラスのsolveメソッドの実引数に文字Rを渡すと文字列Yesを返す」テストを記述します。

SampleTest.php
class SampleTest extends TestCase
{
    /**
     * @test
     */
    public function 文字Rを渡した時、文字列Yesを返す()
    {
        $sample = new Sample();
        $result = $sample->solve('R');
        $this->assertSame('Yes', $result);
    }
}

(これ以降のテストメソッドでは@testアノテーションを省略します)

ここからがTDDの真髄です。この時点でテストを実行して、エラーメッセージを確認します。初めてのレッドです。

1) SampleTest::testB
Error: Class 'AtCoder\Sample' not found

実は、Sampleクラスとsolveメソッドのどちらも実装していません。

なぜなら、「テストがレッドになって初めて実装コードを追加する」というルールに従っているからです。

テストが落ちることを確認したので、これをグリーンにするための実装コードを書いていきましょう。

グリーン

まずは上記のエラーを解消するためにAtCoder\Sampleクラスを作成します。

Sample.php
namespace AtCoder;

class Sample
{
}

またテストを実行します。まだテストが落ちることは想像できますね。エラーメッセージは以下です。

1) SampleTest::testB
Error: Call to undefined method AtCoder\Sample::solve()

次はsolveメソッドを実装してテストを実行します。

class Sample
{
    public function solve(string $input): string
    {
    }
}
1) SampleTest::test
TypeError: Return value of AtCoder\Sample::solve() must be of the type string, none returned

返り値はStringであるということなので、空の文字列を返すようにしましょう。

public function solve(string $input): string
{
    return '';
}

これでもまだテストは落ちることが想定できますね。「今はテストが落ちる」という感覚が重要なんです。

テストに慣れると、アプリケーションの動きに対する開発者の予想と結果が一致するようになります。

つまり、「ああすればレッドになり、こうすればグリーンになる」という感覚を得ることができます。結果、アプリケーションが想定外の動きをすることに対する不安が減少していくのです。

テストを実行してみましょう。

1) SampleTest::文字Rを渡した時、文字列Yesを返す
Failed asserting that two strings are identical.
--- Expected
+++ Actual
@@ @@
-'Yes'
+''

案の定テストは落ちています。しかし、テストのエラーメッセージが、テストが通る実装方法を教えてくれています。

solveメソッドは文字列のYesを返すようにしましょう。

PHPUnit 8.0.0 by Sebastian Bergmann and contributors.

.                                                                   1 / 1 (100%)

Time: 17 ms, Memory: 4.00MB

OK (1 test, 1 assertion)

テストが通りました。初めてのグリーンです。

ただ文字列を返す渡すだけでいいのか、という疑問について

ただ、実装コードでは引数も使ってないし、文字列Yes返すだけです。

本当にこれでいいのかと疑問に思われることでしょう。しかし、アサーションで期待している結果を返すためにベタ書きでもいいのでレッドをグリーンにすることは、TDDにおける立派なテクニックなのです。

このテクニックには**「仮実装」**という名前がついています。

TDDでは**「動作する綺麗なコード」**を書くことが目標です。

仮実装により、最短で「動作するコードを書く」ことができました。

テストがグリーンになったので、次のステップに進みましょう。

リファクタリング

リファクタリングのステップは、「テストがグリーンである間はプログラムの挙動が変わっていないので、内部実装をどのように書き換えても良い」というスタンスを取ります。

また、リファクタリングの途中でテストがレッドになったら変更箇所を元に戻します。

これにより、開発者はリファクタリングによってプログラムを壊してしまう不安から解放されます。

では、実際にリファクタリングをしていきましょう。

SampleTest.php
public function 文字Rを渡した時、文字列Yesを返す()
{
    $sample = new Sample();
    $result = $sample->solve('R');
    $this->assertSame('Yes', $result);
}
Sample.php
public function solve(string $input): string
{
    return 'Yes';
}

ただ、この段階ではあまり書き換えるところが見当たりませんね。これは好みによりますが、強いて言えばテストコードの行数を減らすことくらいでしょうか。

SampleTest.php
public function 文字Rを渡した時、文字列Yesを返す()
{
    $sample = new Sample();
    $this->assertSame('Yes', $sample->solve('R'));
}

コードを書き換えたのでテストを実行します。

PHPUnit 8.0.0 by Sebastian Bergmann and contributors.

.                                                                   1 / 1 (100%)

Time: 17 ms, Memory: 4.00MB

OK (1 test, 1 assertion)

テストがグリーンであることを確認して、先に進みます。

TODOリストの見直し

完了した項目にチェックを入れましょう。

  • L,R,U,Dのいずれかの文字を渡した時、YesまたはNoを返す
    • 文字Rを渡した時、Yesを返す
    • 文字Uを渡した時、Yesを返す
    • 文字Dを渡した時、Yesを返す
    • 文字Lを渡した時、Noを返す
  • 奇数文字目の少なくとも1つがLである場合、Noを返す
    • 文字列UULを渡した時、Noを返す
  • 偶数文字目の少なくとも1つがRである場合、Noを返す
    • 文字列URUを渡した時、Noを返す

このタイミングでTODOリストを見直して、項目の追加や削除、変更をします。

今回はこのまま次に移りましょう。

2周目 - 明白な実装

次は「文字Lを渡した時、Noを返す」という項目を選びます。

レッド

SampleTest.php
public function 文字Lを渡した時、Noを返す()
{
    $sample = new Sample();
    $this->assertSame('No', $sample->solve('L'));
}

テストを実行して、レッドであることを確認しましょう。

1) SampleTest::文字Lを渡した時、Noを返す
Failed asserting that two strings are identical.
--- Expected
+++ Actual
@@ @@
-'No'
+'Yes'

テストをグリーンにするためのコードを実装していきます。

グリーン

Sample.php
public function solve(string $input): string
{
    if ($input === 'L') {
        return 'No';
    }
    return 'Yes';
}

テストが通りました。

実装方法が明らかに頭の中にある場合は、仮実装をせずに直接実装します。

これをTDDでは**「明白な実装」**と呼んでいます。

OK (2 tests, 2 assertions)

リファクタリング

テストコードを見てみましょう。

SampleTest.php
public function 文字Rを渡した時、文字列Yesを返す()
{
    $sample = new Sample();
    $this->assertSame('Yes', $sample->solve('R'));
}

public function 文字Lを渡した時、Noを返す()
{
    $sample = new Sample();
    $this->assertSame('No', $sample->solve('L'));
}

このリファクタリングでは、重複を排除します。Sampleオブジェクトを生成している箇所を、setUpメソッドにまとめましょう。

SampleTest.php
class SampleTest extends TestCase
{
    private $sample;

    protected function setUp()
    {
        $this->sample = new Sample();
    }

    // ...
}

リファクタリングの基本は一歩ずつです。まだ$sampleプロパティを消さずに、テストを実行しましょう。

OK (2 tests, 2 assertions)

テストが壊れていないことを確認してから$sampleプロパティを使用します。

SampleTest.php
public function 文字Rを渡した時、文字列Yesを返す()
{
    $sample = new Sample();
    $this->assertSame('Yes', $this->sample->solve('R'));
}

public function 文字Lを渡した時、Noを返す()
{
    $sample = new Sample();
    $this->assertSame('No', $this->sample->solve('L'));
}

変数$sampleはもう使われていませんが、ここでまた再度テストを実行します。

OK (2 tests, 2 assertions)

グリーンであることを確認したら、変数$sampleを削除してテストを実行します。

SampleTest.php
public function 文字Rを渡した時、文字列Yesを返す()
{
    $this->assertSame('Yes', $this->sample->solve('R'));
}

public function 文字Lを渡した時、Noを返す()
{
    $this->assertSame('No', $this->sample->solve('L'));
}
OK (2 tests, 2 assertions)

このリファクタリングでは何も壊していないことを確認できました。

他にリファクタリングをする箇所はないので、先に進みます。

TODOリストの見直し

完了した項目にチェックを入れましょう。

また、今の実装では文字U・Dを渡してもYesを返します。問題の条件より「Noを返す場合以外はYesを返す」と読み替えることができるので、TODOの項目を2つ削除してもいいと判断します。

  • L,R,U,Dのいずれかの文字を渡した時、YesまたはNoを返す
    • 文字Rを渡した時、Yesを返す
    • 文字Uを渡した時、Yesを返す
    • 文字Dを渡した時、Yesを返す
    • 文字Lを渡した時、Noを返す
  • 奇数文字目の少なくとも1つがLである場合、Noを返す
    • 文字列UULを渡した時、Noを返す
  • 偶数文字目の少なくとも1つがRである場合、Noを返す
    • 文字列URUを渡した時、Noを返す

この記事の構成を練っている段階では、U、Dのテストケースも必要だと考えていました。

しかし、コードを実装しているうちに、今回このケースは不要だと思ったのでリストから削除します。

このように、TODOリストを絶対的なものではなく、状況に応じて柔軟に変更を加えるものとして扱いましょう。

3周目

次は「文字列UULを渡した時、Noを返す」という項目を選びます。

レッド

テストコードを書きます。

public function 文字列UULを渡した時、Noを返す()
{
    $this->assertSame('No', $this->sample->solve('UUL'));
}

先ほどリファクタリングをしたおかげで、とてもシンプルなコードになりました。

テストを実行して、レッドであることを確認します。

1) SampleTest::文字列UULを渡した時、Noを返す
Failed asserting that two strings are identical.
--- Expected
+++ Actual
@@ @@
-'No'
+'Yes'

グリーン

現状の実装コードを見てみましょう。

public function solve(string $input): string
{
    if ($input === 'L') {
        return 'No';
    }
    return 'Yes';
}

さて、困りましたね。ここでもう一度条件と現状を確認しましょう。

「奇数文字目がLならNoを返す」という一般的な条件は、「1文字がLならNoを返す」という条件を包含しています。

現状では後者は実装済みですが、前者は未実装です。

今回のTODOリストの項目、「文字列UULを渡すとき、Noを返す」の文字列UULは3文字目がLであることを利用して、一般化した実装をしていきましょう。

Sample.php
public function solve(string $input): string
{
    $len = strlen($input);

    // インデックスを2ずつインクリメントすることで奇数文字目を走査する
    for ($i = 0; $i < $len; $i += 2) {
        if ($input[$i] === 'L') {
            return 'No';
        }
    }

    if ($input === 'L') {
        return 'No';
    }
    return 'Yes';
}

テストを実行してみます。

PHPUnit 8.0.0 by Sebastian Bergmann and contributors.

...                                                                 3 / 3 (100%)

Time: 34 ms, Memory: 4.00MB

OK (3 tests, 3 assertions)

無事に通りました。

三角測量

なお、より一般的な条件を満たすコードを実装するためにテストケースを増やすというテクニックをTDDでは**「三角測量」**と呼びます。

1文字目がLである文字Lを入力するケースと、3文字目がLであるUULを入力するケースを合わせて三角測量をすることで、一般化の方向が定まります。

ただし、三角測量は毎回使うテクニックではありません。実装方法が明らかではない場合に、歩幅を狭めて自分の立ち位置を確認しながら進むために使います。

実際にTDD本では、著者が三角測量を使うときは一般的な実装が本当にわからないときだけと書かれています。実装方法がわかっている場合は、明白な実装で構いません。

リファクタリング

今回は、不要になったコードを削除します。

Sample.php
public function solve(string $input): string
{
    $len = strlen($input);

    for ($i = 0; $i < $len; $i += 2) {
        if ($input[$i] === 'L') {
            return 'No';
        }
    }

    return 'Yes';
}

テストを実行して、挙動が変わっていないこと、また以前に作成したのテストも通っていることを確認します。

OK (3 tests, 3 assertions)

TODOリストの見直し

現在のTODOリストは以下の通りです。

  • L,R,U,Dのいずれかの文字を渡した時、YesまたはNoを返す
    • 文字Rを渡した時、Yesを返す
    • 文字Uを渡した時、Yesを返す
    • 文字Dを渡した時、Yesを返す
    • 文字Lを渡した時、Noを返す
  • 奇数文字目の少なくとも1つがLである場合、Noを返す
    • 文字列UULを渡した時、Noを返す
  • 偶数文字目の少なくとも1つがRである場合、Noを返す
    • 文字列URUを渡した時、Noを返す

ここで1文字目と3文字目をLにしたからといって、99文字目がLの時にNoを返すかはわかりません。「奇数文字目の少なくとも1つがLである場合、Noを返す」ことを満たしていると言えないとも考えられます。

そのときは、TODOリストに項目を追加して、99文字目がLになるようなテストを追加しましょう。

テストケースを追加するにつれ、プログラムは堅牢さを獲得し、その動きは開発者が予測できるものになります。

どこまでテストを書けばいいのか、という疑問にTDD本の著者Kent Beckは「不安がなくなるまで」と答えています。

自分がプログラムに対して不安であれば、テストを追加しましょう。

4周目 - 最後のTODO

いよいよ最後の項目「文字列URUを渡した時、Noを返す」です。

レッド

まずはテストコードを追加して、テストを実行します。

public function 文字列URUを渡した時、Noを返す()
{
    $this->assertSame('No', $this->sample->solve('URU'));
}
1) SampleTest::文字列URUを渡した時、Noを返す
Failed asserting that two strings are identical.
--- Expected
+++ Actual
@@ @@
-'No'
+'Yes'

グリーン

テストがレッドなので、コードを追加します。奇数文字の時と同様に考えましょう。

Sample.php
public function solve(string $input): string
{
    $len = strlen($input);

    for ($i = 0; $i < $len; $i += 2) {
        if ($input[$i] === 'L') {
            return 'No';
        }
    }

   // 偶数文字目を調べるので、インデックスの初期値は1
   for ($i = 1; $i < $len; $i += 2) {
        if ($input[$i] === 'R') {
            return 'No';
        }
    }

    return 'Yes';
}
PHPUnit 8.0.0 by Sebastian Bergmann and contributors.

....                                                                4 / 4 (100%)

Time: 18 ms, Memory: 4.00MB

OK (4 tests, 4 assertions)

テストがグリーンになりました。

リファクタリング

この実装コードでも問題文の条件を満たします。ただ、ループを2回も回していることが気になりますね。

ループの回数を減らすようにリファクタリングをしてみましょう。

public function solve(string $input): string
{
    $len = strlen($input);

    for ($i = 0; $i < $len; $i++) {
        if ($i % 2 === 0 && $input[$i] === 'L') {
            return 'No';
        }
    }

    for ($i = 1; $i < $len; $i += 2) {
        if ($input[$i] === 'R') {
            return 'No';
        }
    }

    return 'Yes';
}
OK (4 tests, 4 assertions)

テストを実行してもグリーンです。リファクタリングを続けます。

public function solve(string $input): string
{
    $len = strlen($input);

    for ($i = 0; $i < $len; $i++) {
        if ($i % 2 === 0 && $input[$i] === 'L') {
            return 'No';
        }
        if ($i % 2 === 1 && $input[$i] === 'R') {
            return 'No';
        }
    }

    return 'Yes';
}
OK (4 tests, 4 assertions)

テストは通っています。ただ、コードを書き換えたことにより、$input[$i]に重複が発生しているので、これを修正します。

public function solve(string $input): string
{
    $len = strlen($input);

    for ($i = 0; $i < $len; $i++) {
        $currentChar = $input[$i];
        if ($i % 2 === 0 && $currentChar === 'L') {
            return 'No';
        }
        if ($i % 2 === 1 && $currentChar === 'R') {
            return 'No';
        }
    }

    return 'Yes';
}
OK (4 tests, 4 assertions)

テストが通っていることを確認して、条件文の重複を取り除きましょう。

public function solve(string $input): string
{
    $len = strlen($input);

    for ($i = 0; $i < $len; $i++) {
        $currentChar = $input[$i];
        if ($i % 2 === 0 && $currentChar === 'L' ||
            $i % 2 === 1 && $currentChar === 'R'
        ) {
            return 'No';
        }
    }

    return 'Yes';
}

今回もテストが通っているので、変更によってプログラムが壊れていないことを確認できました。

OK (4 tests, 4 assertions)

今回はこれで完成とします。AtCoderでは解答を作成する際、必ずしもクラスや関数を作る必要はないからです。

さらなるリファクタリング例

プロダクションコードのレベルであれば、下記のように書き換えてもいいでしょう。

class Sample
{
    public function solve(string $input): string
    {
        $len = strlen($input);

        for ($i = 0; $i < $len; $i++) {
            $currentChar = $input[$i];

            if ($this->isOddCharL($i, $currentChar) ||
                $this->isEvenCharR($i, $currentChar)
            ) {
                return 'No';
            }
        }

        return 'Yes';
    }

    private function isOddCharL($index, $currentChar): bool
    {
        return $index % 2 === 0 && $currentChar === 'L';
    }

    private function isEvenCharR($index, $currentChar): bool
    {
        return $index % 2 === 1 && $currentChar === 'R';
    }
}

もちろんテストは通っています。

OK (4 tests, 4 assertions)

TODOリストの見直し

今回の項目にチェックを入れましょう。

  • L,R,U,Dのいずれかの文字を渡した時、YesまたはNoを返す
    • 文字Rを渡した時、Yesを返す
    • 文字Uを渡した時、Yesを返す
    • 文字Dを渡した時、Yesを返す
    • 文字Lを渡した時、Noを返す
  • 奇数文字目の少なくとも1つがLである場合、Noを返す
    • 文字列UULを渡した時、Noを返す
  • 偶数文字目の少なくとも1つがRである場合、Noを返す
    • 文字列URUを渡した時、Noを返す

これで全てのTODOリストが完了しました。

提出。 - Accepted

AtCoderでは入力は標準入力から得られ、出力は標準出力で行うため、それに合わせて書き換えます。

<?php
$input = trim(fgets(STDIN));
$len = strlen($input);

for ($i = 0; $i < $len; $i++) {
  $currentChar = $input[$i];
  if ($i % 2 === 0 && $currentChar === 'L' ||
      $i % 2 === 1 && $currentChar === 'R'
     ) {
    echo 'No';
    return;
  }
}

echo 'Yes';

プログラムが完成したので、問題を提出してみましょう。

問題に正答したことが記されている

AtCoderで提出した問題がACになりました。やりましたね。

まとめ

TDDはテストの技法ではなく、設計・分析技法だとTDD本の著者Kent Beckは語っています。

また、ソフトウェアが開発者の意図しない挙動をするという不安を軽減するための手段でもあります。

もちろんTDDを実践したから、ソフトウェアが絶対にバグを起こさないとは言えません。

しかし、TDDのステップの中には、プログラムの挙動を変えず内部実装を書き換えるリファクタリングが含まれています。

これにより、読みやすく、メンテナンスコストの低いコードを書くことができるんです。

このリファクタリングを安心して行うことができるのは、テストを頻繁に実行することでソフトウェアと開発者の現在地を確認できるからです。

これが、TDDの目的である**「動作する綺麗なコード」**を生み出すことに繋がります。

TDDはその実践方法が十分に体系化されており、誰でも明日から現場で実践できるものなのです。

ぜひTDDを実践して、堅牢なソフトウェアを構築しましょう。

テストのデメリットについて

上でまとめを書いておいて何ですが、TDDのデメリットとして挙げられることがいくつかあります。

ここではいくつかピックアップして、簡単に自分が考えていることを記述していきます。

「テストを書いていると実装スピードが遅くなる」

バグを混入している可能性があるコードを本番リリースする方が自分は不安です。

リリースまでのスピードは幾分落ちますが、本番リリース後にバグが出た時の障害対応に割く時間を考えると、長期的に見て時間は節約できると自分は考えています。

また、TDDでは初期実装は確かにスピードダウンするものの、メンテナンスコストが下がるため、保守・運用期間の工数が減少するという研究があります。

「テストが増えるとテスト実行時間が遅くなるからTDDはダメだ」

ローカルでの実行なら、@groupアノテーションをつけて、自分が開発している箇所のみテストを実施しましょう。

CI環境なら、テストを並列実行することでテストの実行時間を短縮できます(例:Laravelで並列テストを導入するための道のり)。

「テストのメンテナンスが大変」

実はテストにもメンテナンスコストはあるのですよね。

メソッド名が分かりづらかったり、別のテストに依存するテストを書いていたり、ランダムで落ちるテストがあるとはっきり言って大変です。

TDDではリファクタリングのステップでテストに対する見直しをできるので、その段階で異変に気付いておきたいです。

まずはテストにもメンテナンスコストが発生するということを認識することが第一歩です。

なお上記の批判は、TODOリストを作成してレッド・グリーン・リファクタリングのリズムでプログラムを実装するというTDDの手法の批判ではありません。TDDという手法ではなく、テストコード一般に当てはまるものです。

TDD云々以前に、まずはテストコードに対する理解を深めることが先なのでしょう。

プロのプログラマはTDDを実践している

上記の批判を額面通り受け取り、テストコードを書かないことはデメリットが大きいのではないでしょうか。

それは「早く実装できるが汚いコードを書いた上に、バグをユーザーに届けてしまう」ことにつながるからです。

クリーンアーキテクチャで有名なボブおじさんも著書「Clean Coder」の中で、ソフトウェアのプロとして備えるべき最低限のことの一つにTDDを挙げています。

ボブおじさんはテストを書くのが面倒だと思った時には、左腕につけているグリーンバンド(テスト駆動開発者の証)を見て、自分はプロのプログラマだからテストを書くのだと自分を奮い立たせるそうです。

ソフトウェアのバグ混入率を低下させ、ソフトウェアが安定して動作する綺麗なコードを安心して書き続けたいのであれば、TDDは必ずあなたの力になるでしょう。

TDD設計AtCoder
プログラミングをするパンダ
プログラミングをするパンダ (@Panda_Program)
Software Engineer