From 8c4e50143838c50c74d76b87446621d11c427da5 Mon Sep 17 00:00:00 2001 From: Dumitru Deveatii Date: Mon, 4 Feb 2019 18:09:04 +0200 Subject: [PATCH] Classes --- README.md | 276 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 276 insertions(+) diff --git a/README.md b/README.md index beba4e8..809c8df 100644 --- a/README.md +++ b/README.md @@ -1095,6 +1095,282 @@ interface Config { } ``` +## Classes + +### Classes should be small + +The class' size is measured by it's responsibility. Following the *Single Responsibility principle* a class should be small. + +**Bad:** + +```ts +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 { /* ... */ } + // ... +} + +``` + +**Good:** + +```ts +class Dashboard { + disable(): void { /* ... */ } + enable(): void { /* ... */ } + getVersion(): string { /* ... */ } +} + +// split the responsibilities by moving the remaining methods to other classes +// ... +``` + +**[⬆ back to top](#table-of-contents)** + +### High cohesion and low coupling + +Cohesion defines the degree to which class members are related to each other. Ideally, all fields within a class should be used by each method. +We then say that the class is maximally cohesive. In practice, this however is not always possible, nor even advisable. You should however prefer cohesion to be high. +Coupling refers to how related or dependent are two classes toward each other. Classes are said to be low coupled if changes in one of them doesn't affect the other one. + +Good software design has high cohesion and low coupling. + +**Bad:** + +```ts +class UserManager { + // Bad: each private variable is used by one or another group of methods. + // It makes clear evidence that the class is holding more than a single responsibility. + // If I need only to create the service to get the transactions for a user, + // I'm still forced to pass and instance of emailSender. + constructor( + private readonly db: Database, + private readonly emailSender: EmailSender) { + } + + async getUser(id: number): Promise { + return await db.users.findOne({ id }) + } + + async getTransactions(userId: number): Promise { + return await db.transactions.find({ userId }) + } + + async sendGreeting(): Promise { + await emailSender.send('Welcome!'); + } + + async sendNotification(text: string): Promise { + await emailSender.send(text); + } + + async sendNewsletter(): Promise { + // ... + } +} +``` + +**Good:** + +```ts +class UserService { + constructor(private readonly db: Database) { + } + + async getUser(id: number): Promise { + return await db.users.findOne({ id }) + } + + async getTransactions(userId: number): Promise { + return await db.transactions.find({ userId }) + } +} + +class UserNotifier { + constructor(private readonly emailSender: EmailSender) { + } + + async sendGreeting(): Promise { + await emailSender.send('Welcome!'); + } + + async sendNotification(text: string): Promise { + await emailSender.send(text); + } + + async sendNewsletter(): Promise { + // ... + } +} +``` + +**[⬆ back to top](#table-of-contents)** + +### Prefer composition over inheritance + +As stated famously in [Design Patterns](https://en.wikipedia.org/wiki/Design_Patterns) by the Gang of Four, you should prefer composition over inheritance where you can. There are lots of good reasons to use inheritance and lots of good reasons to use composition. The main point for this maxim is that if your mind instinctively goes for inheritance, try to think if composition could model your problem better. In some cases it can. + +You might be wondering then, "when should I use inheritance?" It depends on your problem at hand, but this is a decent list of when inheritance makes more sense than composition: + +1. Your inheritance represents an "is-a" relationship and not a "has-a" relationship (Human->Animal vs. User->UserDetails). + +2. You can reuse code from the base classes (Humans can move like all animals). + +3. You want to make global changes to derived classes by changing a base class. (Change the caloric expenditure of all animals when they move). + +**Bad:** + +```ts +class Employee { + constructor( + private readonly name: string, + private readonly email:string) { + } + + // ... +} + +// Bad because Employees "have" tax data. EmployeeTaxData is not a type of Employee +class EmployeeTaxData extends Employee { + constructor( + name: string, + email:string, + private readonly ssn: string, + private readonly salary: number) { + super(name, email); + } + + // ... +} +``` + +**Good:** + +```ts +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) { + } + + // ... +} +``` + +**[⬆ back to top](#table-of-contents)** + +### Use method chaining + +This pattern is very useful and commonly used in many libraries. It allows your code to be expressive, and less verbose. For that reason, use method chaining and take a look at how clean your code will be. + +**Bad:** + +```ts +class Query { + 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; + } + + async execute(db: Database): Promise { + // ... + } +} + +// ... + +const query = new Query(); +query.from('users'); +query.page(1, 100); +query.orderBy('firstName', 'lastName'); + +const users = await query.execute(db); +``` + +**Good:** + +```ts +class Query { + private collection: string; + private pageNumber: number = 1; + private itemsPerPage: number = 100; + private orderByFields: string[] = []; + + + from(collection: string): Query { + this.collection = collection; + return this; + } + + page(number: number, itemsPerPage: number = 100): Query { + this.pageNumber = number; + this.itemsPerPage = itemsPerPage; + return this; + } + + orderBy(...fields: string[]): Query { + this.orderByFields = fields; + return this; + } + + async execute(db: Database): Promise { + // ... + } +} + +// ... + +const users = await new Query() + .from('users') + .page(1, 100) + .orderBy('firstName', 'lastName') + .execute(db); +``` + +**[⬆ back to top](#table-of-contents)** + ## SOLID ### Single Responsibility Principle (SRP)