visit
Previous Parts:
A class should have only one reason to change
// Bad
class UserSettingsService {
constructor(user: IUser) {
this.user = user;
}
changeSettings(settings: IUserSettings): void {
if (this.isUserValidated()) {
// ...
}
}
getUserInfo(): Promise<IUserSettings> {
// ...
}
async isUserValidated(): Promise<boolean> {
const userInfo = await this.getUserInfo();
// ...
}
}
In this example, our class carries out actions in different directions: it sets up context, changes it, and validates it.
// Better
class UserAuth {
constructor(user: IUser) {
this.user = user;
}
getUserInfo(): Promise<IUserSettings> {
// ...
}
async isUserValidated(): boolean {
const userInfo = await this.getUserInfo();
// ...
}
}
class UserSettings {
constructor(user: IUser) {
this.user = user;
this.auth = new UserAuth(user);
}
changeSettings(settings: IUserSettings): void {
if (this.auth.isUserValidated()) {
// ...
}
}
}
Software entities (classes, modules, functions) should be open for extension but closed for modification
// Bad
class Product {
id: number;
name: string[];
price: number;
protected constructor(id: number, name: string[], price: number) {
this.id = id;
this.name = name;
this.price = price;
}
}
class Ananas extends Product {
constructor(id: number, name: string[], price: number) {
super(id, name, price);
}
}
class Banana extends Product {
constructor(id: number, name: string[], price: string) {
super(id, name, price);
}
}
class HttpRequestCost {
constructor(product: Product) {
this.product = product;
}
getDeliveryCost(): number {
if (product instanceOf Ananas) {
return requestAnanas(url).then(...);
}
if (product instanceOf Banana) {
return requestBanana(url).then(...);
}
}
}
function requestAnanas(url: string): Promise<ICost> {
// logic for ananas
}
function requestBanana(url: string): Promise<ICost> {
// logic for bananas
}
In this example, the problem is in class HttpRequestCost
, which, in method, getDeliveryCost
contains conditions for the calculation of different types of products, and we use separate methods for each type of product. So, if we need to add a new type of product, we should modify the HttpRequestCost
class, and it is not safe; we could get unexpected results.
To avoid it, we should create an abstract method request
in Product
class without realizations. The particular realization will have inherited the classes: Ananas and Banana. They will realize the request for themself.
HttpRequestCost
will take the product
parameter following the Product
class interface, and when we pass particular dependencies in HttpRequestCost
, it will already realize request
method for itself.
// Better
abstract class Product {
id: number;
name: string[];
price: string;
constructor(id: number, name: string[], price: string) {
this.id = id;
this.name = name;
this.price = price;
}
abstract request(url: string): void;
}
class Ananas extends Product {
constructor(id: number, name: string[], price: string) {
super(id, name, price);
}
request(url: string): void {
// logic for ananas
}
}
class Banana extends Product {
constructor(id: number, name: string[], price: string) {
super(id, name, price);
}
request(url: string): void {
// logic for bananas
}
}
class HttpRequestCost {
constructor(product: Product) {
this.product = product;
}
request(): Promise<void> {
return this.product.request(url).then(...);
}
}
Objects of a superclass should be replaceable with objects of its subclasses without breaking the application.
// Bad
class Worker {
work(): void {/../}
access(): void {
console.log('Have an access to closed perimeter');
}
}
class Programmer extends Worker {
createDatabase(): void {/../}
}
class Seller extends Worker {
sale(): void {/../}
}
class Designer extends Worker {
access(): void {
throwError('No access');
}
}
In this example, we have an issue with the Contractor
class. Designer
, Programmer
, and Seller
are all Workers, and they inherited from the parent class Worker
. But at the same time, Designers do not have access to closed perimeter because they are Contractors, not Employees. And we have overridden the access
method and broke the Liskov Substitution Principle.
This principle tells us that if we replace the superclass Worker
with its subclass, for example Designer
class, the functionality should not be broken. But if we do it, the functionality of the Programmer
class will be broken - the access
method will have unexpected realizations from Designer
class.
// Better
class Worker {
work(): void {/../}
}
class Employee extends Worker {
access(): void {
console.log('Have an access to closed perimeter');
}
}
class Contractor extends Worker {
addNewContract(): void {/../}
}
class Programmer extends Employee {
createDatabase(): void {/../}
}
class Saler extends Employee {
sale(): void {/../}
}
class Designer extends Contractor {
makeDesign(): void {/../}
}
We created new layers of abstractions Employee
and Contractor
and moved access
method to the Employee
class and defined specific realization. If we replace Worker
class with subclass Contractor
, the Worker
functionality will not be broken.