0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

『脳に収まるコードの書き方』4章をGoで手を動かして理解する。バーティカルスライスとウォーキングスケルトン

0
Last updated at Posted at 2026-07-12

この記事で分かること

  • 新規開発の最初の一歩は「レイヤーを1枚ずつ」ではなく「縦に薄く1本」
  • 最初の1本(ウォーキングスケルトン)は薄すぎると感じるくらいでいい
  • Goで書いた最小のバーティカルスライスを、テスト付きでそのまま動かせる

はじめに

新規開発の初日。
要求は山ほどある。
DB設計が先か、認証が先か、共通ライブラリが先か。
議論は白熱し、設計書は増え、動くものはまだ何もない。

Mark Seemann は『脳に収まるコードの書き方』1でこの状態を「分析麻痺」と呼びました。
処方箋はバーティカルスライス
いちばんシンプルな機能を1つ選び、UIからデータストアまで縦に貫いて全部実装し、さっさとデプロイするやり方です。

この記事は同書4章の学びの整理です(2026年7月時点)。
書籍のC#の例をGoに置き換えて、手元で動く最小のスライスを作ります。

レイヤーではなく、縦に切る

ほとんどのソフトウェアは、構造だけ見ればこの3段に収まります。

よくあるアーキテクチャは、これを水平のレイヤーに整理します。
すると「まずデータアクセス層、次にビジネスロジック層……」と、下から1枚ずつ作りたくなる。
でも、その順番だと全レイヤーが揃うまで誰も動くものを見られません。

バーティカルスライスは切り方を縦に変えます。
単一の機能は、すべてのレイヤーをまたぐ縦の1切れ
その1切れだけを先に完成させれば、初日から「動くソフトウェア」が手に入り、ビルドからデプロイまでの仕組みもそこで検証できます。

この「最初の動く1本」にはウォーキングスケルトン(歩く骸骨)という名前が付いています2
肉(本格的な機能)はまだ付いていないのに、骨格だけで歩いている。
薄すぎると感じるくらいでちょうどいい。

動く1本を通すのを邪魔するのは、レイヤーの積み上げだけではありません。
もうひとつが投機的な一般性
「いつか必要になるかもしれない」だけを根拠に、汎用化を先回りして作り込むことです(呼び出し元が1つなのに抽象を何段も切る、誰も使わない設定オプションを「念のため」用意する)。
動く1本を先に通せば、どのコードが本当に必要かは実物で分かります。
完璧な部品より、まず動く1本。

進め方は外側から内側へ

本書の進め方はアウトサイドイン
システムの外側(HTTP境界)に対するテストを先に書き、それを通すのに必要なものを内側へ掘り進めます。

  1. 境界に対する失敗するテストを書く(POSTして201を期待)
  2. DBの代役としてちゃんとふるまうインメモリのフェイクオブジェクトを差し込み、テストを通す
  3. 永続化をインターフェイスとして抽出する
  4. 本物の実装(DB・ファイル)に差し替える
  5. 手動のスモークテストで火を噴かないか確かめる

このとき本書が守る経験則は3つ。

  • データを入れる機能から選ぶ:読み出す機能を先に作っても、読むデータがない
  • まずハッピーパスだけを狙う:バリデーションもロギングも後回し(やらないのではなく、メモして後でやる)
  • DTOとドメインモデルを分ける:入力JSONを写し取るだけの入れ物と、ビジネスルールを持つモデルは役割が違う

手を動かす

同じ構造をGoの標準ライブラリだけで作ります。
題材はTODOの登録API。
HTTP境界からドメインモデル、リポジトリインターフェイス、永続化までの縦の1本と、フェイクを使った境界テストが、2ファイルで完結します。

main.go
package main

import (
	"encoding/json"
	"log"
	"net/http"
	"os"
)

// Todo はドメインモデル。今はタイトルだけの最小構成
type Todo struct {
	Title string `json:"title"`
}

// TodoRepository は永続化の抽象。ドメイン側がインターフェイスを持ち、
// 実装(本物のDBでもフェイクでも)がこれに従う
type TodoRepository interface {
	Create(todo Todo) error
}

// todoDto は入力JSONの値を写し取るだけの入れ物(DTO)。
// バリデーション前の「信用できない」データを受けるためだけに使う
type todoDto struct {
	Title string `json:"title"`
}

// Server はHTTP境界。リポジトリはコンストラクターインジェクションで受け取る
type Server struct {
	repo TodoRepository
}

