paint-brush
Чистый код: единая ответственность, открытость/закрытость, замена Лискова Принципы SOLID в TS [Часть 4]к@alenaananich
4,774 чтения
4,774 чтения

Чистый код: единая ответственность, открытость/закрытость, замена Лискова Принципы SOLID в TS [Часть 4]

к Alena Ananich7m2023/11/09
Read on Terminal Reader

Слишком долго; Читать

В этой статье мы рассмотрим три первых принципа SOLID: принцип единой ответственности, принцип открытости/закрытости и принцип замены Лискова на практических примерах.
featured image - Чистый код: единая ответственность, открытость/закрытость, замена Лискова Принципы SOLID в TS [Часть 4]
Alena Ananich HackerNoon profile picture


Предыдущие части:


Мы продолжаем рассматривать подходы к тому, чтобы сделать наш код чистым, гибким и удобным в сопровождении. А теперь давайте начнем исследовать чистые архитектурные решения.


Принципы SOLID были представлены Робертом К. Мартином и широко считаются лучшими практиками в объектно-ориентированном программировании и разработке программного обеспечения.


В этой статье мы рассмотрим три первых принципа SOLID: принцип единой ответственности, принцип открытости/закрытости и принцип замены Лискова на практических примерах.

Оглавление:

  1. Принцип единой ответственности
  2. Принцип открытости/закрытости
  3. Принцип замены Лискова

1. Принцип единой ответственности (SRP)

У класса должна быть только одна причина для изменения


Другими словами, у класса должна быть одна обязанность или работа. Это важно, поскольку класс с единственной ответственностью легче понять, изменить и поддерживать. Изменения в одной области кода не будут распространяться на всю систему, что снижает риск появления ошибок.


Давайте рассмотрим практический пример и способы соблюдения принципа единой ответственности:

 // 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(); // ... } }

В этом примере наш класс выполняет действия в разных направлениях: устанавливает контекст, изменяет его и проверяет его.


Чтобы следовать SRP, нам необходимо разделить эти разные обязанности.

 // 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()) { // ... } } }


Почему это важно?

SRP направлен на повышение модульности кода, сводя к минимуму проблемы, возникающие из-за взаимозависимостей. Организуя код в функции, отдельные классы и продвигая модульность, он становится более пригодным для повторного использования, что экономит время, которое в противном случае могло бы быть потрачено на перекодирование существующей функциональности.

2. Принцип открытости/закрытости (OCP)

Программные объекты (классы, модули, функции) должны быть открыты для расширения, но закрыты для модификации.


У вас должна быть возможность добавлять новые функции без изменения существующего кода. Это важно, поскольку, придерживаясь этого принципа, вы можете внедрять в свою систему новые функции или компоненты, не рискуя стабильностью существующего кода.


Это способствует повторному использованию кода и снижает потребность в значительных изменениях при расширении функциональности.

 // 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 }

В данном примере проблема в классе HttpRequestCost , который в методе getDeliveryCost содержит условия для расчета разных типов товаров, а для каждого типа товаров мы используем отдельные методы. Итак, если нам нужно добавить новый тип продукта, нам следует изменить класс HttpRequestCost , а это небезопасно; мы могли бы получить неожиданные результаты.


Чтобы этого избежать, нам следует создать request абстрактного метода в классе Product без реализаций. Конкретная реализация унаследует классы: Ананы и Бананы. Они реализуют запрос для себя.


HttpRequestCost примет параметр product , следующий за интерфейсом класса Product , и когда мы передадим определенные зависимости в HttpRequestCost , он уже реализует метод request для себя.

 // 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(...); } }


Почему это важно?

Следуя этому принципу, вы уменьшите связанность кода и убережете систему от непредсказуемого поведения.

3. Принцип замены Лискова (LSP).

Объекты суперкласса должны быть заменены объектами его подклассов без нарушения работы приложения.


Чтобы понять этот принцип, давайте посмотрим на этот пример:

 // 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'); } }

В этом примере у нас проблема с классом Contractor . Designer , Programmer и Seller — все Workers, и они унаследованы от родительского класса Worker . Но при этом Проектировщики не имеют доступа к закрытому периметру, поскольку они Подрядчики, а не Сотрудники. И мы переопределили метод access и нарушили принцип подстановки Лискова.


Этот принцип говорит нам, что если мы заменим суперкласс Worker его подклассом, например классом Designer , функциональность не должна быть нарушена. Но если мы это сделаем, функциональность класса Programmer будет нарушена — метод access будет иметь неожиданные реализации из класса Designer .


Следуя принципу подстановки Лискова, нам не следует переписывать реализации в подклассах, а необходимо создавать новые уровни абстракции, где мы определяем конкретные реализации для каждого типа абстракции.


Давайте поправим:

 // 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 {/../} }

Мы создали новые уровни абстракций Employee » и Contractor , переместили метод access в класс Employee и определили конкретную реализацию. Если мы заменим класс Worker подклассом Contractor , функциональность Worker не будет нарушена.

Почему это важно?

Если вы придерживаетесь LSP, вы можете заменить любой экземпляр подкласса там, где ожидается экземпляр базового класса, и программа все равно должна работать по назначению. Это способствует повторному использованию кода, модульности и делает ваш код более устойчивым к изменениям.


В следующей статье мы рассмотрим принципы разделения интерфейсов и инверсии зависимостей SOLID.