面接のフレームワーク — 4ステップで攻略する
「地図なしで迷子になっていた」
転職活動を始めて3週間が経ったソウタ(28歳、Webエンジニア歴4年)は、2社目の面接で完全に沈黙した。
「先週の面接、もう一度再現してみて」とレイカが言った。
「え……『Twitterを設計してください』と言われて、『データベースはPostgreSQLで……』と話し始めました。」
「そこで詰んでる。問題を渡された瞬間に設計を始めようとしたでしょ。それが間違い。」
「でも早く答えないとダメだと思って……」
「面接官はスピードを見ているんじゃない。思考のプロセスを見ている。」
レイカはホワイトボードに書いた。
❌ 問題を聞いた → 即座に設計を語る
✅ 問題を聞いた → まず地図を広げる
「システム設計面接には正しい順序がある。その順序を守るだけで、評価が大きく変わる。」
4ステップフレームワーク
「これが基本の地図。面接が60分なら、このくらいの時間配分で進める。守るだけで、少なくとも『どこから手をつければいいかわからない』という状態は回避できる。」
ソウタはメモを取った。合計40分。残りの20分はバッファと面接官の質問時間だ。
Step 1: 要件の明確化(5分)
「最初の5分は絶対に設計を始めない。要件を確認する時間だ。」
ソウタは驚いた。「5分も?」
「そう。むしろここを短縮したら負けだと思って。面接官はあなたが正しい問いを立てられるかを見ている。Amazonでシニアエンジニアを面接していたとき、いきなり設計を始める人の合格率は2割以下だった。」
機能要件 vs 非機能要件
要件には2種類ある。どちらを確認するかで、設計の方向性が完全に変わる。
機能要件(What — ユーザーが何をできるか)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
- コアな機能は何か?
- ユーザーができることは?(投稿する、検索する、フォローする)
- スコープに含まれないものは?(明示的に除外する)
非機能要件(How — システムがどう動くか)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
- ユーザー数・トラフィック規模は?(DAU/MAU)
- 可用性の要件は?(SLA: 99.9% vs 99.99%)
- レイテンシの要件は?(p99で何ミリ秒以内?)
- 一貫性の要件は?(強一貫性 vs 結果整合性)
- データ保持期間は?(ログを何年保つ?)
- 地理的要件は?(グローバル vs 単一リージョン)
INFO
機能要件と非機能要件は対称ではない。機能要件はスコープを絞るために使い、非機能要件は設計のボトルネックを予測するために使う。「可用性が99.9%でいい」と言われたら、Active-Activeのマルチリージョン設計は過剰だ。要件が制約を定義し、制約が設計を決める。
要件確認の技術 — ファンネル形式で絞り込む
闇雲に質問するのではなく、ファンネル形式で絞り込む。
1. まず機能の中心を確認する
「このシステムで最も重要なコア機能は何ですか?」
2. スコープ外を明確にする
「今日の設計に含めなくてよいものを確認させてください。
リアルタイム通知は? 検索機能は? 分析ダッシュボードは?」
3. 非機能要件でスケールを把握する
「想定するユーザー規模を教えてください。
DAUはどのくらいですか? 地理的な分布は?」
4. 重要な制約を確認する
「レイテンシの要件はありますか?
データの一貫性はどの程度重要ですか?」
実際の要件確認ダイアログ
面接官から「Twitterを設計してください」と言われたとき:
ソウタ:「ありがとうございます。いくつか確認させてください。
まずコア機能についてです。
タイムラインの表示は必須ですか?(面接官:Yes)
フォロー/フォロワー機能は必要ですか?(Yes)
いいね、リツイートはスコープ内ですか?
(今日はタイムラインに集中しましょう)
DMは?(対象外) 画像・動画の投稿は?(テキストのみでOK)
次に規模についてです。
ユーザー規模は?(1億DAU)
読み書きの比率はどのくらいを想定しますか?(100: 1)
最後に非機能要件を確認します。
タイムラインのレイテンシ要件は?(500ms以内)
可用性は?(99.9%以上)
データの整合性は?(結果整合性でOK)
了解しました。では要件をまとめます。
機能スコープ:ツイート投稿とホームタイムライン閲覧
規模:1億DAU、読み書き比率100:1
非機能:500ms以内、99.9%可用性、結果整合性
この理解で進めていいですか?」
# Railsモデルで要件を構造化して考える例
# 確認した要件をコードで表現すると思考が整理される
# 機能要件 → モデルの責務に対応する
class Tweet < ApplicationRecord
belongs_to :user
validates :content, presence: true, length: { maximum: 280 }
# 画像は今回スコープ外 → has_many :media_attachments は不要
end
class Follow < ApplicationRecord
belongs_to :follower, class_name: 'User'
belongs_to :followed, class_name: 'User'
validates :follower_id, uniqueness: { scope: :followed_id }
end
# 非機能要件 → インフラの制約に対応する
# DAU 1億、読み100:書き1 → キャッシュ重視の設計が必要
# レイテンシ500ms → データベース直接アクセスは避ける
# 結果整合性OK → タイムラインに多少の遅延は許容できるWARNING
要件確認でよくある失敗:「何でも実装します」と言うこと。良い候補者はスコープを絞る。全部作るより、コア機能を完璧に設計する方が高評価につながる。「DMもリアルタイム通知も実装します」と言った瞬間、面接官は「この人は優先順位をつけられない」と判断する。
Step 2: 規模の概算(5分)
「次は数字。エンジニアは感覚で話してはいけない。スケールを数字で根拠を持って語るのがプロだ。」
「でも、具体的な数字なんてわからないですよね?」
「わからなくていい。オーダーオブマグニチュード(桁の精度)で考えれば十分。間違えるとしたら2倍か0.5倍程度の範囲で。10倍ずれなければ設計の方向性は変わらない。」
基本の数字を頭に入れる
# 面接前に暗記しておく数字(Rubyの定数として整理)
module ScaleConstants
# 時間
SECONDS_PER_DAY = 86_400 # ≈ 10万秒と覚える
SECONDS_PER_MONTH = 2_592_000 # ≈ 300万秒
SECONDS_PER_YEAR = 31_536_000 # ≈ 3000万秒
# データサイズの目安
TWEET_BYTES = 300 # テキストツイート1件
USER_RECORD_BYTES = 1_000 # ユーザーレコード1件
PHOTO_BYTES = 1_000_000 # 写真1枚(圧縮後)
# レイテンシ(Latency Numbers Every Programmer Should Know)
L1_CACHE_NS = 1 # L1キャッシュ参照: 1ns
MEMORY_ACCESS_NS = 100 # メモリアクセス: 100ns
SSD_READ_US = 100 # SSD読み取り: 100μs
HDD_READ_MS = 10 # HDD読み取り: 10ms
NETWORK_ROUND_MS = 150 # ネットワーク往復: 150ms(大陸間)
endTwitterの場合の計算例
DAU: 1億 = 100M
ツイート投稿(書き込み)
━━━━━━━━━━━━━━━━━━━━━━━━
1日のツイート数: 100M DAU × 平均1ツイート = 100M tweets/day
QPS (書き込み): 100M / 86,400秒 ≒ 1,200 tweets/sec
ピーク時 (×5): 1,200 × 5 = 6,000 tweets/sec
タイムライン閲覧(読み込み)
━━━━━━━━━━━━━━━━━━━━━━━━
読み: 書き = 100: 1
読み込みQPS: 1,200 × 100 = 120,000 req/sec
ピーク時: 600,000 req/sec
ストレージ(テキストのみ)
━━━━━━━━━━━━━━━━━━━━━━━━
ツイート1件: 約300バイト
1日のストレージ: 100M × 300B = 30GB/day
5年間: 30GB × 365 × 5 ≒ 55TB
帯域
━━━━━━━━━━━━━━━━━━━━━━━━
読み込みQPS 120,000 × 300B = 36MB/sec
ピーク時: 180MB/sec ≈ 1.5Gbps
// Golangで概算計算を実装する例(面接中のメモとして使える)
package main
import "fmt"
type TwitterScale struct {
DAU int64
ReadWriteRatio int64
TweetSizeBytes int64
PeakMultiplier int64
}
func (s *TwitterScale) WriteQPS() int64 {
tweetsPerDay := s.DAU * 1 // 1ユーザー1日1ツイート
return tweetsPerDay / 86400
}
func (s *TwitterScale) ReadQPS() int64 {
return s.WriteQPS() * s.ReadWriteRatio
}
func (s *TwitterScale) StoragePerYear() int64 {
tweetsPerDay := s.DAU * 1
return tweetsPerDay * s.TweetSizeBytes * 365
}
func main() {
scale := &TwitterScale{
DAU: 100_000_000,
ReadWriteRatio: 100,
TweetSizeBytes: 300,
PeakMultiplier: 5,
}
fmt.Printf("Write QPS: %d/sec\n", scale.WriteQPS())
// ~1,157/sec
fmt.Printf("Read QPS: %d/sec\n", scale.ReadQPS())
// ~115,700/sec
fmt.Printf("Storage/Year: %.0f GB\n",
float64(scale.StoragePerYear())/1_000_000_000)
// ~10.9 TB
}INFO
計算は声に出しながらやること。面接官は答えではなく「どう考えているか」を見ている。「DAUが1億なので、1日の秒数が約10万、つまりQPSは……」と言いながら計算する。静かに計算して数字だけ出すのはNG。思考プロセスを見せるのが目的だ。
時間配分の失敗パターン
概算ステップでよく起こる失敗がある。
❌ 失敗パターン1:精度にこだわりすぎる
「正確なQPSを出すために、もう少し詳しい情報が……」
→ 桁の精度でいい。5分を超えたら次のステップへ。
❌ 失敗パターン2:計算をスキップする
「まあ大規模なので、Redisを使えばいいですね」
→ 数字なしに「大規模」と言っても意味がない。
❌ 失敗パターン3:全部を計算しようとする
「帯域も計算して、レイテンシも……」
→ QPS・ストレージ・帯域の3点を押さえれば十分。
✅ 正しいアプローチ
「概算なので多少誤差はありますが……」と断った上で
桁の精度で計算し、設計の方向性を確定させる。
Step 3: 高レベル設計(20分)
「ここからが本番。でもいきなり細部に入ってはいけない。まず全体の絵を描く。」
コンポーネントの「引き出し」を整理する
高レベル設計では、主要コンポーネントから選んで組み合わせる。それぞれの「使いどころ」を覚えておく。
各コンポーネントの使いどころ:
CDN(コンテンツ配信ネットワーク)
━━━━━━━━━━━━━━━━━━━━━━━━━━
使うとき:静的アセット(画像、JS、CSS)、地理的に分散したユーザー
例:S3 + CloudFront、Cloudflare
効果:オリジンへのトラフィックを90%削減できる
ロードバランサー
━━━━━━━━━━━━━━━━━━━━━━━━━━
使うとき:APIサーバーを複数台に横展開するとき(常に必要)
種別:L4(TCP)vs L7(HTTP)。APIなら通常L7
例:AWS ALB、nginx
キャッシュ(Redis/Memcached)
━━━━━━━━━━━━━━━━━━━━━━━━━━
使うとき:読み込みが多い、DBがボトルネック、計算コストが高い結果
種別:Read-Through、Write-Through、Write-Behind
注意:キャッシュ無効化戦略を必ず説明する
メッセージキュー(Kafka/SQS)
━━━━━━━━━━━━━━━━━━━━━━━━━━
使うとき:非同期処理、サービス間の疎結合、バースト対応
例:ツイート投稿→タイムライン配信(ファンアウト)
効果:ピーク時のスパイクを吸収できる
データベース(RDB vs NoSQL)
━━━━━━━━━━━━━━━━━━━━━━━━━━
RDB(PostgreSQL/MySQL):関係性が複雑、トランザクションが必要
NoSQL(DynamoDB/Cassandra):スケール重視、スキーマが柔軟
両方使う:ユーザー認証はRDB、タイムラインデータはNoSQL
INFO
コンポーネントを追加するたびに「なぜそれが必要か」を説明する。「Kafkaを使います」で終わるのではなく、「ツイート投稿とタイムライン配信を非同期にすることで、投稿者のレスポンスタイムを下げつつ、フォロワー数が多いユーザーのファンアウト処理をバックグラウンドで行えます」と語る。
Twitterの高レベル設計
「絵を描いたら、各コンポーネントの役割を1文で説明する。長々と語らなくていい。」
# Tweet API: ツイートの投稿と取得
class TweetsController < ApplicationController
def create
tweet = current_user.tweets.create!(tweet_params)
# Kafkaにイベントを送信して非同期でファンアウト
TweetPublishedEvent.publish(tweet_id: tweet.id, user_id: tweet.user_id)
render json: tweet, status: :created
end
private
def tweet_params
params.require(:tweet).permit(:content)
end
end
# Timeline API: Redisから事前構築済みタイムラインを返す
class TimelinesController < ApplicationController
def show
# カーソルベースのページネーションで一貫性を保つ
tweet_ids = redis.lrange(
"timeline:#{current_user.id}",
0,
(params[:limit] || 20).to_i - 1
)
tweets = Tweet.where(id: tweet_ids).index_by(&:id)
render json: tweet_ids.map { |id| tweets[id.to_i] }.compact
end
end// Golang: Worker(Kafkaコンシューマー)でのファンアウト実装例
package worker
import (
"context"
"fmt"
"github.com/segmentio/kafka-go"
)
type FanoutWorker struct {
reader *kafka.Reader
redisPool RedisPool
db Database
}
func (w *FanoutWorker) Run(ctx context.Context) {
for {
msg, err := w.reader.ReadMessage(ctx)
if err != nil {
continue
}
tweetID := parseTweetID(msg.Value)
w.fanout(ctx, tweetID)
}
}
func (w *FanoutWorker) fanout(ctx context.Context, tweetID int64) {
// フォロワー一覧を取得してそれぞれのタイムラインに追加
followers := w.db.GetFollowers(ctx, tweetID)
for _, followerID := range followers {
key := fmt.Sprintf("timeline:%d", followerID)
// LPUSHで先頭に追加、LTRIMで上限管理
w.redisPool.LPush(ctx, key, tweetID)
w.redisPool.LTrim(ctx, key, 0, 999) // タイムラインは1000件まで保持
}
}APIエンドポイントも定義する
設計の一部としてAPIを定義すると、コンポーネント間のインターフェースが明確になる。
POST /v1/tweets
Request: { "content": "Hello world" }
Response: { "id": 123, "user_id": 456,
"content": "...", "created_at": "..." }
GET /v1/timelines/home?cursor=<tweet_id>&limit=20
Response: { "tweets": [...], "next_cursor": <tweet_id> }
# Rails routesとして表現する
Rails.application.routes.draw do
namespace :v1 do
resources :tweets, only: [:create, :show, :destroy]
resource :timeline, only: [:show]
end
end
# カーソルベースのページネーション実装
class V1::TimelineController < ApplicationController
def show
cursor_tweet_id = params[:cursor]&.to_i
limit = (params[:limit] || 20).to_i.clamp(1, 100)
tweet_ids = if cursor_tweet_id
# カーソル以降のツイートIDをRedisから取得
all_ids = redis.lrange("timeline:#{current_user.id}", 0, -1)
start_idx = all_ids.index(cursor_tweet_id.to_s) || 0
all_ids[start_idx + 1, limit]
else
redis.lrange("timeline:#{current_user.id}", 0, limit - 1)
end
tweets = Tweet.where(id: tweet_ids).order(created_at: :desc)
render json: {
tweets: tweets,
next_cursor: tweets.last&.id
}
end
endStep 4: 深掘りと改善(10分)
「最後は面接官と対話しながら、特定の部分を深掘りする。ここが一番差がつくフェーズだ。」
「どうやって面接官の関心を探るんですか?」
「直接聞けばいい。」
面接官との対話の技術
深掘りフェーズは相互の会話だ。面接官の関心を引き出してから、そこに集中する。
ステップ1:主導権を渡す
「今のデザインで気になる点があれば教えてください。
どのコンポーネントを深掘りしたいですか?」
ステップ2:懸念を受け止める
「なるほど、タイムラインのスケーラビリティが懸念ですね。
それについて詳しく話しましょう。」
ステップ3:トレードオフを語る
「現在のFan-out-on-writeアプローチには〇〇という問題があります。
代替としてFan-out-on-readが考えられますが、トレードオフとして
読み込み時の遅延が増加します。今回の要件(500ms以内)を考えると……」
ステップ4:自分の判断を示す
「私は〇〇を選択します。理由は……」
面接官のフィードバック会話
実際の面接官はこんなふうにプッシュしてくる。
面接官: 「セレブリティアカウント(フォロワー数千万人)がツイートしたとき、Fan-out-on-writeではどうなりますか?」
ソウタ: 「そこは懸念点です。フォロワーが1000万人いると、ファンアウトに1000万件の書き込みが必要になります。計算すると、1000万件 × 300バイト = 3GBのRedis書き込みが発生します。Workerのスループットに問題が出ます。」
面接官: 「どう解決しますか?」
ソウタ: 「2つのアプローチがあります。ひとつはハイブリッド方式です。フォロワー数が閾値(例:100万人)を超えるアカウントはFan-out-on-readに切り替え、タイムライン取得時にマージします。もうひとつは優先キューです。セレブリティのファンアウトを低優先度キューに分離して、通常ユーザーへの影響を排除します。私は前者を選択します。実装が複雑になりますが、読み込みQPS(120,000/sec)を考えると、セレブリティのリアルタイム性を多少犠牲にしてでもスループットを確保する方が合理的です。」
# ハイブリッド方式のタイムライン取得
class HybridTimelineService
CELEBRITY_THRESHOLD = 1_000_000
def get_home_timeline(user_id, limit: 20)
# 1. Redisから通常ユーザーのプリビルドタイムラインを取得
cached_ids = redis.lrange("timeline:#{user_id}", 0, limit * 2 - 1)
# 2. フォロー中のセレブリティのツイートをDBから直接取得
celebrity_followings = Follow
.where(follower_id: user_id)
.joins(:followed)
.where("users.followers_count > ?", CELEBRITY_THRESHOLD)
.pluck(:followed_id)
celebrity_tweets = Tweet
.where(user_id: celebrity_followings)
.order(created_at: :desc)
.limit(limit)
# 3. マージしてソート
cached_tweets = Tweet.where(id: cached_ids)
(cached_tweets + celebrity_tweets)
.uniq(&:id)
.sort_by { |t| -t.created_at.to_i }
.first(limit)
end
endよくある深掘りポイントと回答の型
単一障害点(SPOF)
「現在の設計でSPOFはAPIサーバーです。
ロードバランサーの背後に複数台配置し、ヘルスチェックで
障害ノードを自動除外します。Redisはレプリカセットを組みます。」
データベースのスケーリング
「最初はリードレプリカで読み込みをスケール。
書き込みがボトルネックになったらシャーディングを検討します。
Twitterの場合、user_idでシャーディングするとツイートの
局所性が保たれます。」
キャッシュの無効化
「今回のタイムラインキャッシュはWrite-Throughです。
ツイートが削除された場合、Workerがタイムラインから該当IDを
削除します。厳密なリアルタイム性より結果整合性を選択しています。」
WARNING
深掘りフェーズの注意点:面接官に誘導されて設計を全部作り直そうとしてはいけない。「その懸念はもっともです。こういった対策が考えられますが、トレードオフとして〇〇があります」と落ち着いてトレードオフを語る。完璧な設計は存在しない——それを面接官は知っている。あなたが「問題を認識して、理由を持って選択できるか」を見ている。
トレードオフの語り方テンプレート
面接で使えるトレードオフの語り方。これを覚えておくと迷わない。
「[選択肢A] と [選択肢B] があります。
[選択肢A] は [メリット] がありますが、[デメリット] という
トレードオフがあります。
今回の要件 [要件の引用] を考慮すると、
私は [選択] を選択します。
理由は [理由] です。」
具体例:
「Fan-out-on-write(プッシュ型)とFan-out-on-read(プル型)があります。
プッシュ型は読み込み時が高速ですが、フォロワー数が多いと
書き込みコストが高くなるトレードオフがあります。
今回の要件(タイムライン表示500ms以内、読み書き比率100:1)を
考慮すると、私はプッシュ型をベースに、セレブリティアカウントは
例外的にプル型を混在させるハイブリッド方式を選択します。
理由は読み込みが圧倒的に多い場合、プッシュ型の方がユーザー体験
に直結するからです。」
4ステップを練習する方法
「フレームワークを知っただけでは使えない。体に染み込ませる必要がある。」
「どう練習すればいいですか?」
「Mock面接を週2回やれ。自分でホワイトボードに向かうか、友人と組む。」
Mock面接の進め方
準備(5分)
━━━━━━━━━━━━━━━━━━━━━━━━
- タイマーを用意する(Step別に管理)
- ホワイトボード or 紙を用意する
- 設計する問題を1つ選ぶ
実践(40分)
━━━━━━━━━━━━━━━━━━━━━━━━
Step 1(5分タイマー):声に出して要件確認
Step 2(5分タイマー):計算を声に出しながら概算
Step 3(20分タイマー):コンポーネント図を描きながら説明
Step 4(10分タイマー):ボトルネックを1つ選んで深掘り
振り返り(15分)
━━━━━━━━━━━━━━━━━━━━━━━━
- どのステップで時間をオーバーしたか?
- トレードオフを語れたか?
- 数字は出せたか?
# 練習ログを記録するモデル(実際に記録をつけると上達が早い)
class MockInterviewSession
SCORING_CRITERIA = {
requirements_clarity: "要件確認でスコープを絞れたか",
scale_estimation: "数字で根拠を示せたか",
component_design: "コンポーネントの役割を説明できたか",
tradeoff_discussion: "トレードオフを語れたか",
time_management: "各ステップの時間を守れたか"
}.freeze
attr_reader :problem, :scores
def initialize(problem)
@problem = problem
@scores = SCORING_CRITERIA.keys.each_with_object({}) { |k, h| h[k] = 0 }
end
def evaluate(criterion, score_0_to_5)
@scores[criterion] = score_0_to_5
end
def weakest_area
scores.min_by { |_, v| v }.first
end
def report
SCORING_CRITERIA.each do |key, label|
puts "#{label}: #{scores[key]}/5"
end
puts "最も改善が必要な領域: #{SCORING_CRITERIA[weakest_area]}"
end
end推奨する練習問題(難易度順)
入門
━━━━━━━━━━━━━━━━━━━━━━━━
- URL短縮サービス(bit.ly)
- ペーストビン(pastebin.com)
中級
━━━━━━━━━━━━━━━━━━━━━━━━
- チャットシステム(LINE/Slack)
- 検索オートコンプリート
- レート制限システム
上級
━━━━━━━━━━━━━━━━━━━━━━━━
- Twitterのタイムライン
- YouTubeの動画配信
- Uber のリアルタイムマッチング
フレームワークをまとめると
# 面接の進め方(擬似コード)
def system_design_interview(problem)
# Step 1: 要件確認(5分)
# ファンネル形式で機能→スコープ外→非機能の順に確認
requirements = clarify_requirements(problem)
confirm_understanding(requirements) # 「この理解で進めていいですか?」
# Step 2: 規模概算(5分)
# 声に出しながら計算。桁の精度でいい
scale = estimate_scale(requirements)
# QPS、ストレージ、帯域の3点を押さえる
# Step 3: 高レベル設計(20分)
# 4ボックス(クライアント→LB→サービス→DB)から始める
design = high_level_design(requirements, scale)
# コンポーネントを追加するたびに「なぜ」を説明する
# Step 4: 深掘り(10分)
# 面接官の関心を聞いてから、トレードオフを語る
deep_dive = refine_with_tradeoffs(design, interviewer_concerns)
# 「〜という懸念はもっともです。A vs Bのトレードオフとして……」
end「地図が手に入った気がします」
セッションを終えて、ソウタは言った。
「なるほど。前回の面接は要件確認を完全に飛ばして、いきなり実装を考えていました。それで詰まったんですね。」
「そう。そして数字も出さなかった。面接官には『この人はスケールのことを考えていない』と見えていたはず。要件確認をしないエンジニアは、ゴールがわからないまま走るのと同じだ。」
「4ステップを守れば、次は大丈夫でしょうか。」
レイカは少し笑った。「守るだけじゃ足りない。フレームワークは地図であって、目的地じゃない。大事なのはトレードオフを語る力——なぜその設計を選んだか、何を犠牲にしたかを説明できることだ。」
「次の課題は何ですか?」
「数字で考える力を鍛えること。計算が苦手でも、桁の感覚さえあれば面接は乗り越えられる。毎日1つ、身近なサービスのQPSを概算してみな。LINEのメッセージ送信数は? Netflixのストリーミング帯域は? それを繰り返すだけで、半月後には感覚が変わる。」
ソウタはノートに書いた。「今日のLINEのQPS概算」——これが次のホームワークだ。
次章では、バックオブエンベロープ計算——あらゆる設計の基礎となる「概算の技術」を体系的に学ぶ。