Slide 1

Slide 1 text

0 Webアプリケーションにおける クラスの設計再⼊⾨ 〜より良いオブジェクト指向を⽬指して〜 2025-04-25 第120回NearMe技術勉強会 Kaito Asahi

Slide 2

Slide 2 text

1 本⽇のゴール ● 改めてクラス設計の重要性を理解する ● より良いクラス設計のための考え⽅を学ぶ ● ⽇々の開発に活かせるヒントを得る ● 皆さんと共に現在抱えているクラス設計の課題などに関してディスカッションをしたい です!!

Slide 3

Slide 3 text

2 1. クラスの前に重要な OOP の考えについて

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

4 2. クラスって何ですか?

Slide 6

Slide 6 text

5 クラスとは? ● オブジェクトの設計図 ○ どのような属性を持ち、どのような振る舞いができるかを定義したもの Ex) Person クラス - Person には属性として name,age というものがある - ただし、まだ具体的には決まっていない class Person { constructor(name, age) { this.name = name; this.age = age; } greet() { console.log(`こんにちは、私は ${this.name}です。現在は ${this.age}歳です。 `); } }

Slide 7

Slide 7 text

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’)

Slide 8

Slide 8 text

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 メソッド

Slide 9

Slide 9 text

8 3. 良いクラス設計とは?

Slide 10

Slide 10 text

9 根底となる原則

Slide 11

Slide 11 text

10 良いクラス設計のための原則 1. SOLID原則 ○ クラス設計に関する以下の5つの原則をまとめたもの ■ 単⼀責任の原則(Single Responsiblity Principle - SRP) ■ オープン/クローズドの原則 (Open/Closed Principle - OCP) ■ リスコフの置換原則 (Liskov Substitution Principle - LSP) ■ インターフェース分離の原則 (Interface Segregation Principle - ISP) ■ 依存性逆転の原則 (Dependency Inversion Principle - DIP)

Slide 12

Slide 12 text

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();

Slide 13

Slide 13 text

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(); 俺はメーラーだよな ...

Slide 14

Slide 14 text

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(); え、俺データベースに値の保存もし なくちゃいけないのかよ 俺はメーラーだよな ...

Slide 15

Slide 15 text

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に 関するクラスの責任 俺はメーラーだよな ... え、俺データベースに値の保存もし なくちゃいけないのかよ

Slide 16

Slide 16 text

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)

Slide 17

Slide 17 text

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');

Slide 18

Slide 18 text

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');

Slide 19

Slide 19 text

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');

Slide 20

Slide 20 text

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');

Slide 21

Slide 21 text

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に合わせて重厚  に なるのは微妙

Slide 22

Slide 22 text

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) } }

Slide 23

Slide 23 text

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); // 正方形なので幅も高さに合わせる } }

Slide 24

Slide 24 text

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 (期待通り)

Slide 25

Slide 25 text

24 Ex) 正⽅形は⻑⽅形ではない? ● 数学の命題としては正しいが、クラス設計では挙動がおかしくなる場合がある ● ⻑⽅形を処理する関数を書き、⾯積を計算して出⼒ ○ ⾯積は 5×10=50 であるので正しい! ● 正⽅形を処理する関数を書き、⾯積を計算して出⼒ ○ なぜか⾯積が 100 として出てしまう ... リスコフの置換原則 (Liskov Substitution Principle - LSP) // 正方形を長方形として処理する const square = new Square(4); processRectangle(square); // 出力: 長方形の面積 : 100 (期待と異な る!)

Slide 26

Slide 26 text

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); // 正方形なので幅も高さに合わせる } }

Slide 27

Slide 27 text

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)

Slide 28

Slide 28 text

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; } } 別の状態として持つからダイジョーブ

Slide 29

Slide 29 text

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}`); } }

Slide 30

Slide 30 text

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がそれらを持っている ...

Slide 31

Slide 31 text

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 { ...

Slide 32

Slide 32 text

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)

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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 を修正)

Slide 36

Slide 36 text

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 を修正)

Slide 37

Slide 37 text

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);

Slide 38

Slide 38 text

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)

Slide 39

Slide 39 text

38 良いクラス設計のための原則 2. デザインパターン ● ソフトウェア設計において繰り返し発⽣する特定の問題に対する、再利⽤可能な、 実績のある解決策 ■ ⽣成パターン (Creational Patterns) ■ 構造パターン (Structural Patterns) ■ 振る舞いパターン (Behavioral Patterns) → ここら辺は量が多いので、今回は省略

Slide 40

Slide 40 text

39 4. クラス設計の動機

Slide 41

Slide 41 text

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);

Slide 42

Slide 42 text

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();

Slide 43

Slide 43 text

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();

Slide 44

Slide 44 text

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}さん!`); } } ...

Slide 45

