Parties précédentes :
Une classe ne devrait avoir qu’une seule raison de changer
// 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(); // ... } }
Dans cet exemple, notre classe effectue des actions dans différentes directions : elle définit le contexte, le modifie et le valide.
// 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()) { // ... } } }
Les entités logicielles (classes, modules, fonctions) doivent être ouvertes à l'extension mais fermées à la 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 }
Dans cet exemple, le problème réside dans la classe HttpRequestCost
, qui, dans la méthode getDeliveryCost
, contient des conditions pour le calcul de différents types de produits, et nous utilisons des méthodes distinctes pour chaque type de produit. Donc, si nous devons ajouter un nouveau type de produit, nous devons modifier la classe HttpRequestCost
, et ce n'est pas sûr ; nous pourrions obtenir des résultats inattendus.
Pour l'éviter, nous devons créer une request
de méthode abstraite dans la classe Product
sans réalisations. La réalisation particulière aura hérité des classes : Ananas et Banane. Ils réaliseront eux-mêmes la demande.
HttpRequestCost
prendra le paramètre product
suivant l'interface de la classe Product
, et lorsque nous transmettrons des dépendances particulières dans HttpRequestCost
, il réalisera déjà la méthode request
pour lui-même.
// 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(...); } }
Les objets d'une superclasse doivent pouvoir être remplacés par des objets de ses sous-classes sans interrompre l'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'); } }
Dans cet exemple, nous avons un problème avec la classe Contractor
. Designer
, Programmer
et Seller
sont tous des Workers et ont hérité de la classe parent Worker
. Mais en même temps, les concepteurs n’ont pas accès à un périmètre fermé car ils sont des entrepreneurs et non des employés. Et nous avons remplacé la méthode access
et brisé le principe de substitution de Liskov.
Ce principe nous dit que si nous remplaçons la superclasse Worker
par sa sous-classe, par exemple la classe Designer
, la fonctionnalité ne doit pas être interrompue. Mais si nous le faisons, la fonctionnalité de la classe Programmer
sera interrompue - la méthode access
aura des réalisations inattendues de la classe 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 {/../} }
Nous avons créé de nouvelles couches d'abstractions Employee
et Contractor
, déplacé la méthode access
vers la classe Employee
et défini une réalisation spécifique. Si nous remplaçons la classe Worker
par la sous-classe Contractor
, la fonctionnalité Worker
ne sera pas interrompue.