# 第4,5章 不変の活用・低凝集 ## 第四章 不変の活用ー安定動作を構築するー ### 再代入 再代入では、変数に新しい値が割り当てられますが、元の値は破壊されません。 ```jsx let num = 10; // 再代入 num = 20; console.log(num); // 20 ``` 再代入のリスクとは、プログラム内で予期しない変数の変更が発生し、バグやデータ破損を引き起こすことです。 - **`const`** の使用 **`const`** を使用して変数を宣言することで、その変数への再代入を防ぐことができます。**`const`** で宣言された変数は一度だけ初期化され、その後は変更できません。 ```jsx const num = 10; num = 20; // エラー: "Assignment to constant variable. ``` - オブジェクトや配列の不変性を保つ JavaScriptのオブジェクトや配列は参照型です。不変性を保つために、新しいオブジェクトや配列を作成し、元のオブジェクトや配列を変更しないようにすることが重要です。これには、**`Object.assign()`**やスプレッド演算子(**`...`**)を使用できます。 ```jsx // オブジェクトの不変性を保つ const originalObj = { a: 1, b: 2 }; const newObj = { ...originalObj, c: 3 }; // 新しいオブジェクトを作成 ``` JavaScriptの関数において、引数自体を不変にすることはできません。しかし、関数内で引数の値を変更しないようにコーディングすることで、引数を不変として扱うことができます。 プリミティブ型(文字列、数値、ブール値)は関数に渡される際、値がコピーされるため、関数内で変更しても元の値に影響はありません。問題は、参照型(オブジェクト、配列)を関数に渡す場合です。参照型は参照(アドレス)がコピーされるため、関数内で変更すると元の値に影響を与えます。 この問題を解決するために、関数内で引数のコピーを作成し、それを使用することで、元の値が変更されないようにできます。ただし、オブジェクトや配列の場合、浅いコピーではなく深いコピーを作成することが重要です。これにより、ネストされたオブジェクトや配列の変更も元の値に影響を与えなくなります。 ```jsx import cloneDeep from 'lodash'; function updateObject(obj, key, value) { const deepCopy = cloneDeep(obj); // 深いコピーを作成 deepCopy[key] = value; return deepCopy; } const originalObj = { a: 1, b: { c: 2 } }; const updatedObj = updateObject(originalObj, 'b', { c: 3 }); console.log(originalObj); // { a: 1, b: { c: 2 } } console.log(updatedObj); // { a: 1, b: { c: 3 } } ``` [https://lodash.com/docs/4.17.15](https://lodash.com/docs/4.17.15#cloneDeep) ### 主作用副作用 主作用:関数が期待される結果を返すことです。 副作用:関数が実行される際に、その関数の外部状態に影響を与えることです。 例→関数がグローバル変数を変更したり、データベースに書き込んだりする場合、副作用が発生します。 副作用のデメリット: 1. 予測不可能な動作: 副作用がある関数は、外部状態に依存しているため、予測が難しくなります。その結果、バグが発生しやすくなります。 2. 再利用性の低下: 副作用がある関数は、外部状態に依存するため、他の状況で再利用することが難しくなります。 3. テストの困難さ: 副作用がある関数は、外部状態や環境を正確に再現する必要があるため、テストが困難になります。 4. コードの読みやすさの低下: 副作用がある関数は、関数の外部状態を考慮する必要があるため、コードの読みやすさが低下します。 対策: 1. 純粋関数の使用: 同じ入力に対して常に同じ出力を返し、副作用のない関数を使用することで、コードの予測可能性と再利用性を向上させることができます。 2. 不変性の維持: オブジェクトや配列を操作する際、元のデータを変更せずに新しいデータ構造を作成することで、副作用を防ぐことができます。 3. 関数型プログラミングの原則に従う: 関数型プログラミングは、純粋関数、不変性、データ変換などの原則に従って、副作用を最小限に抑えることを目指します。 4. 副作用を分離する: 必要な副作用(例: API呼び出し、データベースアクセス)を、関数の外部で行い、それらを純粋関数に渡すようにすることで、副作用の影響を分離できます。 ### Tips. Next.js におけるuseEffectは関係あるのか **`useEffect`**はReactの機能の一つで、コンポーネントのライフサイクルに関連した副作用(サイドエフェクト)を扱うために使用されます。したがって、Next.jsアプリケーションでReactコンポーネントを使用している場合、**`useEffect`**と副作用の関係があります。 **`useEffect`**は、コンポーネントがマウントされたり更新されたりしたときに、副作用を実行するためのフックです。例えば、APIからデータを取得したり、イベントリスナーを設定したりする際に**`useEffect`**を使用します。また、**`useEffect`**内でクリーンアップ関数を返すことで、コンポーネントがアンマウントされる際や、依存関係が更新される際に、副作用をクリーンアップすることができます。 Next.jsにおいても、**`useEffect`**を使って、コンポーネントのライフサイクルに応じて副作用を実行・管理することができます。ただし、**`useEffect`**はクライアントサイドでのみ実行されるため、サーバーサイドレンダリング(SSR)や静的サイト生成(SSG)においては、**`useEffect`**内の副作用は実行されません。 副作用と**`useEffect`**の使用は、Next.jsアプリケーションにおいても、適切に管理された状態でコードを構築する上で重要です。**`useEffect`**を使用することで、コンポーネントのライフサイクルに適したタイミングで副作用を実行し、クリーンアップを行うことができます。これにより、コンポーネントが保守性が高く、予測可能な状態で動作するようになります。 ## 第五章 低凝集ーバラバラになったモノたちー 低凝集(Low Cohesion)は、ソフトウェア設計において、あるモジュールやクラスが関連性の低い複数の責任を持っている状態を指します。低凝集には以下のような問題点があります。 1. 保守性の低下: 関連性の低い複数の機能が1つのモジュールやクラスにまとめられているため、変更が困難になります。コードを修正する際に、予期しない影響が他の機能に及ぶ可能性が高まります。 2. 再利用性の低下: 低凝集のモジュールやクラスは、特定の機能を再利用する際に、関連しない他の機能も含まれてしまうため、再利用が難しくなります。 3. 理解しにくいコード: 関連性の低い機能が混在しているため、コードの目的や動作を理解するのが難しくなります。これにより、コードの品質が低下し、バグが発生しやすくなります。 4. テストの困難さ: 低凝集のモジュールやクラスは、個々の機能を分離してテストするのが難しくなります。これにより、品質保証が困難になり、バグの発見や修正が遅れることがあります。 5. 開発効率の低下: 関連性の低い複数の機能が1つのモジュールやクラスに存在するため、開発者がそのモジュールやクラス全体を把握する必要があります。これにより、開発の効率が低下します。 低凝集の問題点を解決するためには、高凝集(High Cohesion)を目指すことが重要です。高凝集とは、各モジュールやクラスが1つの責任を持ち、関連性のある機能がまとめられている状態を指します。これにより、保守性、再利用性、テストの容易さ、コードの理解しやすさ、開発効率が向上します。 低凝集の例 ```tsx interface User { id: number; name: string; age: number; } class UserManager { private users: User[]; constructor() { this.users = []; } addUser(user: User): void { if (this.isValidUser(user)) { this.users.push(user); } } removeUser(userId: number): void { this.users = this.users.filter(user => user.id !== userId); } // バリデーションの責任も持っている isValidUser(user: User): boolean { return user.id !== undefined && user.name !== undefined && user.age !== undefined; } // 計算の責任も持っている calculateAverageAge(): number { const totalAge = this.users.reduce((sum, user) => sum + user.age, 0); return totalAge / this.users.length; } } ``` リファクタ ```tsx interface User { id: number; name: string; age: number; } class UserManager { private users: User[]; constructor() { this.users = []; } addUser(user: User): void { if (UserValidator.isValidUser(user)) { this.users.push(user); } } removeUser(userId: number): void { this.users = this.users.filter(user => user.id !== userId); } getUsers(): User[] { return this.users; } } class UserValidator { static isValidUser(user: User): boolean { return user.id !== undefined && user.name !== undefined && user.age !== undefined; } } class UserCalculator { static calculateAverageAge(users: User[]): number { const totalAge = users.reduce((sum, user) => sum + user.age, 0); return totalAge / users.length; } } const userManager = new UserManager(); userManager.addUser({ id: 1, name: 'John', age: 30 }); userManager.addUser({ id: 2, name: 'Jane', age: 25 }); console.log(UserCalculator.calculateAverageAge(userManager.getUsers())); // 27.5 ``` zod ```tsx import { z } from 'zod'; const UserSchema = z.object({ id: z.number(), name: z.string(), age: z.number(), }); type User = z.infer<typeof UserSchema>; class UserManager { private users: User[]; constructor() { this.users = []; } addUser(user: User): void { if (UserValidator.isValidUser(user)) { this.users.push(user); } } removeUser(userId: number): void { this.users = this.users.filter(user => user.id !== userId); } getUsers(): User[] { return this.users; } } class UserValidator { static isValidUser(user: unknown): user as User { try { UserSchema.parse(user); return true; } catch (error) { return false; } } } class UserCalculator { static calculateAverageAge(users: User[]): number { const totalAge = users.reduce((sum, user) => sum + user.age, 0); return totalAge / users.length; } } const userManager = new UserManager(); userManager.addUser({ id: 1, name: 'John', age: 30 }); userManager.addUser({ id: 2, name: 'Jane', age: 25 }); console.log(UserCalculator.calculateAverageAge(userManager.getUsers())); // 27.5 ``` Tips デメテルの法則 デメテルの法則(Law of Demeter)は、オブジェクト指向プログラミングにおいて、オブジェクト間の疎結合を促す設計原則です。デメテルの法則は「最小知識の原則(Principle of Least Knowledge)」とも呼ばれ、オブジェクトはできるだけ少数の隣接するオブジェクトとだけやりとりし、他のオブジェクトの詳細については知らないようにすべきと提案しています。 デメテルの法則は、以下のような指針で表されることが多いです: 1. オブジェクト自身 2. メソッドに渡された引数 3. そのメソッドで作成されたオブジェクト 4. オブジェクトのプロパティ(インスタンス変数) デメテルの法則に従うことで、以下の利点が得られます: - オブジェクト間の疎結合が促され、コードの保守性や拡張性が向上します。 - オブジェクトの変更が他のオブジェクトに影響を与えにくくなります。 - コードの可読性が向上します。 デメテルの法則に反する例: ```go package main type Engine struct { Status string } type Car struct { Engine *Engine } func main() { car := &Car{Engine: &Engine{Status: "Running"}} // デメテルの法則に反している:carのエンジンの状態を直接参照している if car.Engine.Status == "Running" { // do something } } ``` デメテルの法則に従った例: ```go package main type Engine struct { Status string } func (e *Engine) IsRunning() bool { return e.Status == "Running" } type Car struct { Engine *Engine } func (c *Car) IsEngineRunning() bool { return c.Engine.IsRunning() } func main() { car := &Car{Engine: &Engine{Status: "Running"}} // デメテルの法則に従っている:Carから提供されるメソッドを使用してエンジンの状態を確認する if car.IsEngineRunning() { // do something } } ``` typescript ```tsx class Engine { status: string; constructor(status: string) { this.status = status; } } class Car { engine: Engine; constructor(engine: Engine) { this.engine = engine; } } const car = new Car(new Engine("Running")); // デメテルの法則に反している:carのエンジンの状態を直接参照している if (car.engine.status === "Running") { // do something } ``` ```tsx class Engine { status: string; constructor(status: string) { this.status = status; } isRunning(): boolean { return this.status === "Running"; } } class Car { engine: Engine; constructor(engine: Engine) { this.engine = engine; } isEngineRunning(): boolean { return this.engine.isRunning(); } } const car = new Car(new Engine("Running")); // デメテルの法則に従っている:Carから提供されるメソッドを使用してエンジンの状態を確認する if (car.isEngineRunning()) { // do something } ``` --- [https://web-engineer-wiki.com/javascript/const-object-array/](https://web-engineer-wiki.com/javascript/const-object-array/) ## **【JavaScript】オブジェクトや配列でconstを使っても代入できてしまう** - **`const` 宣言** は、値への読み取り専用の参照を作ります。これは、定数に保持されている値は**不変ではなく** 、その変数の識別子が再代入できないということです。たとえば、定数の中身がオブジェクトの場合、オブジェクトの内容(プロパティなど)は変更可能です。 ```jsx const person = { id:1, name:"Alice" } person.age = 20; console.log(person); // -> {id:1, name:"Alice", age:20} Object.freeze(person); person.age = 25; console.log(person.age); // -> 20 ```   JavaScriptにおいて、deep copyとshallow copyの違いは以下のとおりです。 ### shallow copy(キャロウコピー) shallow copyは、オブジェクトのプロパティをコピーする方法です。この場合、オブジェクトのプロパティが参照型であった場合、元のオブジェクトとコピーされたオブジェクトで同じ参照が共有されます。 ``` const originalObject = { name: 'John', age: 30, address: { city: 'Tokyo', country: 'Japan' } }; // shallow copy const copiedObject = { ...originalObject }; console.log(originalObject === copiedObject); // false console.log(originalObject.address === copiedObject.address); // true ``` 上記の例では、`copiedObject`は`originalObject`とは別のオブジェクトとなりますが、`address`プロパティは同じオブジェクトを参照しています。 ### deep copy deep copyは、オブジェクトの全てのプロパティを再帰的にコピーする方法です。この場合、オブジェクトのプロパティが参照型であった場合でも、新しいオブジェクトが作成されます。 ```jsx const originalObject = { name: 'John', age: 30, address: { city: 'Tokyo', country: 'Japan' } }; // deep copy const copiedObject = JSON.parse(JSON.stringify(originalObject)); console.log(originalObject === copiedObject); // false console.log(originalObject.address === copiedObject.address); // false ``` 上記の例では、`copiedObject`は`originalObject`とは別のオブジェクトとなり、`address`プロパティも新しいオブジェクトを参照しています。 ただし、注意点があります。`JSON.parse(JSON.stringify(object))`は、オブジェクトのプロパティのうち、関数や`undefined`、`Symbol`などのデータ型を含むプロパティはコピーされないため、注意が必要です。