はじめに
オブジェクト指向の概念(クラス、カプセル化、抽象化など)は、初学者にとって抽象的で理解しづらいものです。「なぜカプセル化が必要なのか」「抽象化って何が良いのか」といった疑問を持つ方も多いのではないでしょうか。
本記事では、オブジェクト指向の概念を人間社会の役割や仕事に置き換えて理解するアプローチを紹介します。
想定読者
この記事は、以下のような方を対象としています:
- オブジェクト指向や設計原則を勉強し始めているが、実際に設計/実装するときはうまく適用できない人
- クラスやメソッドの基本的な概念は理解しているが、設計の判断に迷うことがある人
この記事で学べること
本記事では、特に 「マイクロマネージメントしない設計」 という観点から、オブジェクト指向の設計原則を人間社会のメタファーで説明します。
マイクロマネージメントしない設計の2つの柱:
- 仕事は細かく指示しすぎない:ゴールを示して担当者を任せる
- 途中経過を見せない:担当者が責任をもって成果物の整合性を守る
加えて、実社会での業務と親和性のある概念:
- ルールや情報は一つにまとめる:矛盾を防ぎ効率性をあげる
- 誰かに読んでもらうドキュメントとして意識する:階層構造/MECEを明確に
説明しないこと
- 一つ一つの原理原則の説明
補足: 本記事のコードサンプルはTypeScriptで記述しています。TypeScriptは厳密にはOOP言語ではありませんが、クラスやインターフェースなどOOPの概念を説明するのに十分な機能があるため、ここではTypeScriptを使用しています。
仕事は細かく指示しすぎない:ゴールを示して担当者を任せる
人間社会での例
例えば、プロダクトの企画で市場調査をする際に、マネージャーが複数の調査担当者に依頼をするときに、以下のどちらが良いでしょうか?
前提
それぞれの担当者には強みがある
- 担当者A:対人コミュニケーションの強みを活かしたユーザーインタビューなどのフィールドリサーチを得意とする
- 担当者B:雑誌、TVなど、複数メディアからの情報収集を得意とする
- 担当者C:最新のデジタルツールに精通しており、生成AIを活用した調査を得意とする
悪い例(マイクロマネージメント)
- 担当者Aに対して:「実際にユーザーにインタビューして、質問項目をリストアップして、回答を記録して」
- 担当者Bに対して:「この5社の雑誌を比較するために、本屋に行って雑誌を買って、関連記事を整理して」
- 担当者Cに対して:「AIに質問して、回答を整理して、参考資料を確認して」
この場合以下の問題点があります。
- マネージャー目線:それぞれの担当者の調査手法を細かく全て把握している必要がある。また、新しい担当者が増えるたびにその担当者の特性を理解し、指示を覚え直す必要がある。
- 担当者目線:細かく指示をされることで裁量がなくなり、自由に動きづらくなりやる気も削がれる。
良い例(適切な委譲)
- 全員に対して:「競合他社の動向を調査して、主要なポイントをまとめて報告して」
それぞれの担当者がプロフェッショナルとして自分の責務を全うする前提で、マネージャーは「調査してまとめる」という共通のゴールだけを伝え、具体的な方法は担当者に任せます。もし新しい担当者が増えたとしても、その方針だけ伝えることでマネージャーの負担は増えません。
これにより、それぞれの担当者が自分の裁量で強みを発揮でき、より効率の良いチーム運営ができます。
設計/実装での対応
この考え方は、設計/実装においては以下の原則/方針として表現されます。
- 抽象化:インターフェースを定義し、具体的な実装に依存しない
- ポリモーフィズム:同じインターフェースに対して、異なる実装を提供できる
- オープン・クローズド原則(OCP):既存のコードを変更せずに、新しい実装を追加できる
- Fatコントローラーの回避:コントローラーが詳細実装をしすぎない
悪い例:具象クラスに直接依存
// 悪い例:具体的な実装に依存
class Manager {
requestResearch(researcherName: string): void {
// それぞれの担当者の具体的な実装方法を知っている必要がある
if (researcherName === "Alice") {
const fieldResearcher = new FieldResearcher();
fieldResearcher.interviewUsers(); // 具体的なメソッドを呼び出し
fieldResearcher.createQuestionList();
fieldResearcher.recordAnswers();
} else if (researcherName === "Bob") {
const mediaResearcher = new MediaResearcher();
mediaResearcher.buyMagazines(); // 具体的なメソッドを呼び出し
mediaResearcher.compareArticles();
} else if (researcherName === "Charlie") {
const digitalToolResearcher = new DigitalToolResearcher();
digitalToolResearcher.askAI(); // 具体的なメソッドを呼び出し
digitalToolResearcher.organizeAnswers();
}
}
}
// 便宜上、上記の例に従った命名ですので、Managerという名前が神クラスの温床になるとかそのあたりはスルーしてください
各担当者の仕事の仕方や、新しい担当者が増えるたびに、Managerクラスのコードを変更する必要があります。また、Managerクラスが多くの責務を持ち、肥大化してしまいます(Fatコントローラー)。
良い例:インターフェースに依存
// 良い例:抽象(インターフェース)に依存
/**
* 調査担当者のインターフェース:何をしてほしいかを定義
*/
interface Researcher {
research(topic: string): ResearchResult;
}
class FieldResearcher implements Researcher {
/**
* フィールド調査担当者A:ユーザーインタビューを実施
*/
research(topic: string): ResearchResult {
// 内部実装の詳細はここに隠されている
this.interviewUsers();
this.createQuestionList();
this.recordAnswers();
return new ResearchResult(/* ... */);
}
private interviewUsers(): void { /* ... */ }
private createQuestionList(): void { /* ... */ }
private recordAnswers(): void { /* ... */ }
}
class MediaResearcher implements Researcher {
/**
* メディア調査担当者B:雑誌・記事などから情報収集
*/
research(topic: string): ResearchResult {
// 内部実装の詳細はここに隠されている
this.buyMagazines();
this.compareArticles();
return new ResearchResult(/* ... */);
}
private buyMagazines(): void { /* ... */ }
private compareArticles(): void { /* ... */ }
}
class DigitalToolResearcher implements Researcher {
/**
* デジタルツール担当者C:AIなどを活用した調査
*/
research(topic: string): ResearchResult {
// 内部実装の詳細はここに隠されている
this.askAI();
this.organizeAnswers();
return new ResearchResult(/* ... */);
}
private askAI(): void { /* ... */ }
private organizeAnswers(): void { /* ... */ }
}
class Manager {
requestResearch(researcher: Researcher, topic: string): ResearchResult {
// 抽象に依存:具体的な実装を知らなくても使える
return researcher.research(topic);
}
}
// 使用例
const manager = new Manager();
const topic = "競合他社の動向";
const resultA = manager.requestResearch(new FieldResearcher(), topic);
const resultB = manager.requestResearch(new MediaResearcher(), topic);
const resultC = manager.requestResearch(new DigitalToolResearcher(), topic);
// 実際はFactoryでResearcherを選択/生成したり、Enumでタイプ分類したりなど工夫が必要です
Managerクラスは、各担当者(FieldResearcher、MediaResearcher、DigitalToolResearcherなど)の具体的な実装詳細を知らなくても、Researcherインターフェースを通じて動作します。これにより、新しい担当者クラスを追加しても、Managerのコードを修正する必要がありません。また、Managerクラス自体も役割が明確かつシンプルになり、「調査を依頼する」という単一の責務に集中できます。
途中経過を見せない:担当者が責任をもって成果物の整合性を守る
人間社会での例
先ほどの「細かく指示しすぎない」にも似ていますが、チームワークで仕事をする際には、各担当者の責任範囲に対して途中結果や細々した情報を見る/見せるべきではないです。
(担当者がジュニアレベルであればリスクヘッジや教育観点で細かく見てあげるべきですが、ここは各人信頼があるプロフェッショナルチームが前提となっています)
例えば、管理者が担当者の作業の細かい手順や途中経過を逐一確認していると、老婆心から悪気はなくてもついつい口を出してしまい、 その結果、担当者は自分のペースで自由に仕事ができなくなってしまいます。
また、担当者の責任範囲のルールや作業途中の記録に対して、他人が勝手に書き換えまでできてしまったら、担当者は自分の管理するドキュメントが書き換えられていないかを常に気にしながら仕事をする必要があるため余計な工数・ストレスがかかります。また、時にはそのことにも気づかずに間違った成果物を作成してしまうかもしれません。
仮に周りの人が修正したいことがある場合、適切なタイミング・適切な手順を踏んで担当者に伝え、担当者自身がその依頼内容を見て修正することが、一貫性持って成果物の整合性を保つ上で重要です。
設計/実装での対応
この考え方は、設計/実装においては以下の原則/方針として表現されます:
- カプセル化:内部実装を隠し、外部から直接アクセスできないようにする
- 情報隠蔽:細かい情報を見ない・見せないことで、責任範囲を明確にする
- デメテルの法則:必要以上に他のオブジェクトの内部構造に立ち入らず、自分が直接関係する相手とだけやりとりする
- Tell, Don't Askの原則:内部情報を聞き出すのではなく、やって欲しいことをただ命じる
以下は、購入する商品を候補リストから選定する業務における例です。
悪い例:内部実装の詳細が見えてしまう
// 悪い例:内部状態が外部から見える・変更できる
class ProductSelector {
// publicフィールド:外部から変更可能(問題)
candidates: Product[] = [];
// 候補条件も外部から変更可能(問題)
minPrice: number = 10000;
maxPrice: number = 100000;
requiredCertification: string = "PSE";
addProductByUrl(url: string): void {
const product = this.fetchProductData(url);
this.tryAddProduct(product);
}
addProductBySpec(spec: ProductSpec): void {
const product = this.createProductFromSpec(spec);
this.tryAddProduct(product);
}
selectBest(criteria: SelectionCriteria): Product {
if (this.candidates.length === 0) {
throw new Error("候補商品がありません");
}
return this.findBestProduct(this.candidates, criteria);
}
private tryAddProduct(product: Product): void {
if (
product.price >= this.minPrice &&
product.price <= this.maxPrice &&
product.certification === this.requiredCertification
) {
this.candidates.push(product);
}
}
private findBestProduct(products: Product[], criteria: SelectionCriteria): Product {
// 選定基準に基づいて最適な商品を選定
return products.sort((a, b) =>
criteria.priority === "price" ? a.price - b.price : b.performance - a.performance
)[0];
}
}
// 呼び出し (UIイベントなど、適宜呼び出しされる)
const specialist = new ProductSelector();
specialist.addProductByUrl("https://example.com/product1");
specialist.addProductBySpec({ name: "商品B", price: 30000, certification: "PSE" });
// 問題: ルールに合わない商品が外部から登録される
specialist.candidates.push(new Product("直接追加", 999999, "None"));
// 問題: 候補リストの中身が外部から変更される
specialist.candidates[0].price = 100;
// 問題: 候補条件を外部から勝手に変更できる!
specialist.maxPrice = 500000; // 上限を引き上げ
specialist.requiredCertification = "None"; // 認証を不要に
specialist.addProductByUrl("https://example.com/product2"); // 変更後の条件で追加される
// 結果は、ProductSelectorが保つべき整合性が破綻している
const criteria = { priority: "price" };
const selected = specialist.selectBest(criteria);
console.log(`選定結果: ${selected.name}`);
良い例:カプセル化で責任範囲を明確にする
// 良い例:設計で不正な状態を作れないようにする
class ProductSelector {
// privateフィールド:外部から変更不可
private candidates: Product[] = [];
// 候補条件もprivate・readonly(外部から変更不可)
// 実際は、コンストラクトなどで指定できると良い
private readonly minPrice: number = 10000;
private readonly maxPrice: number = 100000;
private readonly requiredCertification: string = "PSE";
addProductByUrl(url: string): void {
const product = this.fetchProductData(url);
this.tryAddProduct(product);
}
addProductBySpec(spec: ProductSpec): void {
const product = this.createProductFromSpec(spec);
this.tryAddProduct(product);
}
selectBest(criteria: SelectionCriteria): Product {
if (this.candidates.length === 0) {
throw new Error("候補商品がありません");
}
return this.findBestProduct(this.candidates, criteria);
}
private tryAddProduct(product: Product): void {
if (
product.price >= this.minPrice &&
product.price <= this.maxPrice &&
product.certification === this.requiredCertification
) {
this.candidates.push(product);
}
}
private findBestProduct(products: Product[], criteria: SelectionCriteria): Product {
// 選定基準に基づいて最適な商品を選定
return products.sort((a, b) =>
criteria.priority === "price" ? a.price - b.price : b.performance - a.performance
)[0];
}
}
// 呼び出し (UIイベントなど、適宜呼び出しされる)
const specialist = new ProductSelector();
specialist.addProductByUrl("https://example.com/product1");
specialist.addProductByUrl("https://example.com/product2");
specialist.addProductBySpec({ name: "商品B", price: 30000, certification: "PSE" });
specialist.addProductBySpec({ name: "商品C", price: 5000, certification: "PSE" }); // minPrice未満なので除外される
// 内部の情報には直接アクセスできない
// specialist.candidates.push(new Product("直接追加", 999999, "None")); //コンパイルエラー
// specialist.candidates[0].price = 100; //コンパイルエラー
// specialist.minPrice = 1000; // コンパイルエラー
// 選定基準を渡して最適商品を選定してもらう (整合性のあった正しい結果が得られる)
const criteria = { priority: "price" };
const selected = specialist.selectBest(criteria);
console.log(`選定結果: ${selected.name}`);
内部状態を外部から直接アクセスできないようにすることで、不正状態が作れなくなります。外部から情報登録したい場合は、チェック機構のある適切なI/Fからのみアクセスします。それによって、整合性を保つことが容易になり、安心して最終結果を使用することができます。
ルールや情報は一つにまとめる:矛盾を防ぎ効率性をあげる
人間社会での例
チームで業務を進める際、同じルールや手順を複数の文書に記載するのは非効率です。また、ルールが一元管理されていない場合、変更時に全ての箇所を修正する必要があり、修正漏れや不整合が発生しやすくなります。
悪い例(同じルールを複数箇所に記載):
-
営業部の手順書に「経費申請は3万円以上の場合、部長承認が必要」
-
経理部のマニュアルに「経費申請は3万円以上の場合、部長承認が必要」
-
新入社員向けガイドに「経費申請は3万円以上の場合、部長承認が必要」
-
社内Wikiに「経費申請は3万円以上の場合、部長承認が必要」
→ルールが「5万円以上」に変更されたら、4箇所すべてを修正する必要があり、修正漏れによる混乱が発生する
良い例(ルールを一元管理):
- 「経費規程」という正式なドキュメントに承認ルールを一元管理
- 各部署の手順書やガイドでは「詳細は経費規程を参照」とする
- ルールが変更されたら、経費規程の1箇所だけを更新すれば良い
設計/実装での対応
この考え方は、設計/実装においては以下の原則/方針として表現されます。
- DRYの原則:同じことを何度も繰り返さない
- SSOT(Single Source of Truth):情報を一元管理し、重複や矛盾の発生を防ぐ
誰かに読んでもらうドキュメントとして意識する:階層構造/MECEを明確に
人間社会での例
ドキュメントやプレゼン資料を作る時、目次構成は各階層でのレベル感を合わせてMECEにすることを意識します。
例えば、報告書を書くとき以下の例だと、読み手は「今どのレベルの話をしているのか」を見失い、理解が困難になります。
悪い例:各レベルでの粒度がバラバラ
第1章:はじめに
1.1 調査の背景
1.2 表1:調査対象者の年齢分布の詳細内訳 ← 前後に対して具体的
1.3 今後の提言
第2章:2024年3月の事例A ← 第1レベルなのに具体的(第1章の「はじめに」との粒度が合わない)
2.1 概要
2.1.1 背景
2.1.2 2024年3月15日14時の調査結果 ← 前後に対して具体的
2.1.3 考察
一方で以下であれば、各章が同じ抽象度レベルの内容でまとめられており、階層構造が明確です。 読み手は「概要だけ知りたい」なら第1章だけを読み、「詳細を知りたい」なら第3章を読むという選択ができます。
これは、コードにおいて各メソッドが同じ抽象度レベルの処理をまとめ、凝集度が高い状態に対応します。
良い例:各レベルでの粒度が揃っている
第1章:はじめに
1.1 調査の背景
1.2 調査の目的
1.3 調査の範囲
第2章:調査結果の分析
2.1 全体的な傾向
2.1.1 地域別の傾向
2.1.2 年代別の傾向
2.2 課題の整理
2.2.1 課題A:参加率の低下
2.2.2 課題B:地域格差
第3章:個別事例
3.1 事例A
3.2 事例B
第4章:結論と提言
4.1 まとめ
4.2 今後の方針
設計/実装での対応
この考え方は、設計/実装においては以下の原則/方針として表現されます:
- 凝集度:関連する処理を一つのメソッドやクラスにまとめ、粒度を揃える
- 単一責任の原則(SRP):各クラス・メソッドは一つの責任や目的に限定し、役割を明確にする(MECEを保つ)
悪い例:メソッド内で抽象度・粒度がバラバラ
// 悪い例:高レベルと低レベルの処理が混在
class ReportGenerator {
generateReport(): void {
const data = this.fetchData(); // 高レベル:データ取得
// 問題:低レベルの詳細実装が突然出現
const processed = data.map(item => {
const adjusted = item.value * 1.1;
const formatted = adjusted.toFixed(2);
return parseFloat(formatted);
});
this.saveReport(processed); // 高レベル:保存
}
}
メソッド内で「データ取得」「保存」という高レベルの処理と、「計算」「フォーマット」という低レベルの詳細実装が混在しています。
良い例:各メソッドで粒度が揃っている
// 良い例:各メソッドが同じ抽象度レベルで統一
class ReportGenerator {
generateReport(): void {
// 高レベルの処理だけを記述
const data = this.fetchData();
const processed = this.processData(data);
this.saveReport(processed);
}
private processData(data: Data[]): ProcessedData[] {
// 中レベルの処理だけを記述
return data.map(item => this.processItem(item));
}
private processItem(item: Data): ProcessedData {
// 低レベルの詳細実装だけを記述
const adjusted = item.value * 1.1;
const formatted = adjusted.toFixed(2);
return parseFloat(formatted);
}
}
generateReport()は高レベルの処理だけ、processData()は中レベルの処理だけ、processItem()は低レベルの詳細実装だけを担当し、各メソッドの粒度が揃っています。
まとめ
オブジェクト指向の概念を人間社会のメタファーで理解することで、抽象的な概念がより直感的に理解できるようになります。
実務で設計に迷ったときは、「実社会での仕事だったらどうするか」を考えてみてください。
筆者も設計/実装する際には、自身がクラスの気持ちになり変わって、「ちょっと自分で仕事持ち過ぎててマネージャーとしてはしんどいな。楽になるために部下に仕事委譲しようかな」とか「この情報は人に見せちゃうと仕事がしづらいな」など考えながらやっています。
(結果的に、あとから「これってこういう原則の名前が付いていたのか」と思うことも多々ありました)
プログラミングという技術的に特別な作業のように思えますが、上記のように実社会での仕事の仕方と地続きだなと感じます。
- 設計においてマイクロマネージメントしない。
- コードを「ただの動かすだけのスクリプト」ではなく、「誰かが読む/運用するドキュメント」としてとらえる。
など、そういった意識によって、自然とより良い設計/実装ができるのではないかと思います。