SOLID é uma sigla para 5 princípios base que regem o desenvolvimento de software legível, de fácil manutenção e escalável.
Originalmente foi idealizado por Robert C. Martin (também conhecido como Uncle Bob) para softwares com design orientado a objeto, ou seja, são princípios que foram inicialmente estruturados visando aplicação no back-end, porém, independem de tecnologia, e isso faz com que possam ser abstraídos e aplicados no desenvolvimento front end, lembrando que assim como toda abstração essa é apenas minha visão de como exatamente funcionaria a aplicação desses conceitos, não significa que é a única ou que seja perfeita.
S - Single-responsibility Principle:
Princípio da responsabilidade única - um componente deve ter uma única responsabilidade. Isso significa que um componente deve fazer apenas uma coisa e fazê-la bem.
Exemplo:
Vamos supor que você esteja trabalhando em um sistema de gestão de supermercado e que precise listar todos os usuários, pra isso, cria-se um componente que será responsável por essa listagem
function UserList() {
const [users, setUsers] = useState([]);
const fetchUsers = () => {
const { data } = await UserService.getAllUsers();
setUsers(data)
}
useEffect(() => {
fetchUsers();
}, []);
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
Perfeito, agora você possui um componente que faz a listagem dos usuários, porém, ele não possui uma única responsabilidade, observe que esse componente não apenas lista os usuários, mas também faz a requisição para o back-end, isso acopla o código e prejudica o reaproveitamento.
Por exemplo:
Nessa listagem você certamente terá mais de um tipo de usuário e vamos trabalhar do ponto de vista que um Gerente tenha vinculado a ele uma equipe a qual é responsável, logo, na visualização dessa equipe será necessário listar os membros, e seu componente poderia ser reaproveitado, mas não será, porque ele foi feito para listar o retorno de um endpoint específico.
A Forma correta aplicando SOLID, seria:
// Um componente para carregar e exibir uma lista de usuários
function LoadAndDisplayUsers() {
const [users, setUsers] = useState([]);
const fetchUsers = () => {
const { data } = await UserService.getAllUsers();
setUsers(data)
}
useEffect(() => {
fetchUsers();
}, []);
return <UserList users={users} />;
}
// Componente para listar os usuários
function UserList({ users }) {
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
O - Open-closed Principle:
Princípio do aberto fechado - os componentes devem ser construídos de forma que possam ser estendidos (por exemplo, com props ou context) sem a necessidade de modificá-los
Vamos continuar no âmbito do sistema de gestão de supermercado, dessa vez você precisa processar uma venda
interface PaymentMethodRef<T> {
getData: () => PaymentFormData<T>
}
function PaymentCart() {
const [paymentMethod, setPaymentMethod] = useState<number | null>(null);
const bankSlipRef= useRef<PaymentMethodRef<bankSlipRef>>(null);
const cardRef = useRef<PaymentMethodRef<CardData>(null);
const handleSubmit = async () => {
if(paymentMethod == 0) {
// Executa a logica de processamento para boleto.
} else if(paymentMethod == 1) {
// Executa a logica de processamento para cartão de credito e debito.
} else {
// Dispara um erro.
}
}
return (
<PaymentListItems />
<SelectPaymentMethod onSetPaymentMethod={setPaymentMethod}/>
{ paymentMethod == 0 && <BankSlipForm ref={bankSlipRef}/>}
{ paymentMethod == 1 && <CardForm ref={cardRef}/>}
<PaymentCartFooter submit={handleSubmit}/>
)
}
Pronto, temos um carrinho de compras, mas, no momento ele não esta seguindo nem o “S” nem o “O”, isso porque ele possui responsabilidade demais e da forma como esta construída sempre que o supermercado decidir implementar um novo método de pagamento, eu preciso modificar o meu PaymentCart já que a logica de processamento está contida nele e isso gera um aumento na chance de erros futuros em funcionalidades que estavam funcionando corretamente.
A Forma correta aplicando SOLID, seria:
interface PaymentMethodRef {
submit: () => void
}
function PaymentCart() {
const [paymentMethod, setPaymentMethod] = useState<number | null>(null);
const paymentMethodRef = useRef<PaymentMethodRef>(null);
const handleSubmit = async () => {
if(!paymentMethodRef) // Dispara um erro.
await paymentMethodRef.current?.submit();
}
return (
<PaymentListItems />
<SelectPaymentMethod onSetPaymentMethod={setPaymentMethod}/>
{ paymentMethod == 0 && <BanckSlipForm ref={paymentMethodRef}/>}
{ paymentMethod == 1 && <CardForm ref={paymentMethodRef}/>}
<PaymentCartFooter submit={handleSubmit}/>
)
}
Veja que agora toda a responsabilidade de processamento esta contida no método de pagamento selecionado, então independente de quais métodos sejam adicionados, contando que implementem a interface PaymentMethodRef serão encaixados e funcionarão sem que seja necessário modificar o funcionamento do nosso Componente.
L - Liskov Substitution Principle:
Princípio da substituição de Liskov - se tivermos um componente pai e um componente filho, o componente filho deve ser capaz de substituir o componente pai sem quebrar a aplicação.
Eu particularmente interpreto esse princípio aplicado no front como a implementação de componentes puros.
Por exemplo: vamos supor que você precise criar um botão que possua uma estilização padrão para aquele projeto e que tenha um loading
//Interface que define as propriedades e sueus tipos.
interface ButtonProps {
children: React.ReactNode;
className?: string;
isLoading?: boolean;
onClick?: () => void;
}
//Botão personalizado
export const BaseButton = ({
children,
className,
isLoading,
onClick
}: ButtonProps) => {
return (
<button
className={twMerge(`Estilização personalizada`, className)}
onClick={onClick}
>
{isLoading ? (
<BiLoaderAlt className="animate-spin h-5 w-5 text-white" />
) : (
children
)}
</button>
);
};
Perfeito, aqui nós temos um botão que estende o padrão e que se encaixaria na maioria das situações, porém, ele é incompleto, porque por mais que estenda um , ele não implementa todas as suas funcionalidades e por isso, nunca seria capaz de substituí-lo.
por exemplo, se ao executar uma promise eu não queira apenas deixar o botão em loading, mas também deseje bloqueá-lo para que não haja double request?
nesse caso, seria necessário voltar ao botão e realizar uma modificação, e isso se repetiria sempre que houvesse a necessidade do BaseButton *******************************************************realizar uma tarefa do seu componente pai, mesmo que ele apenas o estenda.*******************************************************
Ok, mas então qual seria a forma correta?
// Agora nosso componente estende todas as propriedades padrões do button
interface ButtonProps extends ComponentProps<"button"> {
children?: React.ReactNode;
isLoading?: boolean;
}
//Nosso botão passou a possuir todas as propriedades do button, mas nem todas
//serão exploradas pelo nosso componente, por isso separamos as que vamos explorar,
//e mantemos o restante no ...rest
export const BaseButton = ({
children,
className,
isLoading,
...rest
}: ButtonProps) => {
return (
<button
className={twMerge(`Estilização personalizada`, className)}
{...rest}
>
{isLoading ? (
<BiLoaderAlt className="animate-spin h-5 w-5 text-white" />
) : (
children
)}
</button>
);
};
Dessa forma nosso BaseButton passa a ser um componente puro, que estende por completo um
I - Interface Segregation Principle
Princípio da segregação de interfaces - as propriedades dos componentes devem ser específicas para sua funcionalidade. Não devemos ter propriedades que não são usadas pelo componente ou que são implementadas apenas para atender as exigências de uma interface
Pode parecer um pouco estranho quando a ideia de um componente front end ser forçado a implementar alguma coisa por causa de uma interface, mas isso pode acontecer quando se tem um componente Ref como o apresentado na letra “O” isso acontece porque o componente é obrigado a seguir as regras de contrato definidos na tipagem de sua Ref.
Por exemplo: nesse formulário step, cada step referenciado precisa ter a implementação do getData
interface FormStepRef<T> {
getData: () => T
}
function RegisterUserFormStep() {
const [activeStep, setActiveStep] = useState<number>(0)
const formStepRef = useRef<FormStepRef>(null);
//Estados para armazenar os valores de cada step
const stepControl = {
0: (setpData) => // Armazena a informação no estado da step
1: (setpData) => // --,
2: (setpData) => // --,
}
const nextStep = async () => {
const data = formStepRef.current?.getData()
stepControl[activeStep](data)
if(activeStep < 2) setActiveStep(activeStep + 1)
}
const prevStep = async () => {
const data = formStepRef.current?.getData()
stepControl[activeStep](data)
if(activeStep > 0) setActiveStep(activeStep - 1)
}
return (
{ activeStep == 0 && <PersonalStep ref={formStepRef} setStepInfo={} stepInfo={} />}
{ activeStep == 1 && <AddressStep ref={formStepRef} setStepInfo={} stepInfo={}/>}
{ activeStep == 2 && <ConfirmStep ref={formStepRef} setStepInfo={} stepInfo={}/>}
<FormStepFooter prev={prevStep} next={nextStep}/>
)
}
Aqui nós temos uma situação interessante, porque caso você analise e chegue a conclusão que o deva ter a responsabilidade de realizar o submit, então nosso está infringindo o “I” porque se a função dele é exibir as informações coletadas anteriormente, ele não precisa me retornar nenhum “data”, mas da forma como está, será forçado a implementar um getData para atender as especificações da Ref
Já se chegar a conclusão que o que deve realizar o submit, então, você precisa criar uma nova Ref específica para executar somente o submit, pois se modificar a
interface FormStepRef<T> {
getData: () => T;
submit: () => void;
}
você cria a consequência de que todas as steps que usarem FormStepRef como referencia deverão implementar um método submit
D - Dependency Inversion Principle
Princípio da inversão de dependência - os componentes devem depender de abstrações, não de implementações concretas. Isso é geralmente alcançado no React usando props ou context para passar dados e funções para os componentes.
Um ótimo exemplo de inversão de dependência é o que fizemos com o quando estávamos aprendendo sobre a implementação do “S”, isso porque nessa ocasião nós tínhamos um componente que inicialmente possuía uma forte dependência do endpoint que requisitava os usuários, impossibilitando o seu reuso, abstrair a camada de listagem da camada de requisição resultou em uma inversão de dependência, permitindo que nossa lista pudesse ser aplicada em qualquer tela que precisasse listar usuários.
Mas só pra reforçar, vamos analisar uma situação onde seria necessário visualizar as informações de um usuário:
// Componente com dependencia injetada
function UserProfile({ user }) {
return <div>{user.name}</div>;
}
//Perfil do Gerente
import UserContext from '../context/UserContext';
function Profile({ user }) {
const user = useContext(UserContext);
return <UserProfile user={user} />
}
//Perfil do funcionario
function EmployeeProfile() {
const { uuid } = useRouter().query;
const [user, setUser] = useState([]);
const fetchEmployee = async (uuid: string) => {
const { data } = await UserService.getEmployee(uuid);
setUser(data)
}
useEffect(() => {
if(uuid) fetchEmployee(uuid);
}, [uuid]);
return <UserProfile user={user} />
}
Espero ter conseguido explicar cada ponto de forma satisfatória, estou aberto a críticas construtivas caso alguém tenha uma interpretação diferente da minha sobre a implementação no front dos princípios SOLID ou da forma como decidi abordar a explicação desse conceito.
Obrigado pela atenção 😊
Top comments (0)