A versão 18+ do Angular trouxe uma série de melhorias significativas, como a introdução do modo standalone e um suporte aprimorado à injeção de dependência, facilitando a aplicação dos princípios SOLID de forma mais eficiente e intuitiva.
Para ajudar você a colocar esses conceitos em prática, aqui está um guia com exemplos práticos, mostrando tanto as abordagens incorretas quanto as corretas para cada um dos princípios do SOLID.
1. SRP – Princípio da Responsabilidade Única
“Uma classe deve ter apenas uma razão para mudar e focar exclusivamente em uma única responsabilidade.”
Em outras palavras, cada componente ou classe deve resolver um único problema, evitando a mistura de tarefas não relacionadas. Isso garante código mais coeso, fácil de manter e menos propenso a bugs. Por exemplo, não deve misturar lógica de negócios com chamadas HTTP ou outras tarefas que não sejam diretamente relacionadas à sua função principal.
Exemplo Errado: Componente fazendo tudo
import { Component, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
@Component({
selector: 'app-user-profile',
standalone: true,
template: `
<div *ngIf="user">
<h1>{{ user.name }}</h1>
<p>Email: {{ user.email }}</p>
</div>
`,
})
export class UserProfileComponent {
user: any;
private http = inject(HttpClient);
constructor() {
this.loadUser();
}
loadUser() {
this.http.get('/api/user').subscribe((data) => (this.user = data));
}
}
Problemas Identificados:
Dificuldade em testes:
A dependência direta do HttpClient
torna os testes complexos e frágeis, já que é impossível testar a UI isoladamente sem mockar serviços ou APIs externas.
Violação do SRP (Responsabilidade Única):
O componente mistura responsabilidades distintas, como renderização da interface (UI) e gestão da lógica de dados, contrariando o princípio SOLID.
Acoplamento Excessivo:
Qualquer alteração na forma de obtenção de dados (ex: substituir HttpClient
por outra fonte) exigirá modificações diretas no componente, gerando rigidez e dificuldade de manutenção.
Exemplo Correto: Separando Responsabilidades
1 – Criamos um serviço para buscar os dados
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class UserService {
private http = inject(HttpClient);
getUser(): Observable<User> {
return this.http.get<User>('/api/user');
}
}
Agora, o serviço é responsável apenas por obter os dados.
2 – O componente apenas consome o serviço
import { Component, inject } from '@angular/core';
import { AsyncPipe, NgIf } from '@angular/common';
import { UserService } from '../services/user.service';
@Component({
selector: 'app-user-profile',
standalone: true,
imports: [AsyncPipe, NgIf],
template: `
<div *ngIf="user$ | async as user">
<h1>{{ user.name }}</h1>
<p>Email: {{ user.email }}</p>
</div>
`,
})
export class UserProfileComponent {
private userService = inject(UserService);
user$ = this.userService.getUser();
}
Vantagens:
Componente mais limpo e focado:
O componente agora se dedica exclusivamente à renderização da interface (UI), seguindo o princípio da responsabilidade única e tornando o código mais organizado e fácil de manter.
Reutilização do Service:
A lógica de negócios e acesso a dados foi encapsulada em um Service, que pode ser reutilizado em diferentes partes da aplicação, promovendo a modularidade e evitando duplicação de código.
Melhor testabilidade:
Com a separação das responsabilidades, o componente se torna mais fácil de testar, já que o UserService
pode ser mockado durante os testes, permitindo a validação isolada da UI sem dependências externas.
2. OCP – Princípio Aberto/Fechado
“Entidades de software devem ser abertas para extensão, mas fechadas para modificação.”
Isso significa que você pode adicionar novos comportamentos sem alterar o código existente. Por exemplo, em vez de modificar uma classe diretamente para adicionar funcionalidades, utilize herança, interfaces ou injeção de dependência para estender seu comportamento.
Exemplo Errado: Modificando um serviço existente
@Injectable({ providedIn: 'root' })
export class LoggerService {
log(message: string) {
console.log(`LOG: ${message}`);
}
}
// Depois precisamos adicionar logs para arquivos, então modificamos o serviço:
@Injectable({ providedIn: 'root' })
export class LoggerService {
log(message: string) {
console.log(`LOG: ${message}`);
// Nova funcionalidade, agora salvamos em um arquivo
this.saveToFile(message);
}
private saveToFile(message: string) {
// Código para salvar em arquivo
}
}
Problema:
• Se adicionarmos novas formas de log (ex: banco de dados), teremos que modificar o código original, o que pode quebrar outras partes do sistema.
Exemplo Correto: Uso de Herança ou Injeção de Dependência
export abstract class Logger {
abstract log(message: string): void;
}
@Injectable({ providedIn: 'root' })
export class ConsoleLogger extends Logger {
log(message: string) {
console.log(`LOG: ${message}`);
}
}
@Injectable({ providedIn: 'root' })
export class FileLogger extends Logger {
log(message: string) {
// Código para salvar em arquivo
}
}
Agora, podemos adicionar novos loggers sem modificar a implementação existente!
3. LSP – Princípio da Substituição de Liskov
“Subclasses devem ser substituíveis por suas superclasses sem quebrar o comportamento esperado do programa.”
Em outras palavras, qualquer instância de uma classe base deve poder ser substituída por uma instância de sua subclasse sem causar erros ou comportamentos inesperados.
Exemplo Errado: Subclasse alterando comportamento
class Rectangle {
constructor(public width: number, public height: number) {}
setWidth(w: number) { this.width = w; }
setHeight(h: number) { this.height = h; }
}
class Square extends Rectangle {
setWidth(w: number) {
this.width = w;
this.height = w; // ⚠ Quebra o comportamento esperado!
}
}
Problema:
• Square quebra a expectativa de que largura e altura podem ser modificadas separadamente.
Exemplo Correto: Usando Interfaces
interface Shape {
area(): number;
}
class Rectangle implements Shape {
constructor(public width: number, public height: number) {}
area(): number {
return this.width * this.height;
}
}
class Square implements Shape {
constructor(public side: number) {}
area(): number {
return this.side * this.side;
}
}
Agora, ambas as classes seguem a mesma interface sem quebrar comportamentos.
4. ISP – Princípio da Segregação de Interfaces
“Classes não devem ser forçadas a implementar métodos que não utilizam.”
Esse princípio sugere que interfaces devem ser específicas e focadas em um único propósito, evitando a criação de “superinterfaces” que agregam métodos desnecessários.
Exemplo Errado: Interface genérica demais
interface UserActions {
login(): void;
logout(): void;
register(): void;
resetPassword(): void;
}
class GuestUser implements UserActions {
login(): void { /* OK */ }
logout(): void { /* OK */ }
register(): void { /* OK */ }
resetPassword(): void { /* Mas um Guest não pode resetar senha! */ }
}
Problema:
• GuestUser é forçado a implementar métodos desnecessários.
Exemplo Correto: Interfaces mais específicas
interface Login {
login(): void;
logout(): void;
}
interface Register {
register(): void;
}
interface ResetPassword {
resetPassword(): void;
}
class GuestUser implements Login, Register {
login(): void { /* OK */ }
logout(): void { /* OK */ }
register(): void { /* OK */ }
}
5. DIP – Princípio da Inversão de Dependência
“Módulos de alto nível não devem depender de módulos de baixo nível. Ambos devem depender de abstrações.”
Em outras palavras, dependa de abstrações (interfaces ou classes abstratas), e não de implementações concretas. Isso promove um sistema mais flexível, desacoplado e fácil de manter.
Exemplo Errado: Componente dependendo diretamente de um serviço concreto
@Component({ /*...*/ })
export class MyComponent {
constructor(private apiService: ApiService) {} // ⚠ Fortemente acoplado!
}
Exemplo Correto: Injetando uma abstração
export abstract class DataService {
abstract getData(): Observable<string[]>;
}
@Injectable({ providedIn: 'root' })
class ApiService extends DataService {
getData(): Observable<string[]> {
return this.http.get<string[]>('/api/data');
}
}
@Component({ /*...*/ })
export class MyComponent {
constructor(private dataService: DataService) {}
}
Agora, podemos trocar ApiService sem modificar MyComponent.
Entender e aplicar SOLID sempre será uma boa prática em qualquer situação ou linguagem =D
Obrigada pela leitura <3
8