はじめに
プログラミング学習を始めて8ヶ月、Rubyを中心にプログラミング学習してきました。
平澤章さんの『オブジェクト指向でなぜつくるのか』(第3版)の第3章を読んだので、自分が理解したことのアウトプットとして学んだことを共有します。
本書の3章は「OOPの前史」、つまり構造化プログラミング時代までの歴史を扱う章です。最初は「歴史パートだから」と読み飛ばそうとしましたが、4章以降の理解に必須だと気づき、戻って読み直しました。
この記事では、3章で学んだ概念を、自分が開発しているRailsアプリのコードに当てはめながら整理します。同じ本を読んでいる方、これから読む方の参考になれば嬉しいです。
想定読者
- 『オブジェクト指向でなぜつくるのか』を読んでいる/読みたい人
- RubyやRailsを学習中で「クラスがなぜ存在するか」をしっくり理解したい人
- 構造化プログラミングとOOPの違いを知りたい人
用語の整理
本記事では本の用語に合わせて「OOP」と表記しますが、これは Object-Oriented Programming の略で、オブジェクト指向プログラミングのことです。
ざっくり言えば、ソフトウェアを整理するための設計の考え方で、ソフトウェアの保守や再利用をしやすくすることを重視した技術です。
Ruby、Java、Python など多くの言語で採用されています。
3章の核心:構造化プログラミングが残した2つの宿題
平澤さんは3章で、プログラミング言語の進化を前半と後半で性質が変わると整理しています。
-
前半:表現力の向上を追求した時代
機械語 → アセンブリ言語 → 高級言語(FORTRAN / COBOL)
人間にとって読み書きしやすい記述方法へと進化しました。
-
後半:保守性・再利用性の向上へと方向転換
高級言語 → 構造化言語(Pascal / C) → OOP(Smalltalk / C++ / Java)
ハードウェアの進化とともに、できあがったプログラムを「いかに保守し、再利用するか」が重要な課題になり、進化の方向が変わります。
そして3章の最後で、構造化プログラミングが解決できなかった2つの課題を挙げます。
課題① グローバル変数問題
構造化言語ではローカル変数や値渡しの仕組みが導入され、サブルーチン間の情報共有を最小限にできるようになりました。
ただしローカル変数はサブルーチンを抜けると消えるため、サブルーチンの実行期間を超えて保持したい情報は、結局グローバル変数に置くしかありませんでした。
グローバル変数はプログラム全体のどこからでもアクセスできるため、変更時には影響範囲を確かめるためにロジックをすべて調べなければなりません。
―― 平澤章『オブジェクト指向でなぜつくるのか』(第3版)
課題② 貧弱な再利用
構造化言語で再利用できるのはサブルーチンだけでした。汎用ライブラリ(入出力、文字列処理など)は提供されたものの、増大するアプリ規模に対しては効果が限定的でした。
共通部分として作れるのがサブルーチンだけだったことが大きな原因でした。
―― 平澤章(同書)
構造化プログラミングとOOPの違い
3章の前半で構造化プログラミングの貢献と限界を学び、後半で「これらをOOPが解決した」と予告されます。両者を表で整理すると以下のようになります。
| 観点 | 構造化プログラミング | OOP(オブジェクト指向) |
|---|---|---|
| 代表的な言語 | Pascal、C言語 | Smalltalk、C++、Java、Ruby |
| 登場時期 | 1970年代 | 1980〜90年代に普及 |
| 主な単位 | サブルーチン(関数) | クラス(データ + 処理) |
| データの管理 | 変数(ローカル/グローバル) | クラス内部にカプセル化 |
| 再利用の単位 | サブルーチン単位 | クラス単位、継承による拡張 |
| 残った課題 | グローバル変数問題、貧弱な再利用 | (これらをOOPが解決) |
両者の最大の違いは 「データと処理の関係」 です。
構造化プログラミングでは、データ(変数)と処理(サブルーチン)がバラバラに存在し、グローバル変数を経由してつながっていました。これがプログラム規模の拡大とともに保守性を悪化させました。
OOPでは、データと処理を同じクラスにまとめることで、グローバル変数問題を局所化し、影響範囲を把握しやすくします。クラスの中の処理は、外部のグローバル変数ではなく、自分自身が持つデータを参照する。これによって「どの処理がどのデータを使っているか」が明確になります。
具体的にどう書くと違うのか、4章以降で詳しく学んでいきますが、本記事ではその一例を、次のセクションで自分のRailsコードから紹介します。
自分のRailsコードに当てはめてみる
ここからが本題です。Ruby on Rails で開発している自分のアプリ(MabaTalk — 重度障害のある方のコミュニケーション支援アプリ)から、3章の概念が現れている箇所を取り出します。
例1:サブルーチンの独立性 → サービスクラスの切り出し
3章では「サブルーチンの独立性を高めるために、共有する情報を少なくする」ことが保守性の鍵だと説明されています。
これを現代のRails開発で実践している例が、サービスクラスです。自分のコードを見てみます。
# app/controllers/analytics_controller.rb
def show
@month = parse_month(params[:month])
@stats = LogStatsService.call(user: current_user, month: @month)
# ...
end
# app/services/log_stats_service.rb
class LogStatsService
def self.call(user:, month:)
new(user:, month:).call
end
def initialize(user:, month:)
@user = user
@month = month.to_date.beginning_of_month
end
def call
logs = MessageLog.for_viewer(@user)
.where(created_at: @month..@month.end_of_month)
# ...
end
private
def build_detail_data(logs)
# ローカル変数だけで処理する
end
end
このコードには3章の概念が3つ詰まっています。
-
値渡し:
current_userや@monthを引数として明示的に渡している -
ローカル変数:
build_detail_dataの中で使う変数は、メソッドを抜ければ消える -
サブルーチンの独立性:
LogStatsServiceはコントローラの存在を知らない。paramsもflashもsessionも触らない
3章の言葉で言えば、「呼び出し側とサブルーチンで共有する情報を少なくする」設計を、自分は無意識のうちにやっていました。本書を読むまで、この設計の理論的背景を言語化できていませんでした。
例2:グローバル変数問題の現代版 → セッション
3章の「グローバル変数問題」は、現代のRailsでも形を変えて存在します。セッションがその一つです。
自分のアプリでは、複数ステップの入力フローを session[:detail_flow]で管理しています。
# app/controllers/detail_flows_controller.rb
session[:detail_flow] = { "flow_item_key" => @flow_item.key, "answers" => {} }
# app/controllers/message_completions_controller.rb
df = session[:detail_flow]
# app/controllers/message_logs_controller.rb
session.delete(:detail_flow)
セッションは3つのコントローラーから読み書きされている。これはまさに3章の「グローバル変数問題」です。
グローバル変数はプログラム全体のどこからでもアクセスできるため、変更時には影響範囲を確かめるためにロジックをすべて調べなければなりません。
もし session[:detail_flow] の構造を変更したくなったら、3つのコントローラー全部を確認する必要があります。これを書いていたとき、なんとなく感じていた「ここを変えたら他にも影響あるかも」という不安の正体は、3章で説明されている問題そのものでした。
例3:データと処理の一体化 → モデルクラス(OOPへの伏線)
3章では深く触れられませんが、4章以降の「OOPによる解決策」を先取りして見つけられる箇所もあります。
# app/models/ai_summary.rb
class AiSummary < ApplicationRecord
belongs_to :user
REGENERATE_INTERVAL = 1.day
def regeneratable?
generated_at < REGENERATE_INTERVAL.ago
end
def next_regeneratable_at
generated_at + REGENERATE_INTERVAL
end
end
このクラスには:
-
データ:
generated_at、content(DBのカラム) -
処理:
regeneratable?、next_regeneratable_at
がセットになっています。構造化プログラミング中心の設計であれば、データと処理は別々に管理されていたはずです。OOPがデータと処理を一体として扱えるようにした、というのが4章の中心テーマになっていきます。
学んだこと:歴史を知るとコードの意味が変わる
3章を読む前と後で、自分のコードを見る目が変わりました。
before:「サービスクラスはよく使われるパターンだから使う」
after:「サブルーチンの独立性を高めて、保守性を上げるために使う」
before:「セッションは便利だから使う」
after:「セッションはグローバル変数に近い性質を持つので、依存範囲を意識して使う」
before:「クラスは便利だから使う」
after:「クラスはデータと処理を一体化して、グローバル変数問題を解決するための仕組み」
「便利だから」「ベストプラクティスだから」で書いていたコードに、理由が見えるようになりました。これは本書を読んだ最大の収穫です。
著者が繰り返し強調する「よくある誤解」
3章までを通して、平澤さんは「OOPは現実世界をそのまま表現する技術」という説明を否定的に扱っています。
OOPは現実世界の模倣ではなく、ソフトウェアを整理するための技術である
「動物クラス → 犬クラス」のような入門書の比喩は理解の入口としては有効ですが、業務システムでそんな継承はほぼ使いません。OOPの本質は、構造化プログラミングが残した2つの宿題(グローバル変数問題、貧弱な再利用)を解くために生まれたソフトウェア整理術である。これが本書の一貫した立場です。
まとめ
3章で学んだことをまとめます。
- 構造化プログラミングは2つの宿題を残した
- グローバル変数問題
- 貧弱な再利用
- OOPはこの2つを解決するために生まれた
- クラス・カプセル化 → グローバル変数問題
- 継承・ポリモーフィズム → 貧弱な再利用
- 自分のコードにも、構造化プログラミングの流れが見える
- セッション = グローバル変数に近い性質を持つ共有状態
- サービスクラス = 独立性の高いサブルーチン
- モデルクラス = データと処理の一体化(OOP的)
「OOPはなぜ存在するのか」という問いの答えは、「過去のプログラミング技術の限界を超えるため」でした。
次は4章「OOPの全体像」を読んで、3章で予告された解決策の中身を学びます。
参考
- 平澤章『オブジェクト指向でなぜつくるのか』(第3版、日経BP、2021)
おわりに
学習記録として書きましたが、同じ本を読んでいる方の参考になれば幸いです。