Предыдущие части:
У класса должна быть только одна причина для изменения
// 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(); // ... } }
В этом примере наш класс выполняет действия в разных направлениях: устанавливает контекст, изменяет его и проверяет его.
// 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()) { // ... } } }
Программные объекты (классы, модули, функции) должны быть открыты для расширения, но закрыты для модификации.
// 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(...); } }
Объекты суперкласса должны быть заменены объектами его подклассов без нарушения работы приложения.
// 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
не будет нарушена.