O princípio de responsabilidade única é um dos cinco projetos orientados a objetos ( OOD) diretrizes que abrangem os princípios de design SOLID .
Neste tutorial, vamos nos concentrar no princípio da responsabilidade única e demonstrar como ele pode ajudar a orientar suas decisões de design em estruturas JavaScript, especialmente Angular e React .
Aqui está o que vamos cobrir:
- O que são princípios SOLID?
- Qual é o princípio de responsabilidade única?
- O princípio de responsabilidade única no React
- Separando interesses em um componente React
- O princípio de responsabilidade única no Angular
- Responsabilidade única: efeitos colaterais
- Efeitos colaterais no React
- Efeitos colaterais no Angular
- Componentes de contêiner e apresentação
O que são princípios SOLID?
SOLID é um acrônimo que representa os primeiros cinco princípios OOD descritos pelo renomado engenheiro de software Robert C. Martin . Os princípios SOLID são projetados para ajudar os desenvolvedores a projetar aplicativos robustos e sustentáveis.
Os cinco princípios SOLID são:
- Princípio de responsabilidade única
- Princípio aberto-fechado
- Princípio de substituição de Liskov
- Princípio de segregação de interface
- Princípio de inversão de dependência
Qual é o princípio de responsabilidade única?
O princípio de responsabilidade única em JavaScript trata da coesão dos módulos. Ele afirma que funções e classes devem ter apenas uma tarefa.
Pegue, por exemplo, um modelo de Carro
:
class Car { construtor (nome, modelo, ano) { this.name=nome this.model=model this.year=year } getCar (id) { retornar this.http.get ('api/cars/'+ id) } saveCar () { return this.post ('api/cars', {name: this.name, year: this.year, model: this.model}) } }
O exemplo acima viola o princípio da responsabilidade única. Por quê? O modelo Car
foi criado para conter/representar um carro, mas tem um método getCar
que busca um carro na Internet. Isso dá a ele outra responsabilidade de obter carros de um ponto final.
Uma linha deve ser desenhada sob a responsabilidade da classe Car
: será usada como modelo ou como objeto?
Se tocarmos nos métodos saveCar
ou getCar
para fazer uma alteração, essa alteração pode nos forçar a redesenhar o modelo Car
por adicionar uma propriedade extra ou adicionar outra coisa na classe Car
. Se nos esquecermos de fazer isso, o aplicativo pode falhar de maneiras imprevisíveis.
Podemos separar as responsabilidades para diferentes classes:
class Car { construtor (nome, modelo, ano) { this.name=nome this.model=model this.year=year } } class CarService { getCar (id) { retornar this.http.get ('api/cars/'+ id) } saveCar (carro) { this.http.post ('api/cars', car) } }
Como você pode ver neste exemplo, agora temos as responsabilidades separadas. Agora, o modelo Car
gerencia um carro e o CarService
tem a responsabilidade de obter e salvar os carros de um endpoint.
Se uma turma tiver mais de uma responsabilidade, as responsabilidades serão acopladas. Mudanças em uma responsabilidade podem inibir a capacidade da classe de atender as outras. Esse tipo de acoplamento leva a projetos frágeis que quebram de maneiras inesperadas quando alterados.
Os exemplos abaixo mostram como usar o princípio de responsabilidade única nos componentes React e Angular. Esses exemplos também são aplicáveis em outras estruturas JavaScript, como Vue.js , Svelte , etc.
O princípio de responsabilidade única em React
Digamos que temos o seguinte componente React:
class Movies extends Component { componentDidMount () { store.subscribe (()=> this.forceUpdate ()) } render () { const state=store.getState () const movies=state.movies.map ((filme, índice)=> {{{movie.name}} Ano: {{movie.year}} Bruto: {{movie.gross}}}) Retorna () } }Aplicativo Filmes{filmes}
Este componente tem vários problemas:
- Gerenciamento de estado-O componente se inscreve na loja
- Busca de dados-obtém o estado da loja
- Apresentação da IU-renderiza a lista de filmes
- Lógica de negócios-está ligada à lógica de negócios do aplicativo (a lógica de como obter filmes)
Este componente React não é reutilizável. Se quisermos reutilizar a lista de filmes em outro componente do aplicativo-por exemplo, um componente que exibe filmes de alta bilheteria, filmes por ano, etc.-devemos reescrever o código em cada componente, mesmo que sejam o mesmo.
Este componente será difícil de manter porque contém muitas partes. Haverá alterações significativas se uma parte for alterada. Não pode ser otimizado, produz efeitos colaterais e não podemos memoize de maneira eficaz o componente React para desempenho, pois isso resultaria em dados obsoletos.
Separando interesses em um componente React
Continuando nosso exemplo de componente React acima, precisamos extrair a apresentação da IU do componente Filmes
.
Criaremos outro componente, MoviesList
, para lidar com isso. O componente MoviesList
espera a matriz de filmes de seus adereços:
class MoviesList extends Component { render () { const movies=props.movies.map ((filme, índice)=> {{{movie.name}} Ano: {{movie.year}} Bruto: {{movie.gross}}}) Retorna ({filmes}) } } class Movies extends Component { componentDidMount () { store.subscribe (()=> this.forceUpdate ()) } render () { const state=store.getState () const movies=state.movies return () } }Aplicativo Filmes
Refatoramos o componente Movies
e separamos o código de apresentação da IU dele. Agora ele se preocupa apenas em como assinar a loja, obter os dados dos filmes da loja e passá-los para o componente MoviesList
. Não está mais preocupado em como renderizar os filmes; isso agora é responsabilidade do componente MoviesList
.
O componente MoviesList
é o componente de apresentação. Ele apenas apresenta os filmes dados a ele por meio dos adereços movies
. Não importa de onde os filmes são obtidos, seja da loja, localStorage
ou de um servidor/dados fictícios, etc.
Com eles, podemos reutilizar o componente MoviesList
em qualquer lugar em nosso aplicativo React ou mesmo em outros projetos. Este componente React pode ser compartilhado com a nuvem Bit para permitir que outros usuários ao redor do mundo usem o componente em seus projetos.
O princípio de responsabilidade única no Angular
Os aplicativos angulares são compostos por componentes. Um componente mantém uma única visualização composta de elementos.
Os componentes facilitam a construção de aplicativos complexos a partir de uma única unidade de visão simples. Em vez de mergulhar de cabeça na construção de aplicativos complexos, os componentes permitem que você divida e componha o aplicativo a partir de pequenas unidades.
Por exemplo, digamos que você queira criar um aplicativo de mídia social semelhante ao do Facebook. Você não pode simplesmente criar arquivos HTML e inserir elementos. Você precisaria dividi-lo em pequenas unidades de visualização para organizar seus arquivos HTML em uma estrutura semelhante a esta:
- Página de feed
- página de perfil
- página de registro
- página de login
Cada arquivo será composto de componentes. Por exemplo, a página de feed consistirá em feeds de nossos amigos, comentários, curtidas e compartilhamentos, para citar alguns. Tudo isso precisa ser tratado individualmente.
Se os compormos em componentes, temos um componente FeedList
que pega uma matriz de feeds buscados de uma API e um componente FeedView
que para lidar com a exibição do feeds de dados.
Ao construir um novo aplicativo Angular, comece por:
- Dividindo o aplicativo em componentes separados
- Descreva as responsabilidades de cada componente
- Descreva as entradas e saídas de cada componente-ou seja, sua interface voltada para o público.
A maioria dos componentes que escrevemos violam o princípio de responsabilidade única. Digamos, por exemplo, que temos um aplicativo que lista filmes de um endpoint:
@Component ({ seletor:'filmes', template: `` }) export class MoviesComponent implementa OnInit { this.movies=[] construtor (http: Http privado) {} ngOnInit () { this.http.get ('api/movies/'). subscribe (data=> { this.movies=data.movies }) } delMovie (filme) { //algo de exclusão } }{{movie.name}}
{{movie.year}}
{{movie.producer}}
Este componente é responsável por:
- Buscando os filmes da API
api/movies
- Gerenciando a variedade de filmes
Isso é ruim para os negócios. Por quê? Este componente deve ser responsável por uma tarefa ou outra; não pode ser responsável por ambos.
O objetivo de atribuir a cada componente uma única responsabilidade é torná-lo reutilizável e otimizável. Precisamos refatorar nosso componente de exemplo para empurrar algumas responsabilidades para outros componentes. Outro componente precisa lidar com a matriz de filmes, e a lógica de busca de dados deve ser tratada por uma classe Serviço
.
@Injectable () { fornecido em:'root' } export class MoviesService { construtor (http: Http privado) {} getAllMoives () {...} getMovies (id) {...} saveMovie (filme: Filme) {...} deleteMovie (filme: Filme) {...} } @Componente({ seletor:'filmes', template: `` }) export class MoviesComponent implementa OnInit { this.movies=[] construtor (privado moviesService: MoviesService) {} ngOnInit () { this.moviesService.getAllMovies (). subscribe (data=> { this.movies=data.movies }) } } @Componente({ seletor:'lista de filmes', template: ` ` }) export class MoviesList { @Input () movies=null delMovie (filme) { //algo de exclusão } }{{movie.name}}
{{movie.year}}
{{movie.producer}}
Aqui, separamos as várias preocupações no MoviesComponent
. Agora, MoviesList
lida com a matriz de filmes e o MoviesComponent
agora é seu pai que envia a matriz de filmes para MoviesList
via entrada de filmes. O MoviesComponent
não sabe como a matriz será formatada e renderizada; isso depende do componente MoviesList
. A única responsabilidade de MoviesList
é aceitar uma matriz de filmes por meio de sua entrada de filmes e exibir/gerenciar os filmes.
Digamos que desejamos exibir filmes recentes ou filmes relacionados em uma página de perfil de filme. Podemos reutilizar a lista de filmes sem escrever um novo componente para ela:
@Component ({ template: `` }) export class MovieProfile { filme: Filme=nulo; relatedMovies=null; construtor (privado moviesService: MoviesService) {} }Página de perfil de filme
Nome: {{movie.name}} Ano: {{movie.year}} Produtor: {{movie.producer}}
Descrição do filme
{{movie.description}}Filmes relacionados
Como nosso MoviesComponent
é usado para exibir filmes na página principal de nosso aplicativo, podemos reutilizar a MovieList
na barra lateral para exibir filmes de tendência, filmes de maior classificação , filme de maior bilheteria, melhores filmes de anime, etc. Não importa o que aconteça, o componente MovieList
pode se encaixar perfeitamente. Também podemos adicionar uma propriedade extra à classe Movie
e isso não quebrará nosso código onde usamos o componente MovieList
.
Em seguida, movemos a lógica de busca de dados de filmes para um MoviesService
. Este serviço lida com quaisquer operações CRUD em nossa API de filmes.
@Injectable () { fornecido em:'root' } export class MoviesService { construtor (http: Http privado) {} getAllMovies () {...} getMovies (id) {...} saveMovie (filme: Filme) {...} deleteMovie (filme: Filme) {...} }
O MoviesComponent
injeta o MoviesService
e chama qualquer método necessário. Um benefício da separação de interesses é que podemos otimizar essa classe para evitar o desperdício de renderizações.
A detecção de alterações no Angular começa no componente raiz ou no componente que o aciona. MoviesComponent
renderiza MovieList
; sempre que um CD é executado, o MoviesComponent
é renderizado novamente, seguido pelo MovieList
. Renderizar novamente um componente pode ser um desperdício se as entradas não mudaram.
Pense em MoviesComponent
como um componente inteligente e em MovieList
como um componente burro. Por quê? Porque MoviesComponent
busca os dados a serem renderizados, mas a MovieList
recebe os filmes a serem renderizados. Se não receber nada, não renderiza nada.
Os componentes inteligentes não podem ser otimizados porque têm/causam efeitos colaterais imprevisíveis. Tentar otimizá-los fará com que os dados errados sejam exibidos. Componentes burros podem ser otimizados porque são previsíveis; eles geram o que recebem e seu gráfico é linear. O gráfico de um componente inteligente é como uma curva fractal com inúmeras diferenças de anomalias.
Em outras palavras, os componentes inteligentes são como funções impuras e os componentes burros são funções puras, como redutores em Redux . Podemos otimizar o componente MovieList
adicionando changeDetection
a OnPush
:
@Component ({ seletor:'lista de filmes', template: ``, changeDetection: ChangeDetectionStrategy.OnPush }) export class MoviesList { @Input () movies=null delMovie (filme) { //exclusão de algo } }{{movie.name}}
{{movie.year}}
{{movie.producer}}
MovieList
renderizará novamente apenas quando:
- O botão
Del
é clicado
Verifique. Se o valor anterior de filmes for:
[ { nome:'MK', ano:'Desconhecido' } ]
E o valor atual é:
[ { nome:'MK', ano:'Desconhecido' }, { nome:'AEG', ano:'2019' } ]
O componente precisa ser renderizado novamente para refletir as novas alterações. Quando clicamos no botão Del
, a nova renderização ocorrerá. Aqui, o Angular não começa a renderizar novamente a partir da raiz; ele começa a partir do componente pai do componente MovieList
. Isso ocorre porque estamos removendo um filme da matriz movies, portanto, o componente deve renderizar novamente para refletir a matriz restante. Este componente exclui um filme de sua matriz de filmes, o que pode limitar sua capacidade de reutilização.
O que acontece se um componente pai deseja excluir dois filmes da matriz? Veríamos que tocar em MovieList
para se adaptar à mudança violaria o princípio de responsabilidade única.
Na verdade, não deve excluir um filme de sua matriz. Ele deve emitir um evento que faria com que o componente pai pegasse o evento, excluísse um filme de sua matriz e passasse de volta os valores restantes na matriz para o componente.
@Component ({ seletor:'lista de filmes', template: ``, changeDetection: ChangeDetectionStrategy.OnPush }) export class MoviesList { @Input () movies=null @Output () deleteMovie=new EventEmitter () delMovie (filme) { //algo de exclusão this.deleteMovie.emit (filme) } }{{movie.name}}
{{movie.year}}
{{movie.producer}}
Então, com isso, o componente pai pode emitir dois eventos se quiser excluir dois filmes.
@Component ({ seletor:'filmes', template: `` }) export class MoviesComponent implementa OnInit { this.movies=[] construtor (privado moviesService: MoviesService) {} ngOnInit () { this.moviesService.getAllMovies (). subscribe (data=> { this.movies=data.movies }) } delMovie () { this.movies.splice (this.movies.length, 2) } }
Como você pode ver, os componentes burros são renderizados novamente com base no componente pai e nas interações do usuário, o que é previsível e, portanto, otimizável.
Os componentes inteligentes podem ser otimizados adicionando a estratégia de detecção de mudanças OnPush
:
@Component ({ seletor:'filmes', template: ``, changeDetection: ChangeDetctionStrategy.OnPush }) export class MoviesComponent implementa OnInit { this.movies=[] construtor (privado moviesService: MoviesService) {} ngOnInit () { this.moviesService.getAllMovies (). subscribe (data=> { this.movies=data.movies }) } }
Mas isso leva a efeitos colaterais que podem fazer com que seja disparado várias vezes, tornando a estratégia OnPush
totalmente inútil.
Os componentes burros devem constituir a maior parte do seu aplicativo porque eles são otimizáveis e, portanto, proporcionam alto desempenho. Usar muitos componentes inteligentes pode tornar o aplicativo lento porque eles não são otimizáveis.
Responsabilidade única: efeitos colaterais
Os efeitos colaterais podem ocorrer quando o estado do aplicativo muda a partir de um determinado ponto de referência. Como isso afeta o desempenho?
Digamos que temos estas funções:
let globalState=9 função f1 (i) { return i * 90 } função f2 (i) { return i * globalState } f1 pode ser otimizado para interromper a execução quando a entrada for igual à anterior, mas f2 não pode ser otimizado porque é imprevisível, pois depende da variável globalState. Ele armazenará seu valor anterior, mas o globalState pode ter sido alterado por um fator externo, o que tornará a otimização de f2 difícil. f1 é previsível porque não depende de uma variável externa fora de seu escopo.
Efeitos colaterais no React
Os efeitos colaterais podem levar a dados desatualizados ou dados imprecisos no React. Para evitar isso, o React fornece um useEffect
Hook que podemos usar para realizar nossos efeitos colaterais em seu retorno de chamada.
function SmartComponent () { const [token, setToken]=useState ('') useEffect (()=> { //código de efeitos colaterais aqui... const _token=localStorage.getItem ("token") setToken (token) }) Retorna (Token: {token}) }
Aqui, estamos obtendo dados externos usando localStorage
, o que é um efeito colateral. Isso é feito dentro do gancho useEffect
. A função de retorno de chamada no gancho useEffect
é chamada sempre que o componente é montado/atualizado/desmontado.
Podemos otimizar o gancho useEffect
passando um segundo argumento chamado array de dependência. As variáveis são o que useEffect
verifica em cada atualização para saber se deve pular a execução em um renderizador.
Efeitos colaterais em Angular
Componentes inteligentes, quando otimizados com OnPush
, resultam em imprecisão dos dados.
Pegue nosso MoviesComponent
, por exemplo. Digamos que otimizamos com OnPush
e temos uma entrada que recebe certos dados.
@Component ({ template: ` ... `, changeDetection: ChangeDetectionStartegy.OnPush }) export class MoviesComponent implementa OnInit { @Input () data=9 this.movies=[] construtor (privado moviesService: MoviesService) {} ngOnInit () { this.moviesService.getAllMovies (). subscribe (data=> { this.movies=data.movies }) } atualizar () { this.moviesService.getAllMovies (). subscribe (data=> { this.movies=data.movies }) } }
Este componente causa um efeito colateral ao realizar uma solicitação HTTP . Essa solicitação altera os dados no array movies dentro do componente e precisa renderizar o array movies. Nossos dados têm o valor 9
. Quando este componente é renderizado novamente, talvez clicando em um botão que faz com que o método de atualização seja executado, uma solicitação HTTP ocorrerá para buscar uma nova matriz de filmes da rede e uma ChangeDetection
será executada neste componente. Se os @Input () data
deste componente não mudarem de seu pai, este componente não será renderizado novamente, resultando em uma exibição imprecisa da matriz de filmes. Os filmes anteriores são exibidos, mas novos filmes também são buscados.
Agora você viu os efeitos dos efeitos colaterais. Um componente que causa efeitos colaterais é imprevisível e difícil de otimizar.
Os efeitos colaterais incluem:
- solicitações HTTP
- Mudança de estado global (em Redux)
ngrx
efeitos
ngrx
é uma coleção de extensões reativas para Angular. Como vimos, nossos componentes são baseados em serviços. Os componentes injetam serviços para realizar operações diferentes de solicitações de rede para fornecer estado. Esses serviços também injetam outros serviços para funcionar, o que fará com que nossos componentes tenham responsabilidades diferentes.
Como em nosso MoviesComponent
, ele injetou o MoviesService
para realizar operações CRUD na API de filmes.
Este serviço também injeta a classe de serviço HTTP para ajudá-lo a realizar solicitações de rede. Isso torna nosso MoviesComponents
dependente da classe MoviesService
. Se a classe MoviesService
fizer uma alteração significativa, isso pode afetar nosso MoviesComponent
. Imagine seu aplicativo crescendo para centenas de componentes injetando o serviço; você se pegaria vasculhando cada componente que injeta o serviço para refatorá-los.
Muitos aplicativos baseados em loja incorporam o modelo de efeito colateral alimentado por RxJS . Os efeitos liberam nossos componentes de inúmeras responsabilidades.
Para mostrar um exemplo, vamos fazer com que MoviesComponent
use efeitos e mova os dados dos filmes para Store
:
@Component ({ seletor:'filmes', template: `` }) export class MoviesComponent implementa OnInit { filmes: observáveis =this.store.select (state=> state.movies) construtor (loja particular: Loja) {} ngOnInit () { this.store.dispatch ({type:'Load Movies'}) } }
Não há mais MoviesService
; ele foi delegado à classe MoviesEffects
:
classe MoviesEffects { loadMovies $=this.actions.pipe ( ofType ('Carregar filmes'), switchMap (ação=> this.moviesService.getMovies () .map (res=> ({type:'Load Movies Success', payload: res})) .catch (err=> Observable.of ({type:'Load Movies Failure', payload: err})) ); ) construtor (moviesService privado: MoviesService, ações privadas: Actions) {} }
O serviço MoviesService
não é mais responsabilidade do MoviesComponent
. As alterações em MoviesService
não afetarão MoviesComponent
.
Componentes de contêiner e apresentação
Componentes de contêiner são componentes independentes que podem gerar e renderizar seus próprios dados. Um componente de contêiner está preocupado em como suas operações internas funcionam dentro de seus próprios limites de sandbox.
De acordo com Oren Farhi , um componente de contêiner é inteligente o suficiente para realizar algumas operações e tomar algumas decisões:
- Geralmente é responsável por buscar dados que podem ser exibidos
- Pode ser composto de vários outros componentes
- É “com estado”, o que significa que pode gerenciar um determinado estado
- Ele lida com eventos de componentes internos e operações assíncronas
Os componentes do contêiner também são chamados de componentes inteligentes.
Os componentes de apresentação obtêm seus dados de seus pais. Se eles não obtiverem nenhuma entrada do pai, eles não exibirão dados. Eles são burros porque não podem gerar seus próprios dados; isso depende do pai.
Conclusão
Investigamos profundamente para tornar nossos componentes em React/Angular reutilizáveis. Não se trata apenas de escrever código ou saber codificar, mas de saber codificar bem.
Não comece construindo coisas complexas; componha-os a partir de pequenos componentes. O princípio de responsabilidade única ajuda a garantir que escrevamos código limpo e reutilizável.
A postagem Princípios SOLID: Responsabilidade única em estruturas JavaScript apareceu primeiro no LogRocket Blog .