Первая часть, судя по комментариям вызвала неоднозначное мнение, особенно что касалось части enum. Где-то я так же могу не соглашаться, как с автором оригинала, так и с некоторыми комментариями. Но как и указывалось в начальном описании к первой части, чистый код это не догма которую нужно соблюдать, это всего лишь рекомендации, соблюдение которых каждый сам выбирает под свои потребности и взгляды.
Объекты и структуры данных
Используйте иммутабельность
Система типов в TypeScript позволяет помечать отдельные свойства интерфейса/класса как readonly поля (только для чтения). Это позволяет вам работать функционально (неожиданная мутация это плохо). Для более сложных сценариев есть встроенный тип Readonly
, который принимает тип T
и помечает все его свойства только для чтения с использованием mapped types (смотрите mapped types).
Плохо:
interface Config {
host: string;
port: string;
db: string;
}
Хорошо:
interface Config {
readonly host: string;
readonly port: string;
readonly db: string;
}
В случае массива вы можете создать массив только для чтения, используя ReadonlyArray<T>
. который не позволяет делать изменения с использованием push()
и fill()
, но можно использовать concat()
и slice()
они не меняют значения.
Плохо:
const array: number[] = [ 1, 3, 5 ];
array = []; // error
array.push(100); // array will updated
Хорошо:
const array: ReadonlyArray<number> = [ 1, 3, 5 ];
array = []; // error
array.push(100); // error
Объявление аргументов только для чтения TypeScript 3.4 is a bit easier.
function hoge(args: readonly string[]) {
args.push(1); // error
}
Предпочтение const assertions для литеральных значений.
Плохо:
const config = {
hello: 'world'
};
config.hello = 'world'; // значение изменено
const array = [ 1, 3, 5 ];
array[0] = 10; // значение изменено
// записываемые объекты возвращаются
function readonlyData(value: number) {
return { value };
}
const result = readonlyData(100);
result.value = 200; // значение изменено
Хорошо:
// объект только для чтения
const config = {
hello: 'world'
} as const;
config.hello = 'world'; // ошибка
// массив только для чтения
const array = [ 1, 3, 5 ] as const;
array[0] = 10; // ошибка
// Вы можете вернуть объект только для чтения
function readonlyData(value: number) {
return { value } as const;
}
const result = readonlyData(100);
result.value = 200; // ошибка
Типы vs. интерфейсы
Используйте типы, когда вам может понадобиться объединение или пересечение. Используйте интерфейс, когда хотите использовать extends
или implements
. Однако строгого правила не существует, используйте то, что работает у вас. Для более подробного объяснения посмотрите это ответы о различиях между type
and interface
в TypeScript.
Плохо:
interface EmailConfig {
// ...
}
interface DbConfig {
// ...
}
interface Config {
// ...
}
//...
type Shape = {
// ...
}
Хорошо:
type EmailConfig = {
// ...
}
type DbConfig = {
// ...
}
type Config = EmailConfig | DbConfig;
// ...
interface Shape {
// ...
}
class Circle implements Shape {
// ...
}
class Square implements Shape {
// ...
}
Классы
Классы должны быть маленькими
Размер класса измеряется его ответственностью. Следуя Принципу единственной ответственности класс должен быть маленьким.
Плохо:
class Dashboard {
getLanguage(): string { /* ... */ }
setLanguage(language: string): void { /* ... */ }
showProgress(): void { /* ... */ }
hideProgress(): void { /* ... */ }
isDirty(): boolean { /* ... */ }
disable(): void { /* ... */ }
enable(): void { /* ... */ }
addSubscription(subscription: Subscription): void { /* ... */ }
removeSubscription(subscription: Subscription): void { /* ... */ }
addUser(user: User): void { /* ... */ }
removeUser(user: User): void { /* ... */ }
goToHomePage(): void { /* ... */ }
updateProfile(details: UserDetails): void { /* ... */ }
getVersion(): string { /* ... */ }
// ...
}
Хорошо:
class Dashboard {
disable(): void { /* ... */ }
enable(): void { /* ... */ }
getVersion(): string { /* ... */ }
}
// разделить обязанности, переместив оставшиеся методы в другие классы
// ...
Высокая сплоченность низкая связь
Сплоченность определяет степень, в которой члены класса связаны друг с другом. В идеале все поля в классе должны использоваться каждым методом. Мы говорим, что класс максимально связный. На практике это, однако, не всегда возможно и даже нежелательно. Однако вы должны добиваться, того чтобы сплоченность была высокой.
Связанность относится и к тому, как связаны или зависимы два класса друг от друга. Классы считаются слабосвязанными если изменения в одном из них не влияют на другой.
Плохо:
class UserManager {
// Плохо: каждая закрытая переменная используется той или иной группой методов.
// Это ясно показывает, что класс несет больше, чем одну ответственность
// Если мне нужно только создать сервис, чтобы получить транзакции для пользователя,
// Я все еще вынужден передавать экземпляр `emailSender`.
constructor(
private readonly db: Database,
private readonly emailSender: EmailSender) {
}
async getUser(id: number): Promise<User> {
return await db.users.findOne({ id });
}
async getTransactions(userId: number): Promise<Transaction[]> {
return await db.transactions.find({ userId });
}
async sendGreeting(): Promise<void> {
await emailSender.send('Welcome!');
}
async sendNotification(text: string): Promise<void> {
await emailSender.send(text);
}
async sendNewsletter(): Promise<void> {
// ...
}
}
Хорошо:
class UserService {
constructor(private readonly db: Database) {
}
async getUser(id: number): Promise<User> {
return await this.db.users.findOne({ id });
}
async getTransactions(userId: number): Promise<Transaction[]> {
return await this.db.transactions.find({ userId });
}
}
class UserNotifier {
constructor(private readonly emailSender: EmailSender) {
}
async sendGreeting(): Promise<void> {
await this.emailSender.send('Welcome!');
}
async sendNotification(text: string): Promise<void> {
await this.emailSender.send(text);
}
async sendNewsletter(): Promise<void> {
// ...
}
}
Предпочитайте композицию наследованию
Как сказано в Design Patterns от банды черытех вы должны
Предпочитать композицию наследованию где можете. Есть много веских причин использовать наследование и много хороших причин использовать композицию. Суть этого принципа в том, что если ваш ум инстинктивно идет на наследование, попробуйте подумать, может ли композиция лучше смоделировать вашу проблему. В некоторых случаях может.
Тогда вы можете спросить: "Когда я должен использовать наследование?" Это зависит от вашей проблемы, но это достойный список, когда наследование имеет больше смысла, чем композиция:
- Ваше наследование представляет собой "is-a" отношения а не "has-a" отношения (Human->Animal vs. User->UserDetails).
- Вы можете повторно использовать код из базовых классов (Люди могут двигаться как все животные).
- Вы хотите внести глобальные изменения в производные классы, изменив базовый класс. (Изменение расхода калорий у всех животных при их перемещении).
Плохо:
class Employee {
constructor(
private readonly name: string,
private readonly email: string) {
}
// ...
}
// Плохо, потому что Employees "имеют" налоговые данные. EmployeeTaxData не является типом Employee
class EmployeeTaxData extends Employee {
constructor(
name: string,
email: string,
private readonly ssn: string,
private readonly salary: number) {
super(name, email);
}
// ...
}
Хорошо:
class Employee {
private taxData: EmployeeTaxData;
constructor(
private readonly name: string,
private readonly email: string) {
}
setTaxData(ssn: string, salary: number): Employee {
this.taxData = new EmployeeTaxData(ssn, salary);
return this;
}
// ...
}
class EmployeeTaxData {
constructor(
public readonly ssn: string,
public readonly salary: number) {
}
// ...
}
Используйте цепочки вызовов
Этот паттерн очень полезен и обычно используется во многих библиотеках. Это позволяет вашему коду быть выразительным и менее многословным. По этой причине используйте цепочку методов и посмотрите, насколько чистым будет ваш код.
Плохо:
class QueryBuilder {
private collection: string;
private pageNumber: number = 1;
private itemsPerPage: number = 100;
private orderByFields: string[] = [];
from(collection: string): void {
this.collection = collection;
}
page(number: number, itemsPerPage: number = 100): void {
this.pageNumber = number;
this.itemsPerPage = itemsPerPage;
}
orderBy(...fields: string[]): void {
this.orderByFields = fields;
}
build(): Query {
// ...
}
}
// ...
const queryBuilder = new QueryBuilder();
queryBuilder.from('users');
queryBuilder.page(1, 100);
queryBuilder.orderBy('firstName', 'lastName');
const query = queryBuilder.build();
Хорошо:
class QueryBuilder {
private collection: string;
private pageNumber: number = 1;
private itemsPerPage: number = 100;
private orderByFields: string[] = [];
from(collection: string): this {
this.collection = collection;
return this;
}
page(number: number, itemsPerPage: number = 100): this {
this.pageNumber = number;
this.itemsPerPage = itemsPerPage;
return this;
}
orderBy(...fields: string[]): this {
this.orderByFields = fields;
return this;
}
build(): Query {
// ...
}
}
// ...
const query = new QueryBuilder()
.from('users')
.page(1, 100)
.orderBy('firstName', 'lastName')
.build();
Продолжение следует...
NN1
Ещё есть синтаксис для неизменяемых туплов: readonly [string, number]
mariusschulz.com/blog/read-only-array-and-tuple-types-in-typescript