Принципи SOLID — це набір принципів проектування, які допомагають розробникам створювати чисте, зручне для підтримки та масштабоване програмне забезпечення. Ці принципи особливо важливі в об'єктно-орієнтованому програмуванні та можуть бути ефективно застосовані в TypeScript.
У цій статті ми розглянемо кожен із принципів SOLID і продемонструємо, як їх реалізувати в TypeScript з практичними прикладами.
1. Принцип єдиної відповідальності (SRP)
Клас повинен мати лише одну причину для зміни, тобто він повинен виконувати лише одну відповідальність.
Погана реалізація
class User {
constructor(private name: string, private email: string) {}
saveToDatabase(): void {
console.log(`Saving user ${this.name} to database...`);
}
sendEmail(subject: string, body: string): void {
console.log(`Sending email to ${this.email}: ${subject}`);
}
}
Проблема: Клас User
обробляє як керування даними користувача, так і відправку електронної пошти, що порушує принцип SRP.
Гарна реалізація
Розділімо відповідальності на окремі класи (UserRepository
для операцій з базою даних і EmailService
для відправки електронної пошти).
class User {
constructor(private name: string, private email: string) {}
}
class UserRepository {
saveToDatabase(user: User): void {
console.log(`Saving user ${user.name} to database...`);
}
}
class EmailService {
sendEmail(user: User, subject: string, body: string): void {
console.log(`Sending email to ${user.email}: ${subject}`);
}
}
2. Принцип відкритості/закритості (OCP)
Програмні сутності (класи, модулі, функції) повинні бути відкритими для розширення, але закритими для модифікації.
Погана реалізація
class Discount {
giveDiscount(customerType: string): number {
if (customerType === "regular") {
return 10;
} else if (customerType === "premium") {
return 20;
}
return 0;
}
}
Проблема: Додавання нового типу клієнта вимагає зміни класу Discount
.
Гарна реалізація
Використовуйте інтерфейси та наслідування для розширення функціональності без змін в існуючому коді.
interface Customer {
getDiscount(): number;
}
class RegularCustomer implements Customer {
getDiscount(): number {
return 10;
}
}
class PremiumCustomer implements Customer {
getDiscount(): number {
return 20;
}
}
class Discount {
giveDiscount(customer: Customer): number {
return customer.getDiscount();
}
}
3. Принцип підстановки Ліскова (LSP)
Об'єкти суперкласу повинні бути замінюваними об'єктами підкласу без зміни коректності програми.
Погана реалізація
class Rectangle {
constructor(public width: number, public height: number) {}
setWidth(width: number): void {
this.width = width;
}
setHeight(height: number): void {
this.height = height;
}
area(): number {
return this.width * this.height;
}
}
class Square extends Rectangle {
setWidth(width: number): void {
this.width = width;
this.height = width; // Порушує LSP
}
setHeight(height: number): void {
this.height = height;
this.width = height; // Порушує LSP
}
}
Проблема: Клас Square
змінює поведінку класу Rectangle
, що порушує принцип LSP.
Гарна реалізація
Використовуйте спільний інтерфейс (Shape
), щоб забезпечити замінність.
interface Shape {
area(): number;
}
class Rectangle implements Shape {
constructor(public width: number, public height: number) {}
area(): number {
return this.width * this.height;
}
}
class Square implements Shape {
constructor(public side: number) {}
area(): number {
return this.side * this.side;
}
}
4.
Принцип сегрегації інтерфейсів (ISP)
Клієнти не повинні бути змушені залежати від інтерфейсів, які вони не використовують.
Погана реалізація
interface Worker {
work(): void;
eat(): void;
}
class Engineer implements Worker {
work(): void {
console.log("Engineering work...");
}
eat(): void {
console.log("Eating...");
}
}
class Robot implements Worker {
work(): void {
console.log("Building...");
}
eat(): void {
throw new Error("Robots don't eat!");
}
}
Проблема: Robot
змушений реалізовувати метод eat
, хоча він йому не потрібен.
Гарна реалізація
Розділімо інтерфейс на менші, більш специфічні інтерфейси.
interface Workable {
work(): void;
}
interface Eatable {
eat(): void;
}
class Engineer implements Workable, Eatable {
work(): void {
console.log("Engineering work...");
}
eat(): void {
console.log("Eating...");
}
}
class Robot implements Workable {
work(): void {
console.log("Building...");
}
}
5. Принцип інверсії залежностей (DIP)
Модулі високого рівня не повинні залежати від модулів низького рівня. Обидва повинні залежати від абстракцій.
Погана реалізація
class MySQLDatabase {
save(data: string): void {
console.log(`Saving ${data} to MySQL database...`);
}
}
class App {
private database = new MySQLDatabase();
saveData(data: string): void {
this.database.save(data);
}
}
Проблема: Клас App
тісно пов'язаний з класом MySQLDatabase
.
Гарна реалізація
Використовуйте ін'єкцію залежностей і залежіть від абстракцій (інтерфейсів), а не від конкретних реалізацій.
interface Database {
save(data: string): void;
}
class MySQLDatabase implements Database {
save(data: string): void {
console.log(`Saving ${data} to MySQL database...`);
}
}
class MongoDBDatabase implements Database {
save(data: string): void {
console.log(`Saving ${data} to MongoDB database...`);
}
}
class App {
constructor(private database: Database) {}
saveData(data: string): void {
this.database.save(data);
}
}
const mySQLApp = new App(new MySQLDatabase());
mySQLApp.saveData("User Data");
const mongoDBApp = new App(new MongoDBDatabase());
mongoDBApp.saveData("User Data");
Висновок
Дотримуючись принципів SOLID, ви можете створювати TypeScript додатки, які є:
- Модульними (Принцип єдиної відповідальності).
- Розширюваними (Принцип відкритості/закритості).
- Надійними (Принцип підстановки Ліскова).
- Гнучкими (Принцип сегрегації інтерфейсів).
- Розділеними (Принцип інверсії залежностей).
Ці принципи допомагають писати чистий, зручний для підтримки та масштабований код, що робить ваші додатки легшими для розуміння, розширення та налагодження.
Почніть застосовувати принципи SOLID у своїх TypeScript проектах вже сьогодні!
Перекладено з: Implementing SOLID Principles in TypeScript