O Liskov Substitution Principle (LSP) auxilia na redução da fragilidade por meio de hierarquias de classes mais coerentes. Ele afirma: "se S e um subtipo de T, então objetos do tipo T em um programa podem ser substituidos por objetos do tipo S sem alterar nenhuma das propriedades desejaveis daquele programa."
De forma simplificada: subclasses devem ser substituiveis sem quebrar as expectativas do programa.
Exemplo Inicial: AccountGateway
Implementação original com alto acoplamento:
class AccountGateway {
async getAccountById(accountId: string) {
const response = await axios.get(`http://localhost:3000/accounts/${accountId}`);
return response.data;
}
}
Desacoplando com Inversao de Dependência
Criamos uma interface HttpClient:
interface HttpClient {
get(url: string): Promise<any>;
}
class AccountGateway {
constructor(readonly httpClient: HttpClient) {}
async getAccountById(accountId: string) {
const response = await this.httpClient.get(`http://localhost:3000/accounts/${accountId}`);
return response.data;
}
}
Implementação do Adapter
class AxiosAdapter implements HttpClient {
async get(url: string): Promise<any> {
return axios.get(url);
}
}
Tres Regras para Subclasses
1. Precondicoes Não Devem Ser Reforçadas
Precondicoes representam os parametros necessarios para a execução do metodo. Se uma subclasse restringe a entrada além das expectativas da classe pai, a substituição quebra.
Exemplo de violacao:
class ProductionAxiosAdapter implements HttpClient {
async get(url: string): Promise<any> {
if (url.includes("localhost")) throw new Error("This implementation is production only");
return axios.get(url);
}
}
Isso reforca as precondicoes ao rejeitar URLs com localhost, violando o LSP.
Solução: Criar hierarquias independentes como LocalHttpClient e ProductionHttpClient.
2. Poscondicoes Não Devem Ser Enfraquecidas
Poscondicoes (valores de retorno) devem manter as expectativas. O tipo de retorno não deve violar o que o chamador espera.
Cenário problematico:
class FetchAdapter implements HttpClient {
async get(url: string): Promise<any> {
const response = await fetch(url);
return response.json();
}
}
O AccountGateway espera uma propriedade data na resposta. O Axios fornece isso; o Fetch não, quebrando o contrato.
Solução: Abstrair o detalhe de implementação no adapter:
class AccountGateway {
constructor(readonly httpClient: HttpClient) {}
async getAccountById(accountId: string) {
const response = await this.httpClient.get(`http://localhost:3000/accounts/${accountId}`);
return response;
}
}
class AxiosAdapter implements HttpClient {
async get(url: string): Promise<any> {
const response = await axios.get(url);
return response.data;
}
}
3. Invariantes Devem Ser Preservados
Invariantes são estados internos do objeto protegidos pelo encapsulamento. Subclasses devem manter os contratos comportamentais da classe pai.
Violacao classica -- Retangulo vs. Quadrado:
class Rectangle {
private x: number;
private y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
setX(x: number) {
this.x = x;
}
setY(y: number) {
this.y = y;
}
getArea() {
return this.x * this.y;
}
}
class Square extends Rectangle {
setX(x: number) {
this.x = x;
this.y = x;
}
setY(y: number) {
this.x = y;
this.y = y;
}
}
Quando setX() e chamado em Square, ambas as dimensoes mudam -- violando a invariante do Rectangle de que setX() so modifica x.
Solução: Criar hierarquias de classes independentes apesar das relacoes do mundo real. Preservar contratos comportamentais acima de heranca teorica.
Conclusão
O LSP fundamentalmente diz respeito a respeitar as expectativas do programa e permitir substituição sem degradacao. Excecoes podem ser lancadas -- desde que o programa espere e as trate adequadamente.