Você iniciou aquele MVP e a complexidade do projeto começou a te assustar? A manutenção dos componentes se tornou um emaranhado de 'if's ternários' para todo lado? Fica tranquilo que o SOLID pode te ajudar a diminuir a complexidade do seu projeto e transformar o seu código!
O Que é SOLID?
SOLID é o acrônimo para cinco princípios de design de software que visam tornar o código mais compreensível, flexível e de fácil manutenção. Embora seja amplamente utilizado no desenvolvimento backend e em linguagens como Java, C# ou outras que seguem o paradigma de Programação Orientada a Objetos (POO), sua aplicação é igualmente vital para o frontend moderno, especialmente com frameworks como React, Angular e Vue, onde modularidade e reusabilidade são cruciais.
No nosso exemplo de hoje, vamos explorar como o React e o TypeScript podem te ajudar a desmistificar e aplicar os princípios SOLID na prática. Vamos lá!
Single Responsibility Principle (SRP): Princípio da Reponsabilidade Única
Como o nome já deixa explícito, é o princípio em que uma única função, classe ou, no nosso exemplo, componente, deve ter uma única responsabilidade.
// Exemplo Ruim ❌
// Este componente possui duas responsabilidades: exibir o perfil E um formulário de edição.
<>
<UserProfileDisplayAndForm />
</>
Esse componente está com mais de uma responsabilidade, e isso faz com que a complexidade aumente exponencialmente caso venha a se ter mais “features” no futuro que envolva a parte do perfil do usuário.
Para deixar cada qual com sua responsabilidade, precisamos segregar cada responsabilidade ao seu devido componente.
// Bom exemplo ✅
// Cada componente agora tem uma única responsabilidade.
<>
{/* UserProfileDisplay - Responsável apenas por exibir o perfil do usuário */}
<UserProfileDisplay />
{/* UserForm - Responsável apenas pelo formulário de edição do usuário */}
<UserForm />
</>
Open/Closed Principle (OCP): Princípio aberto e fechado
O princípio aberto/fechado nos diz que devemos estar abertos para extensão e fechados para modificações.
No nosso exemplo, o botão tem muitos motivos para mudar. Se quisermos adicionar um estado diferente, como um estilo, precisamos modificar sua estrutura interna, o que aumentará sua complexidade.
// Exemplo Ruim ❌
export const Button({ type, children }) {
if (type === "primary") { /* renderiza com estilo primary */ }
else if (type === "secondary") { /* renderiza com estilo secondary */ }
// Se precisarmos adicionar um novo tipo de botão (ex: "danger"), teríamos que modificar este componente
// e adicionar outro 'else if', aumentando a complexidade.
// ... retorna um estilo genérico
}
Se modificarmos a estrutura para se adaptar ao conceito aberto/fechado, o código fica assim:
// Bom exemplo ✅
// O componente Button está aberto para extensão (novos tipos de estilo)
// e fechado para modificação (não precisamos alterar sua estrutura interna para novos tipos).
export const Button({ type, children }: { type: string, children: ReactNode | string }) {
return <button className={`bg-${type}-500 text-white`}>{children}</button>
}
// Ou você pode utilizar também styled-components para estender o estilo
// Para um novo tipo, você cria um novo componente estilizado, sem alterar o Button base.
// Exemplo com styled-components:
const PrimaryButton = styled(Button)`
background-color: blue;
`;
const SecondaryButton = styled(Button)`
background-color: green;
`;
Liskov Substitution Principle (LSP): Princípio da Substituição de Liskov
Se S é um subtipo de T, então objetos do tipo T devem poder ser substituídos por objetos do tipo S sem alterar a corretude do programa. Em outras palavras, subclasses (ou componentes que se comportam como 'tipos') devem ser substituíveis por suas classes/componentes base sem quebrar o sistema.
Imagine que temos um componente ListItem
e subtipos ProductListItem
e ArticleListItem
.
Cenário Ruim:
Se ProductListItem
e ArticleListItem
herdam de ListItem
, mas ProductListItem
espera uma prop price
que ArticleListItem
não tem, e o ListItem
base não lida com essa ausência.
// Exemplo Ruim ❌
// Componente base ListItem que (erroneamente) assume a existência de 'price'
function ListItem({ item }) {
// Este componente espera que 'item' SEMPRE tenha uma propriedade 'price'.
// Se um item sem 'price' for passado, o componente pode quebrar ou exibir 'undefined'.
return <div>{item.name} - {item.price}</div>; // Bug se item.price não existe
}
// Subtipo ArticleListItem, que não possui a propriedade 'price' esperada por ListItem
function ArticleListItem({ item }) {
// ArticleListItem está usando ListItem, mas seus itens não possuem 'price'.
// Isso viola o LSP porque ArticleListItem não pode substituir ListItem sem causar um erro.
return <ListItem item={item} />; // Item de artigo não tem preço, vai quebrar!
}
// Subtipo ProductListItem, que possui a propriedade 'price'
function ProductListItem({ item }) {
// Embora ProductListItem funcione com ListItem, a dependência implícita de 'price'
// no ListItem base limita a substituibilidade.
return <ListItem item={item} />; // Este funcionaria, mas o base é problemático.
}
// Uso (problema aqui!)
<ArticleListItem item={myArticle} /> // Isso causaria um erro de runtime
Cenário Bom:
Garantir que o componente base ou a interface definam um comportamento esperado, e os componentes "derivados" (subtipos) o respeitem.
Ou seja, se você tem uma lista que espera renderizar ListItem
, qualquer "tipo" de item que você passar (ProductListItem
ou ArticleListItem
) deve ser renderizado corretamente sem causar erros no componente pai. Isso geralmente é garantido por uma boa tipagem (TypeScript) e tipos bem definidos.
// Bom exemplo ✅
// Interface base que define o contrato mínimo para qualquer item de lista.
// Todos os subtipos devem, no mínimo, ter 'title' e 'author'.
interface ListItemBaseProps {
title: string;
author: string;
// Opcionalmente, pode-se adicionar uma propriedade para o tipo específico do item,
// ou usar um discriminante para unir diferentes tipos.
}
// Componente base ListItem que renderiza informações genéricas.
function ListItem({ item }: { item: ListItemBaseProps }) {
return (
<div>
<h1>{item.title}</h1>
<p>Por {item.author}</p>
</div>
);
}
// Interface para um item de artigo, estendendo a base e adicionando propriedades específicas.
interface ArticleItemProps extends ListItemBaseProps {
id: string;
summary: string;
// NENHUMA propriedade que não seja da base é obrigatória aqui,
// garantindo que ArticleItem possa substituir ListItemBaseProps.
}
// Componente ArticleListItem, que mantém o contrato base e estende o comportamento
function ArticleListItem({ item }: { item: ArticleItemProps }) {
return (
<div>
<h3>{item.title}</h3>
<p>{item.summary}</p>
<small>Por {item.author}</small>
</div>
);
}
// Interface para um item de produto, estendendo a base e adicionando propriedades específicas.
interface ProductItemProps extends ListItemBaseProps {
id: string;
price: number;
currency: string;
// NENHUMA propriedade que não seja da base é obrigatória aqui.
}
// Componente ProductListItem, que mantém o contrato base e estende o comportamento
function ProductListItem({ item }: { item: ProductItemProps }) {
return (
<div>
<h2>{item.title}</h2>
<p>Preço: {item.currency} {item.price.toFixed(2)}</p>
<small>Por {item.author}</small>
</div>
);
}
// No uso real, você poderia ter uma lógica para escolher qual componente renderizar:
function SpecificList({ items }: { items: (ArticleItemProps | ProductItemProps)[] }) {
return (
<div>
{items.map((item, index) => {
if ('summary' in item) {
return <ArticleListItem key={index} item={item as ArticleItemProps} />;
} else if ('price' in item) {
return <ProductListItem key={index} item={item as ProductItemProps} />;
}
return <ListItem key={index} item={item as ListItemBaseProps} />;
})}
</div>
);
}
// Exemplo de como você usaria:
const articles = [
{ title: "SOLID em React", author: "Dev João", id: "1", summary: "Um guia completo..." },
{ title: "Performance com Hooks", author: "Dev Maria", id: "2", summary: "Otimizando seus componentes..." },
];
const products = [
{ title: "Monitor Gamer", author: "Tech XYZ", id: "P1", price: 1200, currency: "BRL" },
{ title: "Teclado Mecânico", author: "ABC Peripherals", id: "P2", price: 450, currency: "BRL" },
];
<MyList items={articles} /> // Funciona, mas renderizaria apenas o título e autor
<MyList items={products} /> // Funciona, mas renderizaria apenas o título e autor
// Usando a SpecificList para demonstrar a substituibilidade e especialização
<SpecificList items={[...articles, ...products]} /> // Agora ambos os tipos funcionam lado a lado
Dessa forma, ArticleListItem
e ProductListItem
(os subtipos) podem ser usados onde ListItem
(o tipo base com ListItemBaseProps
) é esperado, sem quebrar o comportamento, pois todos respeitam o contrato mínimo e adicionam suas próprias especializações de forma segura.
Interface Segregation Principle (ISP): Princípio da Segregação de Interfaces
Esse é um caso que já vi em muitos cenários reais do dia a dia: ter interfaces gigantescas fazendo muita coisa, que poderiam ser segregadas para obtermos uma coerência no componente que estamos criando ou refatorando.
// Exemplo Ruim ❌
// Interface UserProps com muitas propriedades que nem sempre são necessárias para todos os componentes.
interface UserProps {
id: string;
name: string;
email: string;
address: string;
phone: string;
// ... e muito mais propriedades que este componente não usa
}
// O componente UserDetails depende de uma interface UserProps, mas utiliza apenas 'name' e 'email'.
// Isso força o componente a ter uma dependência maior do que o necessário.
export function UserDetails({ user }: { user: UserProps }) {
return (
<div>
<p>Nome: {user.name}</p>
<p>Email: {user.email}</p>
</div>
);
}
// Bom exemplo ✅
// Interface UserSummaryProps, segregada para conter apenas as propriedades essenciais
// para exibir um resumo do usuário.
interface UserSummaryProps {
name: string;
email: string;
}
export function UserDetails({ name, email }: UserSummaryProps) {
return (
<div>
<p>Nome: {name}</p>
<p>Email: {email}</p>
</div>
);
}
// O componente agora está mais limpo e com sua interface mais segregada.
// Ele depende apenas das informações que realmente utiliza.
Dessa forma, nosso componente fica mais robusto a mudanças em dados que ele não utiliza e é mais fácil de estender.
Dependency Inversion Principle (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. Abstrações não devem depender de detalhes; detalhes devem depender de abstrações. Isso quer dizer que você não deve depender de implementações concretas.
Se você se sentiu confuso, respira e vem comigo para um exemplo prático!
Imagina que você tem uma lista de produtos para pegar da API:
// Exemplo Ruim ❌
// ProductList.tsx
import { ApiServiceImpl } from './apiService'; // Dependência concreta diretamente importada
function ProductList() {
const [products, setProducts] = useState([]);
useEffect(() => {
// Se o método de busca de produtos mudar (ex: de REST para GraphQL),
// este componente ProductList precisaria ser modificado.
ApiServiceImpl.getProducts().then(setProducts);
}, []);
// ...
}
Se você precisar mudar a forma como os dados são buscados (ex: mudou de REST para GraphQL, aí a situação complicaria, não é mesmo?), você teria que modificar toda a estrutura do seu ProductList
.
Agora o ProductList vai depender de uma abstração (uma interface de serviço, ou uma função que recebe o método de busca). A implementação concreta é injetada através do componente.
// Bom exemplo ✅
// apiService.ts (abstração - define o contrato do serviço)
interface ProductService {
getProducts(): Promise<Product[]>;
}
// apiServiceImpl.ts (implementação concreta - detalhe de como os produtos são buscados, ex: REST ou GraphQL)
class ApiServiceImpl implements ProductService {
async getProducts() { /* busca REST ou GraphQL */ return [] }
}
// mockServiceImpl.ts (implementação concreta para testes - detalhe de como os produtos são mockados)
class MockServiceImpl implements ProductService {
async getProducts() { return [{ id: 1, name: 'Mock Product' }]; }
}
// ProductList.tsx (depende da abstração ProductService, não da implementação concreta)
function ProductList({ productService }: { productService: ProductService }) {
const [products, setProducts] = useState([]);
useEffect(() => {
productService.getProducts().then(setProducts);
}, [productService]);
// ... restante do componente
}
// Onde você instancia vai ficar dessa maneira:
// O ProductList recebe a implementação concreta que precisa, invertendo a dependência.
<ProductList productService={new ApiServiceImpl()} />
// Ou para testes:
<ProductList productService={new MockServiceImpl()} />
Agora não precisamos saber o detalhe de como os produtos são buscados, apenas que existe um productService
que tem o método getProducts()
. Isso faz com que o código seja muito mais testável e flexível a mudanças.
Por Que SOLID é Importantíssimo para a Sua Carreira?
Agora que você já possui uma ideia do que são os princípios, vamos ao ponto crucial: por que são tão importantes pra VOCÊ programador frontend júnior, e para sua carreira?
Código mais limpo e sustentável:
- Menos Bugs: Componentes com responsabilidades únicas e bem separados são fáceis de testar e menos propensos a introduzir bugs.
- Fácil de entender: Um código SOLID é como um quebra-cabeças com peças bem definidas. Qualquer um consegue entender a função de cada parte. Isso é crucial em equipes.
- Menos Dívida Técnica: Você escreve código que envelhece bem, reduzindo o tempo e o custo de manutenção no futuro.
Acelera o Desenvolvimento (a longo prazo):
- Pode parecer que aplicar SOLID no início leva mais tempo, mas é um investimento. Módulos bem desenhados são fáceis de reutilizar e estender, o que acelera significativamente o desenvolvimento de novas funcionalidades e a adaptação a mudanças de requisitos.
Flexibilidade e Adaptabilidade:
- O mundo frontend muda constantemente. Frameworks, bibliotecas e requisitos evoluem. Código SOLID é mais maleável; ele permite que você troque partes do sistema (ex: uma biblioteca de gerenciamento de estado) ou adicione novas funcionalidades sem reescrever tudo do zero.
Colaboração em Equipe:
- Em equipes grandes, muitos desenvolvedores trabalham no mesmo codebase. Princípios SOLID garantem que o código seja previsível, com responsabilidades claras, minimizando conflitos de merge e facilitando a revisão de código. Você será um colega de equipe mais produtivo.
Testabilidade:
- Um dos maiores benefícios do SOLID é que ele força você a escrever código que é inerentemente mais fácil de testar. Componentes com responsabilidade única e dependências invertidas são ideais para testes unitários e de integração, o que é fundamental para a qualidade do software.
Crescimento Profissional e Reconhecimento:
- Diferencial no Mercado: Desenvolvedores que escrevem código limpo, testável e sustentável são altamente valorizados. Em entrevistas, conseguir discutir e aplicar esses princípios demonstra um nível de maturidade que vai além do básico.
- Melhora no Code Review: Quando você entende SOLID, seus "code reviews" serão mais construtivos e você absorverá melhor os feedbacks.
- Base para Outros Conceitos: SOLID é a base para outros conceitos avançados de arquitetura de software, como Arquitetura Limpa (Clean Architecture) e Domain-Driven Design (DDD). Dominá-lo abrirá portas para funções mais sênior e de liderança técnica.
Resolução de Problemas Complexos:
- Projetos frontend modernos são complexos. Aplicar SOLID te dá um "toolkit mental" para decompor problemas grandes em partes menores e gerenciáveis, facilitando a resolução de desafios.
Como Começar a Aplicar?
- Comece pequeno: não tente refatorar todo o seu projeto de uma vez só. Comece aplicando os princípios em novos componentes um de cada vez. Assim que dominar um princípio, vá para o outro; o importante é o entendimento do assunto e sua aplicabilidade.
- Pratique Testes: A escrita de testes por si só é importantíssima desde o começo, e quando você começa a utilizar o SOLID, percebe que é muito mais fácil testar algo com boas práticas do que um componente que não respeita os princípios (como o da Responsabilidade Única ou a Inversão de Dependência).
- Leia e Discuta: Continue lendo sobre SOLID, assista a vídeos, discuta com pessoas e pratique. Peça feedback com amigos e colegas sobre como melhorar seu código em relação a esses princípios.
Lembre-se: SOLID é agnóstico de linguagem ou framework, ele serve apenas como um guia para nós, desenvolvedores. E outra, não é um conjunto a ser religiosamente seguido, beleza? Pense neles como um conjunto de boas práticas a ser seguido, e com o tempo e prática vão se tornar comum para você. Invista nisso e em sua carreira.
Então é isso, Dev! Se você ficou até aqui, meu muito obrigado e espero que este artigo te ajude de alguma forma!
Top comments (0)