func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	// 今回のスライスは「TODOを1件登録する」だけ。それ以外は404でよい
	if r.Method != http.MethodPost || r.URL.Path != "/todos" {
		http.NotFound(w, r)
		return
	}
	var dto todoDto
	if err := json.NewDecoder(r.Body).Decode(&dto); err != nil || dto.Title == "" {
		w.WriteHeader(http.StatusBadRequest)
		return
	}
	// DTOからドメインモデルへ詰め替えてから保存する
	if err := s.repo.Create(Todo{Title: dto.Title}); err != nil {
		w.WriteHeader(http.StatusInternalServerError)
		return
	}
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(http.StatusCreated)
	json.NewEncoder(w).Encode(dto)
}

// FileRepository が「本物」の永続化。1行1JSONでファイルに追記するだけの最小実装。
// ここをRDBに替えても、Server側は一切変わらない
type FileRepository struct {
	path string
}

func (f *FileRepository) Create(todo Todo) error {
	// O_APPEND: 追記モード / O_CREATE: なければ作る
	fp, err := os.OpenFile(f.path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
	if err != nil {
		return err
	}
	defer fp.Close()
	return json.NewEncoder(fp).Encode(todo)
}

func main() {
	srv := &Server{repo: &FileRepository{path: "todos.jsonl"}}
	log.Println("listening on :8080")
	log.Fatal(http.ListenAndServe(":8080", srv))
}
main_test.go
package main

import (
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
)

// FakeRepository はデータベースのふりをするフェイクオブジェクト(インメモリ)。
// テストの検証フェーズで「DBの状態」として中身を覗ける
type FakeRepository struct {
	todos []Todo
}

func (f *FakeRepository) Create(todo Todo) error {
	f.todos = append(f.todos, todo)
	return nil
}

func TestPostTodoReturnsCreated(t *testing.T) {
	// Arrange: フェイクを差し込んだサーバーを立てる
	fake := &FakeRepository{}
	ts := httptest.NewServer(&Server{repo: fake})
	defer ts.Close()

	// Act: 有効なTODOをポストする(まずはハッピーパスだけ)
	res, err := http.Post(ts.URL+"/todos", "application/json",
		strings.NewReader(`{"title":"hogehoge"}`))
	if err != nil {
		t.Fatal(err)
	}

	// Assert: ステータスコードと「DB」の状態を検証する
	if res.StatusCode != http.StatusCreated {
		t.Errorf("status = %d, want %d", res.StatusCode, http.StatusCreated)
	}
	if len(fake.todos) != 1 || fake.todos[0].Title != "hogehoge" {
		t.Errorf("todos = %+v, want 1 item titled hogehoge", fake.todos)
	}
}

実行手順(Go 1.26.4 で動作確認済み。標準ライブラリしか使っていないので、少し古いGoでも動くはずです)。

mkdir /tmp/slicedemo && cd /tmp/slicedemo
# ↑に main.go と main_test.go を置く
go mod init slicedemo
go test -v

期待される出力:

=== RUN   TestPostTodoReturnsCreated
--- PASS: TestPostTodoReturnsCreated (0.00s)
PASS

自動テストが通っても、手動のスモークテストは省略しません。
サーバーを起動して、実際に火を噴かないか確かめます。

go run . &
curl -i http://localhost:8080/todos \
  -H "Content-Type: application/json" \
  -d '{"title":"hogehoge"}'

期待される出力:

HTTP/1.1 201 Created
Content-Type: application/json

{"title":"hogehoge"}

todos.jsonl に1行追記されていれば、HTTP境界から永続化までの縦の1本が通っています。
テストではフェイク、本番構成ではファイル。
TodoRepository の実装を差し替えるだけで、同じ Server が両方で動きます。
この構造がスライスの肝です。

まとめ

  • 最初の一歩は、レイヤーを1枚ずつではなく、UIからデータストアまで縦に薄く1本
  • 最初の1本は薄すぎると感じるくらいでいい。その時点でパイプラインの検証が済んでいる
  • 境界のテストを先に書き、フェイクで通し、本物へ差し替える。この順番がコードを引っ張る

効くのは新規開発やアーキテクチャに不確実性がある場面で、枯れた既存システムへの小さな追加にはありがたみが薄い。
『脳に収まるコードの書き方』4章の実践部分だけを切り出しました。
チェックリストや数値化の話も含めて、本編はもっと濃いです1

参考文献

  1. 脳に収まるコードの書き方(Mark Seemann 著、吉羽龍太郎・原田騎郎 訳、オライリー・ジャパン、2024年) — 参照日 2026-07-12 2

  2. Start with a Walking Skeleton(Matt Blodgett) — 参照日 2026-07-12(用語を定義したアリスター・コーバーンの原文の引用元)

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?