Slide 45 text

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!`); } }

Slide 46

Slide 46 text

45 5. Webアプリケーションにおけるクラス設計

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

48 ● Model層でのクラス設計 ○ エンティティ (Entity) ■ ドメインの中⼼的な概念を表すクラス ■ ⼀意なIDを持ち、状態が変わる(例: User, Product, Order) ○ 値オブジェクト (Value Object) ■ 値そのものを表すクラス ■ 不変(Immutable)であることが多い(例: Address, Money, EmailAddress) ● EmailAddress クラスにバリデーションロジックを持たせるなど、振る舞い もカプセル化できる MVCアーキテクチャとクラス設計

Slide 50

Slide 50 text

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); } }

Slide 51

Slide 51 text

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()} に変更されました。`); } }

Slide 52

Slide 52 text

51 ● Model層でのクラス設計 ○ リポジトリ (Repository) ■ データの永続化(DBアクセスなど)を抽象化するクラスエンティティの取得や 保存を⾏う(例: UserRepository, ProductRepository) ● DIP(依存性逆転の原則)を適⽤し、インターフェース (IUserRepository) に依存させることで、DB実装の変更に強くなる ○ サービス (Service) ■ 特定のビジネスユースケースを実現するロジックを担当 ■ 複数のエンティティやリポジトリを跨る処理をまとめる。(例: OrderService, PaymentService) ● SRP(単⼀責任の原則)に基づき、関⼼事を分離する。 MVCアーキテクチャとクラス設計

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

53 ● Model層(Service) MVCアーキテクチャとクラス設計 // --- サービス (Service) --- // ビジネスロジックを担当 (SRP: 単一責任の原則) // リポジトリインターフェースに依存 (DIP) class UserService { // 依存性の注入 (Constructor Injection) constructor(private userRepository: IUserRepository) {} async createUser(name: string, emailValue: string): Promise { ... } async findUserById(id: number): Promise { return this.userRepository.findById(id); } async changeUserName(userId: number, newName: string): Promise { const user = await this.userRepository.findById(userId); if (!user) { throw new Error(`ユーザー (ID: ${userId}) が見つかりませ ん。`); } user.changeName(newName); await this.userRepository.save(user); // 変更を保存 } }

Slide 55

Slide 55 text

54 ● Controller層でのクラス設計 ○ ユーザーからのHTTPリクエストを受け取り、適切なServiceやRepositoryを呼び出 して処理を実⾏する ○ 処理結果に応じて、表⽰するView/Templateを選択し、必要なデータを渡す ○ 役割 ■ リクエストのルーティング、⼊⼒値のバリデーション(Formクラスなどを使う 場合も)、Service層への処理委譲、レスポンス⽣成 ○ クラス例 ■ UserController, ProductController, OrderForm ○ 注意点 ■ Controller層がビジネスロジックを持ちすぎないように注意する(Fat Controllerを避ける、SRPを意識して、ServiceやModel層に委譲) MVCアーキテクチャとクラス設計

Slide 56

Slide 56 text

55 ● Controller層 MVCアーキテクチャとクラス設計 // --- コントローラー (Controller) 層のイメージ --- // Webフレームワークにおけるリクエストハンドラに相当 // Service層を利用してユースケースを実行 class UserController { constructor(private userService: UserService) {} // 例: ユーザー作成リクエストの処理 async handleCreateUserRequest(req: { body: { name: string; email: string } }, res: any): Promise { ... } // 例: ユーザー取得リクエストの処理 async handleGetUserRequest(req: { params: { id: string } }, res: any): Promise { ... } }

Slide 57

Slide 57 text

56 ● View層でのクラス設計 ○ Controllerから渡されたデータをもとに、HTMLなどのユーザーインターフェース を⽣成する ○ 役割 ■ データの表⽰、ユーザーへの情報提⽰ ○ クラス例 ■ (フレームワークによるが)表⽰のための補助的なクラス(ViewModel, Presenter)が使われることもある (例: UserViewModel) ○ 基本的には表⽰ロジックに専念し、複雑なビジネスロジックは含めない MVCアーキテクチャとクラス設計

Slide 58

Slide 58 text

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'); } }

Slide 59

Slide 59 text

58 ● 全体 → https://gist.github.com/asakaicode/31ba8e237ace4bc5861b702df6d4be27 MVCアーキテクチャとクラス設計

Slide 60

Slide 60 text

59 6. クラス設計に関するディスカッション

Slide 61

Slide 61 text

60 ディスカッション議題になりそうなものを⽣成AIに出⼒してもらいました ● 「この状況でクラスを作成するべきか、それとも単なる関数で済ませるべきか?」 ● 「既存のクラス設計で、保守性や拡張性に課題を感じている点はありますか?それはど のような状況ですか?」 ● 「チーム内でクラス設計に関するガイドラインやルールはありますか?ある場合、ど のように運⽤されていますか?」 ● 「SOLID原則などの設計原則を、実際の開発でどのように意識していますか?具体的な 事例があれば教えてください。」 ● 「リアーキテクチャリングの際に、クラス設計で特に苦労した点はありますか?」 ● 「関数型プログラミングの要素を取り⼊れる中で、クラス設計にどのような影響があり ましたか?」 クラス設計に関するディスカッション

Slide 62

Slide 62 text

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/ 参考⽂献

Slide 63

Slide 63 text

62 Thank you