Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Webアプリケーションにおけるクラスの設計再入門

 Webアプリケーションにおけるクラスの設計再入門

Webアプリケーションにおけるクラス設計、自信ありますか?より良いオブジェクト指向を目指す設計再入門

「クラス設計、なんとなくできているけど、もっと深く理解したい…」

そう感じているWebアプリケーションエンジニアの皆さん、必見です!

本勉強会では、オブジェクト指向プログラミング(OOP)の基礎から、SOLID原則、デザインパターン、そしてMVCアーキテクチャにおけるクラス設計の実践まで、体系的に解説します。  

この勉強会で得られること

クラス設計の重要性を再認識し、より洗練された設計スキルを習得  
保守性、拡張性の高いコードを書くための原則と具体的な手法  
実際の開発現場で直面するクラス設計の課題に対する解決のヒント  
参加者同士での活発なディスカッションを通じた学びと交流  
対象者

Webアプリケーションの開発に携わっているエンジニア
オブジェクト指向プログラミングの理解を深めたい方
より良いクラス設計を学び、日々の開発に活かしたい方
内容

OOPの基本とクラスの関係  
良いクラス設計のための原則 (SOLID原則、デザインパターン)  
クラス設計の動機と具体的な例  
MVCアーキテクチャにおけるクラス設計  
現場で役立つクラス設計のベストプラクティス
参加者全員でのディスカッション  
あなたのクラス設計スキルを一段階引き上げ、より持続可能で高品質なWebアプリケーション開発を実現しませんか?

皆様のご参加をお待ちしております!

More Decks by NearMeの技術発表資料です

Other Decks in Programming

