# clean code ###### tags: `improve` ## Objects and Data Structures ### Use getters and setters TypeScript支持getter/setter語法,使用getter和setter從封裝行為的對像中訪問數據可能比僅在對像上查找屬性更好。以下是一些原因: ```typescript type BankAccount = { balance: number; // ... } const value = 100; const account: BankAccount = { balance: 0, // ... }; if (value < 0) { throw new Error('Cannot set negative balance.'); } account.balance = value; ``` 示例使用了一個類型為BankAccount的對象,它包含了一個balance屬性和一些其他的屬性。在賦值之前,需要手動驗證值是否為負數,這樣做很麻煩,也容易出錯。 ```typescript class BankAccount { private accountBalance: number = 0; get balance(): number { return this.accountBalance; } set balance(value: number) { if (value < 0) { throw new Error('Cannot set negative balance.'); } this.accountBalance = value; } // ... } // Now `BankAccount` encapsulates the validation logic. // If one day the specifications change, and we need extra validation rule, // we would have to alter only the `setter` implementation, // leaving all dependent code unchanged. const account = new BankAccount(); account.balance = 100; ``` 使用了一個BankAccount類,它具有一個私有的accountBalance屬性和一個公共的getter和setter來訪問balance屬性。 setter驗證要設置的值是否為負,並在值為負時拋出錯誤。這有助於確保balance屬性始終是有效的值。 使用getter和setter可以確保對象的內部表示被封裝,任何對對象屬性的更改都被驗證,以確保它們是有效的,從而預防錯誤並提高代碼的可維護性。 1. 當我們想要超出獲取對象屬性時,我們無需查找和更改代碼庫中的每個訪問器。 ```typescript class User { private _name: string; constructor(name: string) { this._name = name; } get name(): string { return this._name.toUpperCase(); } } const user = new User('John'); console.log(user.name); // output: "JOHN" ``` 在上面的示例中,我們定義了一個User類,它具有一個私有的_name屬性和一個公共的getter來訪問該屬性。 getter將_name屬性的值轉換為大寫,並返回該值。這意味著我們可以使用user.name來獲取用戶的姓名,而無需查找和更改代碼庫中的每個訪問器。 2. 在進行設置時添加驗證非常簡單。 ```typescript class BankAccount { private _balance: number = 0; get balance(): number { return this._balance; } set balance(value: number) { if (value < 0) { throw new Error('Cannot set negative balance.'); } this._balance = value; } } const account = new BankAccount(); account.balance = 100; // valid account.balance = -50; // throws an error ``` 在上面的示例中,我們定義了一個BankAccount類,它具有一個私有的_balance屬性和一個公共的getter和setter來訪問該屬性。 setter驗證要設置的值是否為負數,並在值為負數時拋出錯誤。這有助於確保_balance屬性始終是有效的值。 3. 封裝內部表示。 ```typescript class Person { private _name: string; private _age: number; constructor(name: string, age: number) { this._name = name; this._age = age; } get name(): string { return this._name; } set name(value: string) { this._name = value.trim(); } get age(): number { return this._age; } set age(value: number) { if (value < 0) { throw new Error('Age cannot be negative.'); } this._age = value; } } const person = new Person('John', 30); person.name = ' Jane '; person.age = 25; console.log(person.name); // output: "Jane" console.log(person.age); // output: 25 ``` 在上面的示例中,我們定義了一個Person類,它具有一個私有的_name屬性和一個私有的_age屬性,以及公共的getter和setter來訪問這些屬性。 setter使用trim方法來刪除用戶名中的空格。這意味著我們可以使用person.name來獲取用戶的姓名,而無需關心姓名中是否包含空格。 4. 在獲取和設置時添加日誌記錄和錯誤處理非常容易。 ```typescript class Counter { private _count: number = 0; get count(): number { console.log('Getting count...'); return this._count; } set count(value: number) { if (value < 0) { throw new Error('Count cannot be negative.'); } console.log('Setting count...'); this._count = value; } } const counter = new Counter(); counter.count = 10; // logs "Setting count..." console.log(counter.count); // logs "Getting count..." and returns 10 ``` 在上面的示例中,我們定義了一個Counter類,它具有一個私有的_count屬性和一個公共的getter和setter來訪問該屬性。 getter和setter分別在獲取和設置屬性時記錄日誌。 setter還驗證要設置的值是否為負數,並在值為負數時拋出錯誤。這使得添加日誌記錄和錯誤處理變得非常容易。 5. 您可以惰性加載您對象的屬性,比如從服務器獲取它。 ```typescript class User { private _name: string | null = null; get name(): string { if (this._name === null) { // fetch name from server this._name = 'John'; } return this._name; } } const user = new User(); console.log(user.name); // logs "John", fetches name from server first time console.log(user.name); // logs "John", returns cached name second time ``` 我們可以惰性加載用戶的屬性,例如從服務器獲取它,而不必在對象創建時立即獲取該屬性的值。這可以提高性能,並減少網絡請求。 提高 cache hit 減少 cache miss ```typescript type Member = { id: string, name: string, } type Team = { name: string, members: Member[], currentMember: Member } function mustFind<T>(arr: T[], predicate: (t: T) => boolean) { const item = arr.find(predicate) if (item) return item throw new Error('item not find') } function problemGetMemberById(memberId: string): Team { const Members: Member[] = [ { id: '1', name: 'Danny' }, { id: '2', name: 'Alex' }, { id: '3', name: 'Blob' }, ] return { name: 'My_team', members: Members, currentMember: Members.find(item => item.id === memberId) } } function getMemberById(memberId: string): Team { const Members: Member[] = [ { id: '1', name: 'Danny' }, { id: '2', name: 'Alex' }, { id: '3', name: 'Blob' }, ] const currentMember = mustFind(Members, (item) => item.id === memberId) return { name: 'My_team', members: Members, currentMember } } function getMemberById2(memberId: string): Team { const Members: Member[] = [ { id: '1', name: 'Danny' }, { id: '2', name: 'Alex' }, { id: '3', name: 'Blob' }, ] return { name: 'My_team', members: Members, get currentMember() { const member = Members.find(i => i.id === memberId) if (member) return member throw new Error('Member not find') } } } ``` ### Make objects have private/protected members ```typescript! //bad class Circle { radius: number; constructor(radius: number) { this.radius = radius; } perimeter() { return 2 * Math.PI * this.radius; } surface() { return Math.PI * this.radius * this.radius; } } //good class Circle { constructor(private readonly radius: number) { } perimeter() { return 2 * Math.PI * this.radius; } surface() { return Math.PI * this.radius * this.radius; } } ``` 在TypeScript中,支持使用public(默認)、protected和private訪問器來訪問類成員。 上面的示例中,我們定義了一個Circle類,用於計算圓的周長和麵積。在“Bad”示例中,我們將radius屬性設置為公共屬性,這意味著它可以被外部代碼直接訪問和更改。這違反了封裝的原則,因為我們希望將radius屬性的訪問限制在類內部。 在“Good”示例中,我們將radius屬性設置為只讀的私有屬性。這意味著我們可以在類內部訪問和更改它,但是外部代碼不能訪問或更改它。這符合封裝的原則,因為我們可以控制radius屬性的訪問權限,並防止外部代碼對它進行非法操作。 ### Prefer immutability ```typescript //bad interface Config { host: string; port: string; db: string; } //good interface Config { readonly host: string; readonly port: string; readonly db: string; } ``` ```typescript //bad const array: number[] = [ 1, 3, 5 ]; array = []; // error array.push(100); // array will be updated //good const array: ReadonlyArray<number> = [ 1, 3, 5 ]; array = []; // error array.push(100); // error ``` ```typescript! //bad const config = { hello: 'world' }; config.hello = 'world'; // value is changed const array = [ 1, 3, 5 ]; array[0] = 10; // value is changed // writable objects is returned function readonlyData(value: number) { return { value }; } const result = readonlyData(100); result.value = 200; // value is changed //good // read-only object const config = { hello: 'world' } as const; config.hello = 'world'; // error // read-only array const array = [ 1, 3, 5 ] as const; array[0] = 10; // error // You can return read-only objects function readonlyData(value: number) { return { value } as const; } const result = readonlyData(100); result.value = 200; // error ``` 以下是 TypeScript 中的不可變性的 5 個重點: 1. 使用 `readonly` 關鍵字來標記只讀屬性 2. 使用 `ReadonlyArray<T>` 來創建只讀數組 3. 在函數中使用 `readonly` 關鍵字來標記只讀參數 4. 使用 `const` 斷言將對象標記為只讀 5. 使用 `as const` 將數組和對象字面量標記為只讀 下面給出一個示例代碼,演示瞭如何使用上述 5 個技術來實現不可變性: ```typescript interface User { readonly id: number; readonly name: string; readonly age: number; } function createUser(id: number, name: string, age: number): User { return { id, name, age, } as const; } const users: ReadonlyArray<User> = [ createUser(1, 'Alice', 25), createUser(2, 'Bob', 30), createUser(3, 'Charlie', 35), ]; function printUsers(users: ReadonlyArray<User>) { users.forEach((user) => { console.log(`ID: ${user.id}, Name: ${user.name}, Age: ${user.age}`); }); } printUsers(users); // Attempt to modify a property of a User object users[0].name = 'Eve'; // Error: Cannot assign to 'name' because it is a read-only property. ``` 在上面的代碼中,我們定義了一個只讀的 `User` 接口,它包含了 `id`、`name` 和 `age` 三個只讀屬性。我們還定義了一個 `createUser` 函數,它使用 `readonly` 關鍵字來標記返回的對象為只讀。我們使用 `as const` 將對象字面量強制轉換為只讀對象。 我們還定義了一個只讀的 `users` 數組,它包含了三個只讀的 `User` 對象。我們使用 `ReadonlyArray<User>` 來標記這個數組為只讀數組。 最後,我們定義了一個 `printUsers` 函數,它接受一個只讀的 `users` 數組作為參數,並輸出每個 `User` 對象的 `id`、`name` 和 `age` 屬性。我們嘗試修改 `users` 數組中第一個 `User` 對象的 `name` 屬性,但是 TypeScript 報告了一個錯誤,因為這個屬性是只讀的。 ### type vs. interface ```typescript //bad interface EmailConfig { // ... } interface DbConfig { // ... } interface Config { // ... } //... type Shape = { // ... } //good type EmailConfig = { // ... } type DbConfig = { // ... } type Config = EmailConfig | DbConfig; // ... interface Shape { // ... } class Circle implements Shape { // ... } class Square implements Shape { // ... } ``` | Aspect | Type | Interface | | --- | --- | --- | | Can describe functions | ✅ | ✅ | | Can describe constructors | ✅ | ✅ | | Can describe tuples | ✅ | ✅ | | Interfaces can extend it | ⚠️ | ✅ | | Classes can extend it | 🚫 | ✅ | | Classes can implement it (implements) | ⚠️ | ✅ | | Can intersect another one of its kind | ✅ | ⚠️ | | Can create a union with another one of its kind | ✅ | 🚫 | | Can be used to create mapped types | ✅ | 🚫 | | Can be mapped over with mapped types | ✅ | ✅ | | Expands in error messages and logs | ✅ | 🚫 | | Can be augmented | 🚫 | ✅ | | Can be recursive | ⚠️ | ✅ | ### Can intersect another one of its kind: ```typescript type A = { propA: string; }; type B = { propB: number; }; type C = A & B; const c: C = { propA: 'hello', propB: 123, }; ``` ### Can create a union with another one of its kind: ```typescript type ResultType = number | string; function processResult(result: ResultType) { if (typeof result === 'number') { console.log(`The result is a number: ${result}`); } else { console.log(`The result is a string: ${result}`); } } processResult(123); processResult('hello'); ``` ### Can be used to create mapped types: ```typescript type Person = { name: string; age: number; }; type PersonRecord = { [K in keyof Person]: string; } const person: Person = { name: 'Alice', age: 30 }; const personRecord: PersonRecord = { name: 'Alice', age: '30' }; ``` ### Can be mapped over with mapped types: ```typescript type Person = { name: string; age: number; }; type PersonPartial = { [K in keyof Person]?: Person[K]; } const person: Person = { name: 'Alice', age: 30 }; const personPartial: PersonPartial = { name: 'Alice' }; type PersonPartials<T> = { [k in keyof T]?:T[k] } ``` ### Expands in error messages and logs: ```typescript type Person = { name: string; age: number; }; function greet(person: Person) { console.log(`Hello, ${person.name}! You are ${person.age} years old.`); } const person = { name: 'Alice', age: '30' }; greet(person); ``` `person.age` 的类型错误。 ### Can be augmented: ```typescript interface Person { name: string; age: number; } interface Person { gender: string; } const person: Person = { name: 'Alice', age: 30, gender: 'female' }; ``` ### Can be recursive: ```typescript type TreeNode<T> = { value: T; left?: TreeNode<T>; right?: TreeNode<T>; }; const root: TreeNode<number> = { value: 1, left: { value: 2, left: { value: 4 }, right: { value: 5 }, }, right: { value: 3, left: { value: 6 }, right: { value: 7 }, }, }; ```