Partes Anteriores:
Uma classe deve ter apenas um motivo para mudar
// 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(); // ... } }
Neste exemplo, nossa classe realiza ações em diferentes direções: configura o contexto, altera-o e valida-o.
// 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()) { // ... } } }
Entidades de software (classes, módulos, funções) devem estar abertas para extensão, mas fechadas para modificação
// 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 }
Neste exemplo, o problema está na classe HttpRequestCost
, que, no método getDeliveryCost
contém condições para o cálculo de diferentes tipos de produtos, e utilizamos métodos separados para cada tipo de produto. Portanto, se precisarmos adicionar um novo tipo de produto, devemos modificar a classe HttpRequestCost
, e isso não é seguro; poderíamos obter resultados inesperados.
Para evitá-lo, devemos criar uma request
de método abstrato na classe Product
sem realizações. A realização particular terá herdado as classes: Ananas e Banana. Eles realizarão o pedido por si mesmos.
HttpRequestCost
usará o parâmetro product
seguindo a interface da classe Product
, e quando passarmos dependências específicas em HttpRequestCost
, ele já realizará o método request
para si mesmo.
// 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(...); } }
Os objetos de uma superclasse devem ser substituídos por objetos de suas subclasses sem interromper a aplicação.
// 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'); } }
Neste exemplo, temos um problema com a classe Contractor
. Designer
, Programmer
e Seller
são todos Workers e herdaram da classe pai Worker
. Mas, ao mesmo tempo, os Projetistas não têm acesso ao perímetro fechado porque são Contratantes e não Funcionários. E substituímos o método access
e quebramos o Princípio da Substituição de Liskov.
Este princípio nos diz que se substituirmos a superclasse Worker
por sua subclasse, por exemplo a classe Designer
, a funcionalidade não deverá ser quebrada. Mas se fizermos isso, a funcionalidade da classe Programmer
será quebrada - o método access
terá realizações inesperadas da 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 {/../} }
Criamos novas camadas de abstrações Employee
e Contractor
e movemos o método access
para a classe Employee
e definimos realização específica. Se substituirmos a classe Worker
pela subclasse Contractor
, a funcionalidade Worker
não será quebrada.