Transcript

  1. 3 クラスを理解するための OOP についてのまとめ • OOP (Object-Oriented Programming) ◦ オブジェクト指向プログラミングのこと

    ⭐ 主な要素 • オブジェクト (Object): ◦ データ(属性、状態)と、それに対する操作(振る舞い、メソッド)をまとめたもの。 ◦ 例:犬オブジェクト → 属性(名前、種類、年齢)、振る舞い(吠える、歩く、食べる) • クラス (Class): ◦ オブジェクトの設計図。どのような属性を持ち、どのような振る舞いができるかを定義したもの。 ◦ 例:犬クラス → 全ての犬オブジェクトに共通する属性や振る舞いを定義 • カプセル化 (Encapsulation): ◦ データとそれに関連する操作を一つにまとめ、外部からの不適切なアクセスを防ぐ仕組み。 ◦ 情報隠蔽の役割も持つ。 • 継承 (Inheritance): ◦ 既存のクラスの属性や振る舞いを引き継ぎ、新しいクラスを作成する仕組み。 ◦ コードの再利用性と拡張性を高める。 ◦ 例:動物クラス → 犬クラス、猫クラス(動物の基本的な性質を受け継ぎつつ、固有の性質も持つ) • ポリモーフィズム (Polymorphism): ◦ 同じ名前の操作(メソッド)が、オブジェクトの種類によって異なる振る舞いを見せる性質。 ◦ 柔軟で拡張性の高いプログラム設計を可能にする。 ◦ 例:「鳴く」という操作 → 犬は「ワン!」、猫は「ニャー!」
  2. 5 クラスとは? • オブジェクトの設計図 ◦ どのような属性を持ち、どのような振る舞いができるかを定義したもの Ex) Person クラス -

    Person には属性として name,age というものがある - ただし、まだ具体的には決まっていない class Person { constructor(name, age) { this.name = name; this.age = age; } greet() { console.log(`こんにちは、私は ${this.name}です。現在は ${this.age}歳です。 `); } }
  3. 6 クラスとは? • 設計書のようなもの ◦ 実体ではなく、概念としてまず何の属性を持つのかを記述 ◦ 設計書の属性に具体的な値をセットして実体(オブジェクト)を作成し属性の具体 を保持 →

    インスタンス化 Ex) taro オブジェクト - taro は Person の実体で、name, age に具体的に値をセットする class Person { constructor(name, age) { this.name = name this.age = age } greet() { console.log(`こんにちは、私は${this.name}です。現在は${this.age}歳です。`) } } const taro = new Person(‘太郎’, ‘30’)
  4. 7 クラスとは? • 設計書のようなもの ◦ 実体ではなく、概念としてまず何の属性を持つのかを記述 ◦ 設計書の属性に具体的な値をセットして実体(オブジェクト)を作成属性の具体を 保持 →

    インスタンス化 ◦ メソッドを⽤いて具体的な処理を含むことができる Ex) taro オブジェクト - taro は Person の実体で、name, age に具体的に値をセットする class Person { ... greet() { console.log(`こんにちは、私は${this.name}です。現在は${this.age}歳です。`) } } const taro = new Person(‘太郎’, ‘30’) taro.greet() // Person クラスの greet メソッド
  5. 10 良いクラス設計のための原則 1. SOLID原則 ◦ クラス設計に関する以下の5つの原則をまとめたもの ▪ 単⼀責任の原則(Single Responsiblity Principle

    - SRP) ▪ オープン/クローズドの原則 (Open/Closed Principle - OCP) ▪ リスコフの置換原則 (Liskov Substitution Principle - LSP) ▪ インターフェース分離の原則 (Interface Segregation Principle - ISP) ▪ 依存性逆転の原則 (Dependency Inversion Principle - DIP)
  6. 11 単⼀責任の原則(Single Responsiblity Principle - SRP) class UserMailer { constructor(name,

    email) { this.name = name; this.email = email; } saveToDatabase() { console.log(`${this.name}の情報を保存しました。 `); } sendWelcomeEmail() { console.log(`${this.email}にウェルカムメールを送信しました。 `); } } const user = new UserMailer("John Doe", "[email protected]"); user.saveToDatabase(); user.sendWelcomeEmail();
  7. 12 単⼀責任の原則(Single Responsiblity Principle - SRP) class UserMailer { constructor(name,

    email) { this.name = name; this.email = email; } saveToDatabase() { console.log(`${this.name}の情報を保存しました。 `); } sendWelcomeEmail() { console.log(`${this.email}にウェルカムメールを送信しました。 `); } } const user = new UserMailer("John Doe", "[email protected]"); user.saveToDatabase(); user.sendWelcomeEmail(); 俺はメーラーだよな ...
  8. 13 単⼀責任の原則(Single Responsiblity Principle - SRP) class UserMailer { constructor(name,

    email) { this.name = name; this.email = email; } saveToDatabase() { console.log(`${this.name}の情報を保存しました。 `); } sendWelcomeEmail() { console.log(`${this.email}にウェルカムメールを送信しました。 `); } } const user = new UserMailer("John Doe", "[email protected]"); user.saveToDatabase(); user.sendWelcomeEmail(); え、俺データベースに値の保存もし なくちゃいけないのかよ 俺はメーラーだよな ...
  9. 14 単⼀責任の原則(Single Responsiblity Principle - SRP) class UserMailer { constructor(name,

    email) { this.name = name; this.email = email; } saveToDatabase() { console.log(`${this.name}の情報を保存しました。 `); } sendWelcomeEmail() { console.log(`${this.email}にウェルカムメールを送信しました。 `); } } const user = new UserMailer("John Doe", "[email protected]"); user.saveToDatabase(); user.sendWelcomeEmail(); これはメーラーよりも DBに 関するクラスの責任 俺はメーラーだよな ... え、俺データベースに値の保存もし なくちゃいけないのかよ
  10. 15 単⼀責任の原則(Single Responsiblity Principle - SRP) class UserMailer { constructor(name,

    email) { this.name = name; this.email = email; } sendWelcomeEmail() { console.log(`${this.email}にウェルカムメールを送信しました。 `); } } const user = new UserMailer("John Doe", "[email protected]"); user.saveToDatabase(); user.sendWelcomeEmail(); 俺はメーラーなんだから、 メールのことだけにしてよね! 単一責任の原則 (SRP: Single Responsibility Principle)
  11. 16 オープン/クローズドの原則 (Open/Closed Principle - OCP) class ShapeDrawer { draw(shapeType)

    { if (shapeType === 'circle') { console.log('円を描画します。'); } else if (shapeType === 'rectangle') { console.log('四角形を描画します。 '); } } } const drawer = new ShapeDrawer(); drawer.draw('circle'); drawer.draw('rectangle');
  12. 17 オープン/クローズドの原則 (Open/Closed Principle - OCP) class ShapeDrawer { draw(shapeType)

    { if (shapeType === 'circle') { console.log('円を描画します。'); } else if (shapeType === 'rectangle') { console.log('四角形を描画します。 '); } else if (shapeType === 'pentagon') { console.log('五角形を描画します。 '); } } } const drawer = new ShapeDrawer(); drawer.draw('circle'); drawer.draw('rectangle');
  13. 18 オープン/クローズドの原則 (Open/Closed Principle - OCP) class ShapeDrawer { draw(shapeType)

    { if (shapeType === 'circle') { console.log('円を描画します。'); } else if (shapeType === 'rectangle') { console.log('四角形を描画します。 '); } else if (shapeType === 'pentagon') { console.log('五角形を描画します。 '); } else if (shapeType === 'hexagon') { console.log('六角形を描画します。 '); } ... } } const drawer = new ShapeDrawer(); drawer.draw('circle'); drawer.draw('rectangle');
  14. 19 オープン/クローズドの原則 (Open/Closed Principle - OCP) class ShapeDrawer { draw(shapeType)

    { if (shapeType === 'circle') { console.log('円を描画します。'); } else if (shapeType === 'rectangle') { console.log('四角形を描画します。 '); } else if (shapeType === 'pentagon') { console.log('五角形を描画します。 '); } else if (shapeType === 'hexagon') { console.log('六角形を描画します。 '); } ... // どこまで書くねん } } const drawer = new ShapeDrawer(); drawer.draw('circle'); drawer.draw('rectangle');
  15. 20 オープン/クローズドの原則 (Open/Closed Principle - OCP) class ShapeDrawer { draw(shapeType)

    { if (shapeType === 'circle') { console.log('円を描画します。'); } else if (shapeType === 'rectangle') { console.log('四角形を描画します。 '); } else if (shapeType === 'pentagon') { console.log('五角形を描画します。 '); } else if (shapeType === 'hexagon') { console.log('六角形を描画します。 '); } ... // どこまで書くねん } } const drawer = new ShapeDrawer(); drawer.draw('circle'); drawer.draw('rectangle'); 拡張性が悪い → そもそもメソッドの中身が  shapeTypeに合わせて重厚  に なるのは微妙
  16. 21 • クラスのメソッドの中⾝を増やすのではない ◦ クラスごと拡張して、拡張クラスに対してそれぞれ具体的なメソッドを実装 → ポリモーフィズムの実現💡 オープン/クローズドの原則 (Open/Closed Principle

    - OCP) abstract class ShapeDrawer { constructor(shapeName: string) { this.shapeName = shapeName } get shapeName() { return this.shapeName } } class TriangleDrawer extends ShapeDrawer { constructor(shapeName: string) { super(shapeName) } public draw() { console.log(this.shapeName) } }
  17. 22 Ex) 正⽅形は⻑⽅形ではない? • 数学の命題としては正しいが、クラス設計では挙動がおかしくなる場合がある • 以下のように親クラスとして Rectangle を定義し、その拡張クラスで Square

    を作成 リスコフの置換原則 (Liskov Substitution Principle - LSP) class Rectangle { constructor(protected width: number, protected height: number) {} setWidth(width: number): void { this.width = width; } setHeight(height: number): void { this.height = height; } getWidth(): number { return this.width; } getHeight(): number { return this.height; } getArea(): number { return this.width * this.height; } } class Square extends Rectangle { constructor(side: number) { super(side, side); } setWidth(width: number): void { super.setWidth(width); super.setHeight(width); // 正方形なので高さも幅に合わせる } setHeight(height: number): void { super.setHeight(height); super.setWidth(height); // 正方形なので幅も高さに合わせる } }
  18. 23 Ex) 正⽅形は⻑⽅形ではない? • 数学の命題としては正しいが、クラス設計では挙動がおかしくなる場合がある • ⻑⽅形を処理する関数を書き、⾯積を計算して出⼒ ◦ ⾯積は 5×10=50

    であるので正しい! リスコフの置換原則 (Liskov Substitution Principle - LSP) function processRectangle(rect: Rectangle): void { rect.setWidth(5); rect.setHeight(10); console.log(`長方形の面積: ${rect.getArea()}`); // 期待: 50 } // 長方形として処理する const rectangle = new Rectangle(2, 3); processRectangle(rectangle); // 出力: 長方形の面積 : 50 (期待通り)
  19. 24 Ex) 正⽅形は⻑⽅形ではない? • 数学の命題としては正しいが、クラス設計では挙動がおかしくなる場合がある • ⻑⽅形を処理する関数を書き、⾯積を計算して出⼒ ◦ ⾯積は 5×10=50

    であるので正しい! • 正⽅形を処理する関数を書き、⾯積を計算して出⼒ ◦ なぜか⾯積が 100 として出てしまう ... リスコフの置換原則 (Liskov Substitution Principle - LSP) // 正方形を長方形として処理する const square = new Square(4); processRectangle(square); // 出力: 長方形の面積 : 100 (期待と異な る!)
  20. 25 • setWidth, setHeight 部分にて、 正⽅形の定義により状態が上書き されてしまう → 思わぬ挙動に... リスコフの置換原則

    (Liskov Substitution Principle - LSP) class Square extends Rectangle { constructor(side: number) { super(side, side); } // LSP 違反の可能性! setWidth(width: number): void { super.setWidth(width); super.setHeight(width); // 正方形なので高さも幅に合わせる } // LSP 違反の可能性! setHeight(height: number): void { super.setHeight(height); super.setWidth(height); // 正方形なので幅も高さに合わせる } }
  21. 26 • setWidth, setHeight 部分にて、 正⽅形の定義により状態が上書き されてしまう → 思わぬ挙動に... •

    良い設計は、 親クラスで利⽤できるものは 問題なく⼦クラスでも利⽤できる ことである!! リスコフの置換原則 (Liskov Substitution Principle - LSP) class Square extends Rectangle { constructor(side: number) { super(side, side); } // LSP 違反の可能性! setWidth(width: number): void { super.setWidth(width); super.setHeight(width); // 正方形なので高さも幅に合わせる } // LSP 違反の可能性! setHeight(height: number): void { super.setHeight(height); super.setWidth(height); // 正方形なので幅も高さに合わせる } } リスコフの置換原則 (Liskov Substitution Principle - LSP)
  22. 27 ⭐ 解決法 → ⻑⽅形と正⽅形は、   それぞれ独⽴した概念として扱う!! リスコフの置換原則 (Liskov Substitution

    Principle - LSP) class Square implements Shape { constructor(private side: number) {} setSide(side: number): void { this.side = side; } getSide(): number { return this.side; } getArea(): number { return this.side * this.side; } } interface Shape { getArea(): number; } class Rectangle implements Shape { constructor(private width: number, private height: number) {} setWidth(width: number): void { this.width = width; } setHeight(height: number): void { this.height = height; } getWidth(): number { return this.width; } getHeight(): number { return this.height; } getArea(): number { return this.width * this.height; } } 別の状態として持つからダイジョーブ
  23. 28 Ex) プリンターと複合機に関するクラスの設計 インターフェース分離の原則 (Interface Segregation Principle - ISP) interface

    IMachine { print(document: string): void; scan(document: string): string; fax(document: string, phoneNumber: string): void; } // 単機能プリンター class SimplePrinter implements IMachine { print(document: string): void { console.log(`印刷: ${document}`); } scan(document: string): string { throw new Error("スキャン機能はサポートされていません。"); } fax(document: string, phoneNumber: string): void { throw new Error("FAX機能はサポートされていません。"); } } // 複合機 class MultiFunctionPrinter implements IMachine { print(document: string): void { console.log(`印刷: ${document}`); } scan(document: string): string { return `スキャン: ${document} (データ)`; } fax(document: string, phoneNumber: string): void { console.log(`FAX送信: ${document} to ${phoneNumber}`); } }
  24. 29 Ex) プリンターと複合機に関するクラスの設計 インターフェース分離の原則 (Interface Segregation Principle - ISP) interface

    IMachine { print(document: string): void; scan(document: string): string; fax(document: string, phoneNumber: string): void; } // 単機能プリンター class SimplePrinter implements IMachine { print(document: string): void { console.log(`印刷: ${document}`); } scan(document: string): string { // 単機能なのでスキャン機能は持たない throw new Error("スキャン機能はサポートされていません。 "); } fax(document: string, phoneNumber: string): void { // 単機能なので FAX機能は持たない throw new Error("FAX機能はサポートされていません。 "); } } // 複合機 class MultiFunctionPrinter implements IMachine { print(document: string): void { console.log(`印刷: ${document}`); } scan(document: string): string { return `スキャン: ${document} (データ)`; } fax(document: string, phoneNumber: string): void { console.log(`FAX送信: ${document} to ${phoneNumber}`); } } → そもそもscanやfax機能を持たないのに、 SimplePrinterがそれらを持っている ...
  25. 30 Ex) プリンターと複合機に関するクラスの設計 ⭐ 解決法 → インターフェースを細かく分離させる! インターフェース分離の原則 (Interface Segregation

    Principle - ISP) interface IPrinter { print(document: string): void; } interface IScanner { scan(document: string): string; } interface IFax { fax(document: string, phoneNumber: string): void; } // 単機能プリンター class SimplePrinter implements IPrinter { print(document: string): void { console.log(`印刷: ${document}`); } } // スキャナー class Scanner implements IScanner { scan(document: string): string { return `スキャン: ${document} (データ)`; } } // FAX class FaxMachine implements IFax { fax(document: string, phoneNumber: string): void { console.log(`FAX送信: ${document} to ${phoneNumber}`); } } // 複合機 (複数のインターフェースを実装) class MultiFunctionPrinter implements IPrinter, IScanner, IFax { ...
  26. 31 Ex) プリンターと複合機に関するクラスの設計 ⭐ 解決法 → インターフェースを細かく分離させる! インターフェース分離の原則 (Interface Segregation

    Principle - ISP) interface IPrinter { print(document: string): void; } interface IScanner { scan(document: string): string; } interface IFax { fax(document: string, phoneNumber: string): void; } // 単機能プリンター class SimplePrinter implements IPrinter { print(document: string): void { console.log(`印刷: ${document}`); } } // スキャナー class Scanner implements IScanner { scan(document: string): string { return `スキャン: ${document} (データ)`; } } // FAX class FaxMachine implements IFax { fax(document: string, phoneNumber: string): void { console.log(`FAX送信: ${document} to ${phoneNumber}`); } } // 複合機 (複数のインターフェースを実装) class MultiFunctionPrinter implements IPrinter, IScanner, IFax { ... インターフェース分離の原則 (Interface Segregation Principle - ISP)
  27. 32 Ex) 低レベルモジュールと⾼レベルモジュールのクラスの依存 依存性逆転の原則 (Dependency Inversion Principle - DIP) //

    低レベルモジュール: 具体的なデータベース処理 class MySQLDatabase { save(data: string): void { console.log(`MySQLに保存: ${data}`); } } // 高レベルモジュール: ユーザー管理ロジック class UserManager { private database: MySQLDatabase; constructor() { this.database = new MySQLDatabase(); // 具体的な実装に依存 } createUser(name: string): void { // ... ユーザー作成のビジネスロジック ... this.database.save(`User: ${name}`); } } const userManager = new UserManager(); userManager.createUser("Alice"); // 出力: MySQLに保存: User: Alice
  28. 33 Ex) 低レベルモジュールと⾼レベルモジュールのクラスの依存 • ⾼レベルが低レベルに依存している 依存性逆転の原則 (Dependency Inversion Principle -

    DIP) // 低レベルモジュール: 具体的なデータベース処理 class MySQLDatabase { save(data: string): void { console.log(`MySQLに保存: ${data}`); } } // 高レベルモジュール: ユーザー管理ロジック class UserManager { private database: MySQLDatabase; constructor() { this.database = new MySQLDatabase(); // 具体的な実装に依存 } createUser(name: string): void { // ... ユーザー作成のビジネスロジック ... this.database.save(`User: ${name}`); } } const userManager = new UserManager(); userManager.createUser("Alice"); // 出力: MySQLに保存: User: Alice
  29. 34 Ex) 低レベルモジュールと⾼レベルモジュールのクラスの依存 • ⾼レベルが低レベルに依存している → DBの種類を変えると、⾼レベル部分   も変更しなくてはいけない 依存性逆転の原則

    (Dependency Inversion Principle - DIP) // 低レベルモジュール: 具体的なデータベース処理 class MySQLDatabase { save(data: string): void { console.log(`MySQLに保存: ${data}`); } } // 高レベルモジュール: ユーザー管理ロジック class UserManager { private database: MySQLDatabase; constructor() { this.database = new MySQLDatabase(); // 具体的な実装に依存 } createUser(name: string): void { // ... ユーザー作成のビジネスロジック ... this.database.save(`User: ${name}`); } } const userManager = new UserManager(); userManager.createUser("Alice"); // 出力: MySQLに保存: User: Alice // もしデータベースの種類を変更したい場合 ... // UserManager クラスのコードを修正する必要がある // (例: PostgreSQLDatabase クラスを作成し、 UserManager を修正)
  30. 35 Ex) 低レベルモジュールと⾼レベルモジュールのクラスの依存 • ⾼レベルが低レベルに依存している → DBの種類を変えると、⾼レベル部分   も変更しなくてはいけない •

    具体が具体に依存している → 拡張性のためには、   抽象に依存するべき!! 依存性逆転の原則 (Dependency Inversion Principle - DIP) // 低レベルモジュール: 具体的なデータベース処理 class MySQLDatabase { save(data: string): void { console.log(`MySQLに保存: ${data}`); } } // 高レベルモジュール: ユーザー管理ロジック class UserManager { private database: MySQLDatabase; constructor() { this.database = new MySQLDatabase(); // 具体的な実装に依存 } createUser(name: string): void { // ... ユーザー作成のビジネスロジック ... this.database.save(`User: ${name}`); } } const userManager = new UserManager(); userManager.createUser("Alice"); // 出力: MySQLに保存: User: Alice // もしデータベースの種類を変更したい場合 ... // UserManager クラスのコードを修正する必要がある // (例: PostgreSQLDatabase クラスを作成し、 UserManager を修正)
  31. 36 Ex) 低レベルモジュールと⾼レベルモジュールのクラスの依存 ⭐解決法 → ⾼レベル低レベルどちらも   抽象に依存させる!! 依存性逆転の原則 (Dependency

    Inversion Principle - DIP) // 抽象: データベース操作のインターフェース interface IDatabase { save(data: string): void; } // 低レベルモジュール: 具体的な MySQL データベース処理 (抽象に依存) class MySQLDatabase implements IDatabase { save(data: string): void { console.log(`MySQLに保存: ${data}`); } } // 低レベルモジュール: 具体的な PostgreSQL データベース 処理 (抽象に依存) class PostgreSQLDatabase implements IDatabase { save(data: string): void { console.log(`PostgreSQLに保存: ${data}`); } } // 高レベルモジュール: ユーザー管理ロジック (抽象に依存) class UserManager { private database: IDatabase; // 依存性の注入 (Dependency Injection) constructor(database: IDatabase) { this.database = database; } createUser(name: string): void { // ... ユーザー作成のビジネスロジック ... this.database.save(`User: ${name}`); } } // 依存性の注入 const mysqlDatabase = new MySQLDatabase(); const postgresqlDatabase = new PostgreSQLDatabase(); const userManagerWithMySQL = new UserManager(mysqlDatabase); const userManagerWithPostgreSQL = new UserManager(postgresqlDatabase);
  32. 37 Ex) 低レベルモジュールと⾼レベルモジュールのクラスの依存 ⭐解決法 → ⾼レベル低レベルどちらも   抽象に依存させる!! 依存性逆転の原則 (Dependency

    Inversion Principle - DIP) // 抽象: データベース操作のインターフェース interface IDatabase { save(data: string): void; } // 低レベルモジュール: 具体的な MySQL データベース処理 (抽象に依存) class MySQLDatabase implements IDatabase { save(data: string): void { console.log(`MySQLに保存: ${data}`); } } // 低レベルモジュール: 具体的な PostgreSQL データベース 処理 (抽象に依存) class PostgreSQLDatabase implements IDatabase { save(data: string): void { console.log(`PostgreSQLに保存: ${data}`); } } // 高レベルモジュール: ユーザー管理ロジック (抽象に依存) class UserManager { private database: IDatabase; // 依存性の注入 (Dependency Injection) constructor(database: IDatabase) { this.database = database; } createUser(name: string): void { // ... ユーザー作成のビジネスロジック ... this.database.save(`User: ${name}`); } } // 依存性の注入 const mysqlDatabase = new MySQLDatabase(); const postgresqlDatabase = new PostgreSQLDatabase(); const userManagerWithMySQL = new UserManager(mysqlDatabase); const userManagerWithPostgreSQL = new UserManager(postgresqlDatabase); 依存性逆転の原則(Dependency Inversion Principle - DIP)
  33. 38 良いクラス設計のための原則 2. デザインパターン • ソフトウェア設計において繰り返し発⽣する特定の問題に対する、再利⽤可能な、 実績のある解決策 ▪ ⽣成パターン (Creational

    Patterns) ▪ 構造パターン (Structural Patterns) ▪ 振る舞いパターン (Behavioral Patterns) → ここら辺は量が多いので、今回は省略
  34. 40 1. ドメイン概念のモデリング(エンティティと値オブジェクト) Ex) User に関する関数群 クラスを作成しようとする動機 1/N const getUserName

    = (userId: number): string => { // ... ユーザー名を取得する処理 ... return `User ${userId}`; }; const updateUserEmail = (userId: number, newEmail: string): void => { // ... ユーザーのメールアドレスを更新する処理 ... console.log(`User ${userId}'s email updated to ${newEmail}`); }; const deleteUser = (userId: number): boolean => { // ... ユーザーを削除する処理 ... console.log(`User ${userId} deleted`); return true; }; // あちこちでこれらの関数が呼び出される console.log(getUserName(123)); updateUserEmail(123, '[email protected]'); deleteUser(123);
  35. 41 1. ドメイン概念のモデリング(エンティティと値オブジェクト) Ex) User に関する関数群 • ビジネスロジックとデータ構造が 関連付けられて理解しやすくなる! •

    ドメインロジックがドメインオブジ ェクトにカプセル化される → 凝集度が⾼まる!! クラスを作成しようとする動機 1/4 class UserAdmin { constructor(private userId: number) {} get userName(): string { return `User ${this.userId}`; } updateUserEmail(newEmail: string): void { console.log(`User ${this.userId}'s email updated to ${newEmail}`); } deleteUser(): boolean { console.log(`User ${this.userId} deleted`); return true; } } // UserAdminクラスを通して、ユーザー関連の操作を行う const admin = new UserAdmin(456); console.log(admin.userName) admin.updateUserEmail('[email protected]'); admin.deleteUser();
  36. 42 2. 状態(ステート)の管理 Ex) ShoppingCart クラス • カートに⼊っている商品の状態を 管理できる •

    1つのクラスにて状態を管理しながら 様々な関連処理をまとめられるクラス の良いところ クラスを作成しようとする動機 2/4 class ShoppingCart { private items: { name: string; price: number }[] = []; // カート内の商 品(状態) // 商品を追加するメソッド(状態を変更) addItem(name: string, price: number): void { if (price < 0) { console.error("価格が不正です。"); return; } this.items.push({ name, price }); console.log(`${name} をカートに追加しました。`); } // 合計金額を計算するメソッド(状態を利用) calculateTotal(): number { return this.items.reduce((total, item) => total + item.price, 0); } // カートの中身を表示するメソッド(状態を利用) displayItems(): void { ... // 長くなりそうなので省略 } } // 利用例 const cart = new ShoppingCart(); cart.addItem("リンゴ", 150); cart.addItem("バナナ", 100); // cart.items.push({ name: "不正な商品", price: -50 }); // privateなので 直接アクセスできない cart.displayItems();
  37. 43 3. 振る舞い、ロジックのカプセル化 Ex) User クラス • メールアドレスのセットやその正規 表現を⽤いた検証ロジックがカプセル化 •

    関連するロジックをひとまとめに できる → ⾒通しがよく、再利⽤もしやすい! クラスを作成しようとする動機 3/4 class User { private _email: string = ""; // Eメールアドレス(データ) constructor(public readonly id: number, public name: string) {} // バリデーションロジックをカプセル化 set email(newEmail: string) { if (this.isValidEmail(newEmail)) { this._email = newEmail; console.log(`メールアドレスを設定しました: ${newEmail}`); } else { console.error(`不正なメールアドレス形式です: ${newEmail}`); } } get email(): string { return this._email; } // メールアドレスの形式を検証するプライベートメソッド(内部的な振る舞い) private isValidEmail(email: string): boolean { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return emailRegex.test(email); } greet(): void { console.log(`こんにちは、${this.name}さん!`); } } ...
  38. 44 4. コードの再利⽤と構成 Ex) Animal • Animal という抽象クラスを作成して おき、それを拡張して Dog

    や Cat など を作成する → コードの重複を防ぐことが期待でき、   拡張をして使いやすい クラスを作成しようとする動機 4/4 // 親クラス: 動物 class Animal { constructor(public name: string) {} move(distance: number = 0): void { console.log(`${this.name} moved ${distance}m.`); } speak(): void { console.log(`${this.name} makes a noise.`); } } // 子クラス: 犬 (動物クラスを継承) class Dog extends Animal { constructor(name: string) { super(name); // 親クラスのコンストラクタを呼び出す } // 親クラスのメソッドをオーバーライド(上書き) speak(): void { console.log(`${this.name} barks. ワン!`); } // 子クラス独自のメソッド fetch(): void { console.log(`${this.name} fetches the ball!`); } }
  39. 46 • MVCとは? ◦ Model ▪ アプリケーションのデータとビジネスロジックを担当 ◦ View (MVC)

    ▪ ユーザーインターフェース(表⽰)を担当 ◦ Controller ▪ ユーザーからの⼊⼒を受け取り、ModelとViewを制御する MVCアーキテクチャとクラス設計
  40. 47 • MVCとは? ◦ Model ▪ アプリケーションのデータとビジネスロジックを担当 ◦ View (MVC)

    ▪ ユーザーインターフェース(表⽰)を担当 ◦ Controller ▪ ユーザーからの⼊⼒を受け取り、ModelとViewを制御する MVCアーキテクチャとクラス設計
  41. 48 • Model層でのクラス設計 ◦ エンティティ (Entity) ▪ ドメインの中⼼的な概念を表すクラス ▪ ⼀意なIDを持ち、状態が変わる(例:

    User, Product, Order) ◦ 値オブジェクト (Value Object) ▪ 値そのものを表すクラス ▪ 不変(Immutable)であることが多い(例: Address, Money, EmailAddress) • EmailAddress クラスにバリデーションロジックを持たせるなど、振る舞い もカプセル化できる MVCアーキテクチャとクラス設計
  42. 49 • Model層(Value Object) MVCアーキテクチャとクラス設計 // --- 値オブジェクト (Value Object)

    --- // Eメールアドレスを表す不変のクラス class EmailAddress { private readonly value: string; constructor(email: string) { if (!this.isValid(email)) { throw new Error(`不正なメールアドレス形式です: ${email}`); } this.value = email; } getValue(): string { return this.value; } equals(other: EmailAddress): boolean { return this.value === other.getValue(); } // バリデーションロジック (振る舞いをカプセル化) private isValid(email: string): boolean { // 簡単な形式チェック (実際にはより厳密な正規表現などを使用) const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return emailRegex.test(email); } }
  43. 50 • Model層(Entity) MVCアーキテクチャとクラス設計 // --- エンティティ (Entity) --- //

    ユーザーを表すクラス (一意なIDを持ち、状態が変わる可能性がある) class User { private _name: string; private _email: EmailAddress; // 値オブジェクトを利用 constructor( public readonly id: number, // 読み取り専用のID name: string, email: EmailAddress ) { if (!name) { throw new Error("ユーザー名は必須です。"); } this._name = name; this._email = email; } // 諸々getterなど(ここでは省略) // 状態を変更するメソッド (例: 名前変更) changeName(newName: string): void { if (!newName) { throw new Error("新しいユーザー名は必須です。"); } this._name = newName; console.log(`ユーザーID ${this.id} の名前が ${newName} に変更されました。`); } // 状態を変更するメソッド (例: メールアドレス変更) changeEmail(newEmail: EmailAddress): void { this._email = newEmail; console.log(`ユーザーID ${this.id} のメールアドレスが ${newEmail.getValue()} に変更されました。`); } }
  44. 51 • Model層でのクラス設計 ◦ リポジトリ (Repository) ▪ データの永続化(DBアクセスなど)を抽象化するクラスエンティティの取得や 保存を⾏う(例: UserRepository,

    ProductRepository) • DIP(依存性逆転の原則)を適⽤し、インターフェース (IUserRepository) に依存させることで、DB実装の変更に強くなる ◦ サービス (Service) ▪ 特定のビジネスユースケースを実現するロジックを担当 ▪ 複数のエンティティやリポジトリを跨る処理をまとめる。(例: OrderService, PaymentService) • SRP(単⼀責任の原則)に基づき、関⼼事を分離する。 MVCアーキテクチャとクラス設計
  45. 52 • Model層(Repository) MVCアーキテクチャとクラス設計 // --- リポジトリ (Repository) インターフェース ---

    // データ永続化の抽象化 (DIP: 依存性逆転の原則) interface IUserRepository { findById(id: number): Promise<User | null>; findByEmail(email: EmailAddress): Promise<User | null>; save(user: User): Promise<void>; delete(id: number): Promise<void>; } // --- リポジトリ (Repository) 実装例 (インメモリ) --- // 実際のアプリケーションではDBアクセスなどを行う class InMemoryUserRepository implements IUserRepository { private users: Map<number, User> = new Map(); private nextId: number = 1; async findById(id: number): Promise<User | null> { return this.users.get(id) || null; } async findByEmail(email: EmailAddress): Promise<User | null> { for (const user of this.users.values()) { if (user.email.equals(email)) { return user; } } return null; } async save(user: User): Promise<void> { ... // 長くなるので省略 } async delete(id: number): Promise<void> { ... // 長くなるので省略 } }
  46. 53 • Model層(Service) MVCアーキテクチャとクラス設計 // --- サービス (Service) --- //

    ビジネスロジックを担当 (SRP: 単一責任の原則) // リポジトリインターフェースに依存 (DIP) class UserService { // 依存性の注入 (Constructor Injection) constructor(private userRepository: IUserRepository) {} async createUser(name: string, emailValue: string): Promise<User> { ... } async findUserById(id: number): Promise<User | null> { return this.userRepository.findById(id); } async changeUserName(userId: number, newName: string): Promise<void> { const user = await this.userRepository.findById(userId); if (!user) { throw new Error(`ユーザー (ID: ${userId}) が見つかりませ ん。`); } user.changeName(newName); await this.userRepository.save(user); // 変更を保存 } }
  47. 54 • Controller層でのクラス設計 ◦ ユーザーからのHTTPリクエストを受け取り、適切なServiceやRepositoryを呼び出 して処理を実⾏する ◦ 処理結果に応じて、表⽰するView/Templateを選択し、必要なデータを渡す ◦ 役割

    ▪ リクエストのルーティング、⼊⼒値のバリデーション(Formクラスなどを使う 場合も)、Service層への処理委譲、レスポンス⽣成 ◦ クラス例 ▪ UserController, ProductController, OrderForm ◦ 注意点 ▪ Controller層がビジネスロジックを持ちすぎないように注意する(Fat Controllerを避ける、SRPを意識して、ServiceやModel層に委譲) MVCアーキテクチャとクラス設計
  48. 55 • Controller層 MVCアーキテクチャとクラス設計 // --- コントローラー (Controller) 層のイメージ ---

    // Webフレームワークにおけるリクエストハンドラに相当 // Service層を利用してユースケースを実行 class UserController { constructor(private userService: UserService) {} // 例: ユーザー作成リクエストの処理 async handleCreateUserRequest(req: { body: { name: string; email: string } }, res: any): Promise<void> { ... } // 例: ユーザー取得リクエストの処理 async handleGetUserRequest(req: { params: { id: string } }, res: any): Promise<void> { ... } }
  49. 56 • View層でのクラス設計 ◦ Controllerから渡されたデータをもとに、HTMLなどのユーザーインターフェース を⽣成する ◦ 役割 ▪ データの表⽰、ユーザーへの情報提⽰

    ◦ クラス例 ▪ (フレームワークによるが)表⽰のための補助的なクラス(ViewModel, Presenter)が使われることもある (例: UserViewModel) ◦ 基本的には表⽰ロジックに専念し、複雑なビジネスロジックは含めない MVCアーキテクチャとクラス設計
  50. 57 • View層 MVCアーキテクチャとクラス設計 class UserViewModel { public readonly id:

    number; public readonly displayName: string; // 例: 敬称をつけるなど public readonly email: string; public readonly registrationDate: string; // 例: 日付を特定のフォーマットにする constructor(user: User) { this.id = user.id; // 表示用に名前を加工 (例: 様をつける) this.displayName = `${user.name} 様`; this.email = user.email.getValue(); // 日付を指定のフォーマットに変換 (例: YYYY/MM/DD) this.registrationDate = user.createdAt.toLocaleDateString('ja-JP'); } }
  51. 60 ディスカッション議題になりそうなものを⽣成AIに出⼒してもらいました • 「この状況でクラスを作成するべきか、それとも単なる関数で済ませるべきか?」 • 「既存のクラス設計で、保守性や拡張性に課題を感じている点はありますか?それはど のような状況ですか?」 • 「チーム内でクラス設計に関するガイドラインやルールはありますか?ある場合、ど のように運⽤されていますか?」

    • 「SOLID原則などの設計原則を、実際の開発でどのように意識していますか?具体的な 事例があれば教えてください。」 • 「リアーキテクチャリングの際に、クラス設計で特に苦労した点はありますか?」 • 「関数型プログラミングの要素を取り⼊れる中で、クラス設計にどのような影響があり ましたか?」 クラス設計に関するディスカッション
  52. 61 • Clean Architecture ◦ https://asciidwango.jp/post/176293765750/clean-architecture • REFACTORING GURU ~⽣成に関するデザインパターン~

    ◦ https://refactoring.guru/ja/design-patterns/creational-patterns
 • リファクタリング(第2版)既存のコードを安全に改善する ◦ https://www.ohmsha.co.jp/book/9784274224546/ • Difference Between MVC and MVT Architectural Design Patterns ◦ https://www.geeksforgeeks.org/difference-between-mvc-and-mvt-design-patt erns/ 参考⽂献