Por que é uma boa idéia que as camadas de aplicação "inferiores" não estejam cientes das camadas "superiores"?

66

Em um aplicativo Web MVC típico (bem projetado), o banco de dados não está ciente do código do modelo, o código do modelo não está ciente do código do controlador e o código do controlador não está ciente do código de visualização. (Eu imagino que você possa começar tanto quanto o hardware, ou talvez ainda mais, e o padrão pode ser o mesmo.)

Indo na outra direção, você pode ir apenas uma camada abaixo. A visualização pode estar ciente do controlador, mas não do modelo; o controlador pode estar ciente do modelo, mas não do banco de dados; o modelo pode estar ciente do banco de dados, mas não do sistema operacional. (Qualquer coisa mais profunda é provavelmente irrelevante.)

Posso entender intuitivamente por que essa é uma boa ideia, mas não consigo articulá-la. Por que esse estilo unidirecional de camadas é uma boa idéia?

Jason Swett
fonte
10
Talvez seja porque os dados surgem do banco de dados para a visualização. Ele "inicia" no banco de dados e "chega" na exibição. A percepção da camada segue na direção oposta à medida que os dados "viajam". Eu gosto de usar "aspas".
21713 Jason Swett
11
Você marcou na sua última frase: Unidirecional. Por que as listas vinculadas são muito mais típicas do que as listas duplamente vinculadas? A manutenção de relacionamentos se torna infinitamente mais simples com uma lista vinculada individualmente. Criamos gráficos de dependência dessa maneira, porque as chamadas recursivas se tornam muito menos prováveis ​​e as características gerais dos gráficos se tornam mais fáceis de raciocinar como um todo. Estruturas razoáveis ​​são inerentemente mais sustentáveis, e as mesmas coisas que afetam os gráficos no nível micro (implementação) o fazem também no nível macro (arquitetura).
Jimmy Hoffa
2
Na verdade, não acho que seja uma boa prática na maioria dos casos que o View esteja ciente do Controlador. Desde Controladores são quase sempre ciente do View, tendo a vista ciente do controlador cria uma referência circular
Amy Blankenship
8
mau tempo de analogia: pela mesma razão, o cara que o leva de costas com um carro enquanto você dirige é o responsável pelo acidente, no caso geral. Ele pode ver o que você está fazendo e deve estar no controle e, se não puder evitá-lo, significa que não respeitou as regras de segurança. Não o contrário. E, encadeando, isso o liberta de se preocupar com o que está acontecendo atrás dele.
haylem
11
Obviamente, uma visão está ciente de um modelo de visão fortemente tipado.
DazManCat

Respostas:

121

Camadas, módulos, e até a própria arquitetura, são meios de tornar os programas de computador mais fáceis de entender pelos seres humanos . O método numericamente ideal para resolver um problema é quase sempre uma bagunça profana e confusa de código não modular, auto-referenciado ou mesmo auto-modificável - seja código montador altamente otimizado em sistemas embarcados com restrições de memória incapacitantes ou sequências de DNA após milhões de anos da pressão de seleção. Tais sistemas não têm camadas, nenhuma direção discernível do fluxo de informações, de fato nenhuma estrutura que possamos discernir. Para todos, exceto seu autor, eles parecem trabalhar com pura magia.

Na engenharia de software, queremos evitar isso. Uma boa arquitetura é uma decisão deliberada de sacrificar alguma eficiência, a fim de tornar o sistema compreensível por pessoas normais. Compreender uma coisa de cada vez é mais fácil do que entender duas coisas que só fazem sentido quando usadas juntas. É por isso que módulos e camadas são uma boa ideia.

Mas, inevitavelmente, os módulos precisam chamar funções uns dos outros e as camadas devem ser criadas umas sobre as outras. Portanto, na prática, é sempre necessário construir sistemas para que algumas partes exijam outras. O compromisso preferido é construí-los de forma que uma parte exija outra, mas essa parte não exija a primeira de volta. E é exatamente isso que as camadas unidirecionais nos dão: é possível entender o esquema do banco de dados sem conhecer as regras de negócios e entender as regras de negócios sem conhecer a interface do usuário. Seria bom ter independência nas duas direções - permitindo que alguém programe uma nova interface do usuário sem saber nadasobre as regras de negócios - mas, na prática, isso quase nunca é possível. Regras práticas como "Sem dependências cíclicas" ou "Dependências devem atingir apenas um nível" simplesmente capturam o limite praticamente alcançável da ideia fundamental de que uma coisa de cada vez é mais fácil de entender do que duas coisas.

Kilian Foth
fonte
11
O que você quer dizer com "tornar o sistema compreensível por pessoas normais "? Eu acho que a expressão encoraja novos programadores a rejeitar seus pontos positivos, porque, como a maioria das pessoas, eles pensam que são mais inteligentes do que a maioria e isso não será um problema para eles. Eu diria que "tornar o sistema compreensível por seres humanos"
Thomas Bonini
12
Essa é uma leitura obrigatória para aqueles que pensam que o desacoplamento completo é o ideal pelo qual se esforçar, mas não conseguem entender por que não funciona.
22713 Robert Harvey
6
Bem, @ Andreas, sempre há Mel .
TRiG 20/05
6
Eu acho que "mais fácil de entender" não é suficiente. É também facilitar a modificação, a extensão e a manutenção do código.
Mike Weller
11
@Peri: essa lei existe, consulte en.wikipedia.org/wiki/Law_of_Demeter . Se você concorda ou não com isso é outra questão.
Mike Chamberlain
61

A motivação fundamental é a seguinte: você deseja remover uma camada inteira e substituí-la por uma completamente diferente (reescrita), e NINGUÉM DEVE (SER CAPAZ DE) ATENÇÃO À DIFERENÇA.

O exemplo mais óbvio é rasgar a camada inferior e substituir uma diferente. Isto é o que você faz quando desenvolve a (s) camada (s) superior (es) contra uma simulação do hardware e depois a substitui no hardware real.

O próximo exemplo é quando você rasga uma camada intermediária e substitui uma camada intermediária diferente. Considere um aplicativo que usa um protocolo executado em RS-232. Um dia, você precisa alterar completamente a codificação do protocolo, porque "algo mais mudou". (Exemplo: alternando da codificação ASCII direta para a codificação Reed-Solomon de fluxos ASCII, porque você estava trabalhando em um link de rádio do centro de LA para Marina Del Rey e agora está trabalhando em um link de rádio do centro de LA para uma sonda orbitando Europa , uma das luas de Júpiter, e esse link precisa de uma correção de erro avançada muito melhor.)

A única maneira de fazer isso funcionar é se cada camada exportar uma interface definida e conhecida para a camada acima e esperar uma interface definida e conhecida para a camada abaixo.

Agora, não é exatamente o caso que as camadas inferiores não sabem NADA sobre as camadas superiores. Em vez disso, o que a camada inferior sabe é que a camada imediatamente acima dela operará precisamente de acordo com sua interface definida. Ele não pode saber mais nada, porque, por definição, qualquer coisa que não esteja na interface definida está sujeita a alterações SEM AVISO.

A camada RS-232 não sabe se está executando ASCII, Reed-Solomon, Unicode (página de códigos em árabe, página de códigos em japonês, página de código Rigellian Beta) ou o quê. Apenas sabe que está obtendo uma sequência de bytes e está gravando esses bytes em uma porta. Na próxima semana, ele pode estar recebendo uma sequência de bytes completamente diferente de algo completamente diferente. Ele não se importa. Ele apenas move bytes.

A primeira (e melhor) explicação do design em camadas é o artigo clássico de Dijkstra "Estrutura do sistema de multiprogramação" . É leitura obrigatória neste negócio.

John R. Strohm
fonte
Isso é útil e obrigado pelo link. Eu gostaria de poder selecionar duas respostas como a melhor. Basicamente, joguei uma moeda na minha cabeça e peguei a outra, mas ainda votei na sua.
Jason Swett
+1 para excelentes exemplos. Eu gosto da explicação dada pelo JRS
ViSu
@ JasonSwett: Se eu tivesse jogado a moeda, eu a teria jogado até que ela designasse essa resposta! ^^ +1 a John.
Olivier Dulac
Não concordo com isso, porque você raramente deseja extrair a camada de regras de negócios e trocá-la por outra. As regras de negócios mudam muito mais lentamente do que as tecnologias de interface do usuário ou de acesso a dados.
21313 Andy
Ding Ding Ding !!! Eu acho que a palavra que você estava procurando é 'dissociação'. É para isso que servem as boas APIs. Definir as interfaces públicas de um módulo para que ele possa ser usado universalmente.
Evan Plaice
8

Porque os níveis mais altos podem mudar.

Quando isso acontece, seja por causa de mudanças nos requisitos, novos usuários, tecnologia diferente, um aplicativo modular (ou seja, em camadas unidirecional) devem exigir menos manutenção e ser mais facilmente adaptados para atender às novas necessidades.


fonte
4

Eu acho que o principal motivo é que isso torna as coisas mais fortemente acopladas. Quanto mais apertado o acoplamento, maior a probabilidade de ocorrer problemas posteriormente. Veja este artigo mais informações: Acoplamento

Aqui está um trecho:

Desvantagens

Sistemas firmemente acoplados tendem a exibir as seguintes características de desenvolvimento, que geralmente são vistas como desvantagens: Uma mudança em um módulo geralmente força um efeito cascata das mudanças em outros módulos. A montagem dos módulos pode exigir mais esforço e / ou tempo devido ao aumento da dependência entre módulos. Um módulo específico pode ser mais difícil de reutilizar e / ou testar porque os módulos dependentes devem ser incluídos.

Com isso dito, o motivo de ter um sistema acoplado ao tigre é por razões de desempenho. O artigo que mencionei também tem algumas informações sobre isso.

barrem23
fonte
4

OMI, é muito simples. Você não pode reutilizar algo que continua referenciando o contexto em que é usado.

Erik Reppen
fonte
4

As camadas não devem ter dependências bidirecionais

As vantagens de uma arquitetura em camadas são que as camadas devem ser usadas independentemente:

  • você poderá criar uma camada de apresentação diferente, além da primeira, sem alterar a camada inferior (por exemplo, criar uma camada de API, além de uma interface da web existente)
  • você poderá refatorar ou, eventualmente, substituir a camada inferior sem alterar a camada superior

Essas condições são basicamente simétricas . Eles explicam por que geralmente é melhor ter apenas uma direção de dependência, mas não qual .

A direção da dependência deve seguir a direção do comando

A razão pela qual preferimos uma estrutura de dependência descendente é porque os objetos superiores criam e usam os objetos inferiores . Uma dependência é basicamente um relacionamento que significa "A depende de B se A não puder funcionar sem B". Portanto, se os objetos em A usam os objetos em B, é assim que as dependências devem ser.

Isso é de certa forma arbitrário. Em outros padrões, como o MVVM, o controle flui facilmente das camadas inferiores. Por exemplo, você pode configurar um rótulo cuja legenda visível esteja vinculada a uma variável e mude com ela. Normalmente, ainda é preferível ter dependências descendentes, porque os objetos principais são sempre aqueles com os quais o usuário interage e esses objetos fazem a maior parte do trabalho.

Enquanto de cima para baixo usamos a invocação de método, de baixo para cima (normalmente) usamos eventos. Os eventos permitem que as dependências sejam descendentes, mesmo quando o controle flui ao contrário. Os objetos da camada superior assinam eventos na camada inferior. A camada inferior não sabe nada sobre a camada superior, que atua como um plug-in.

Há também outras maneiras de manter uma única direção, por exemplo:

  • continuações (passando um lambda ou um método a ser chamado e evento para um método assíncrono)
  • subclassing (crie uma subclasse em A de uma classe pai em B que será injetada na camada inferior, um pouco como um plugin)
Sklivvz
fonte
3

Gostaria de acrescentar meus dois centavos ao que Matt Fenwick e Kilian Foth já explicaram.

Um princípio da arquitetura de software é que programas complexos devem ser construídos compondo blocos menores e independentes (caixas pretas): isso minimiza as dependências e reduz a complexidade. Portanto, essa dependência unidirecional é uma boa idéia, pois facilita a compreensão do software e o gerenciamento da complexidade é uma das questões mais importantes no desenvolvimento de software.

Portanto, em uma arquitetura em camadas, as camadas inferiores são caixas pretas que implementam camadas de abstração nas quais as camadas superiores são construídas. Se uma camada inferior (por exemplo, camada B) pode ver detalhes de uma camada superior A, então B não é mais uma caixa preta: seus detalhes de implementação dependem de alguns detalhes de seu próprio usuário, mas a idéia de uma caixa preta é que O conteúdo (sua implementação) é irrelevante para o usuário!

Giorgio
fonte
3

Apenas por diversão.

Pense em uma pirâmide de líderes de torcida. A linha inferior suporta as linhas acima delas.

Se a líder de torcida daquela linha estiver olhando para baixo, ela será estável e permanecerá equilibrada para que as pessoas acima dela não caiam.

Se ela olhar para cima para ver como estão todos acima dela, ela perderá o equilíbrio, fazendo com que toda a pilha caia.

Não é realmente técnico, mas foi uma analogia que pensei que poderia ajudar.

Bill Leeper
fonte
3

Embora a facilidade de entendimento e, até certo ponto, os componentes substituíveis sejam certamente boas razões, uma razão igualmente importante (e provavelmente a razão pela qual as camadas foram inventadas em primeiro lugar) é do ponto de vista da manutenção do software. A linha inferior é que dependências causam o potencial de quebrar coisas.

Por exemplo, suponha que A dependa de B. Como nada depende de A, os desenvolvedores são livres para alterar A no conteúdo de seus corações, sem ter que se preocupar com a possibilidade de quebrar algo diferente de A. No entanto, se o desenvolvedor quiser alterar B, qualquer alteração em B, o que é feito pode potencialmente quebrar A. Esse era um problema frequente nos primeiros dias do computador (pense no desenvolvimento estruturado), em que os desenvolvedores corrigiam um bug em uma parte do programa e geravam erros em partes aparentemente não relacionadas do programa em outros lugares. Tudo por causa de dependências.

Para continuar com o exemplo, agora suponha que A dependa de B AND B dependa de A. IOW, uma dependência circular. Agora, sempre que uma alteração é feita em qualquer lugar, isso pode potencialmente quebrar o outro módulo. Uma mudança em B ainda pode quebrar A, mas agora uma mudança em A também pode quebrar B.

Portanto, na sua pergunta original, se você estiver em uma equipe pequena para um projeto pequeno, tudo isso é um exagero, porque você pode alterar livremente os módulos à sua vontade. No entanto, se você estiver em um projeto considerável, se todos os módulos dependerem dos outros, sempre que for necessária uma alteração, isso poderá potencialmente danificar os outros módulos. Em um projeto grande, é difícil determinar todos os impactos, portanto é provável que você perca alguns impactos.

Isso piora em um projeto grande, onde existem muitos desenvolvedores (por exemplo, alguns que trabalham apenas na camada A, alguma camada B e outra camada C). Como é provável que cada mudança precise ser revisada / discutida com os membros das outras camadas, para garantir que suas alterações não quebrem ou forcem o retrabalho no que estão trabalhando. Se suas alterações forçam outras pessoas, você deve convencê-las de que elas devem fazer a alteração, porque não vão querer trabalhar mais só porque você tem essa nova e excelente maneira de fazer as coisas em seu módulo. IOW, um pesadelo burocrático.

Mas se você limitar as dependências para A depende de B, B dependerá de C, somente as pessoas da camada C precisarão coordenar suas alterações nas duas equipes. A camada B só precisa coordenar as alterações com a equipe da camada A e a equipe da camada A é livre para fazer o que quiser, porque o código não afeta a camada B ou C. Portanto, idealmente, você projetará suas camadas para que a camada C mude muito pouco, a camada B muda um pouco e a camada A faz a maior parte das alterações.

Dunk
fonte
+1 No meu empregador, na verdade, temos um diagrama interno descrevendo a essência do seu último parágrafo, conforme se aplica ao produto em que trabalho, ou seja, quanto mais você desce da sua pilha, menor a taxa de alteração (e deve ser).
RobV
1

A razão mais básica pela qual as camadas inferiores não devem estar cientes das camadas superiores é que existem muitos outros tipos de camadas superiores. Por exemplo, existem milhares e milhares de programas diferentes no seu sistema Linux, mas eles chamam a mesma mallocfunção de biblioteca C. Portanto, a dependência é desses programas para essa biblioteca.

Observe que "camadas inferiores" são na verdade as camadas intermediárias.

Pense em um aplicativo que se comunica com o mundo exterior por meio de alguns drivers de dispositivo. O sistema operacional está no meio .

O sistema operacional não depende de detalhes nos aplicativos nem nos drivers de dispositivo. Existem muitos tipos de driver de dispositivo do mesmo tipo e eles compartilham a mesma estrutura de driver de dispositivo. Às vezes, os hackers do kernel precisam colocar algum tratamento especial na estrutura em benefício de um hardware ou dispositivo específico (exemplo recente que me deparei: código específico do PL2303 na estrutura serial USB do Linux). Quando isso acontece, eles geralmente colocam comentários sobre o quanto isso é péssimo e deve ser removido. Embora o sistema operacional chame funções nos drivers, as chamadas passam por ganchos que fazem os drivers parecerem iguais, enquanto que quando os drivers chamam o sistema operacional, eles geralmente usam funções específicas diretamente pelo nome.

Portanto, de certa forma, o sistema operacional é realmente uma camada inferior da perspectiva do aplicativo e da perspectiva do aplicativo: um tipo de hub de comunicação onde as coisas se conectam e os dados são alternados para seguir os caminhos apropriados. Ajuda o design do hub de comunicação a exportar um serviço flexível que pode ser usado por qualquer coisa e a não mover nenhum dispositivo ou aplicativo específico para o hub.

Kaz
fonte
Estou feliz, enquanto eu não precisa se preocupar com a criação tensões específicas nos pinos da CPU específicas :)
um CVn
1

A separação de preocupações e as abordagens de divisão / conquista podem ser outra explicação para essas questões. A separação de preocupações oferece a capacidade de portabilidade e, em algumas arquiteturas mais complexas, oferece vantagens independentes de escala e desempenho à plataforma.

Nesse contexto, se você pensar em uma arquitetura de cinco camadas (cliente, apresentação, negócios, integração e nível de recurso), o nível mais baixo da arquitetura não deve estar ciente da lógica e do negócio dos níveis mais altos e vice-versa. Quero dizer por nível inferior como níveis de integração e recursos. As interfaces de integração do banco de dados fornecidas no banco de dados de integração e nos serviços da web reais (provedores de dados de terceiros) pertencem à camada de recursos. Então, suponha que você altere seu banco de dados MySQL para um banco de dados NoSQL, como o MangoDB, em termos de escalabilidade ou qualquer outra coisa.

Nessa abordagem, a camada de negócios não se importa como a camada de integração fornece a conexão / transmissão pelo recurso. Ele procura apenas objetos de acesso a dados fornecidos pela camada de integração. Isso pode ser expandido para mais cenários, mas basicamente, a separação de preocupações pode ser a razão número um para isso.

Mahmut Canga
fonte
1

Expandindo a resposta de Kilian Foth, essa direção de camadas corresponde a uma direção na qual um humano explora um sistema.

Imagine que você é um novo desenvolvedor encarregado de corrigir um bug no sistema em camadas.

Os erros geralmente são uma incompatibilidade entre o que o cliente precisa e o que ele recebe. Como o cliente se comunica com o sistema através da interface do usuário e obtém resultados através da interface do usuário (interface do usuário significa literalmente 'interface do usuário'), os erros também são relatados em termos de interface do usuário. Portanto, como desenvolvedor, você não tem muita escolha, mas também começa a olhar para a interface do usuário, para descobrir o que aconteceu.

É por isso que é necessário ter conexões da camada de cima para baixo. Agora, por que não temos conexões nos dois sentidos?

Bem, você tem três cenários de como esse bug poderia ocorrer.

Pode ocorrer no próprio código da interface do usuário e, portanto, ser localizado lá. Isso é fácil, você só precisa encontrar um lugar e consertá-lo.

Isso pode ocorrer em outras partes do sistema como resultado de chamadas feitas na interface do usuário. O que é moderadamente difícil, você rastreia uma árvore de chamadas, encontra um local onde o erro ocorre e o corrige.

E isso pode ocorrer como resultado de uma chamada no código da interface do usuário. O que é difícil, você precisa atender a chamada, encontrar sua fonte e descobrir onde ocorre o erro. Considerando que um ponto em que você inicia está situado no fundo de uma única ramificação de uma árvore de chamadas, E você precisa encontrar uma árvore de chamadas correta primeiro, pode haver várias chamadas no código da interface do usuário, sua depuração foi cortada para você.

Para eliminar o mais difícil possível, as dependências circulares são fortemente desencorajadas, as camadas se conectam principalmente de cima para baixo. Mesmo quando uma conexão é necessária de outra maneira, ela geralmente é limitada e claramente definida. Por exemplo, mesmo com retornos de chamada, que são uma espécie de conexão reversa, o código chamado em retorno de chamada geralmente fornece esse retorno de chamada, implementando um tipo de "aceitação" para conexões reversas e limitando seu impacto na compreensão de um sistema.

Camadas é uma ferramenta, e principalmente destinada a desenvolvedores que suportam um sistema existente. Bem, as conexões entre as camadas refletem isso também.

Kaerber
fonte
-1

Outro motivo que eu gostaria de ver explicitamente mencionado aqui é a reutilização do código . Já tivemos o exemplo da mídia RS232 que é substituída, então vamos um passo adiante ...

Imagine que você está desenvolvendo drivers. É o seu trabalho e você escreve bastante. Os protocolos provavelmente podem começar a se repetir em algum momento, assim como a mídia física.

Então, o que você começará a fazer - a menos que seja um grande fã de fazer a mesma coisa repetidamente - é escrever camadas reutilizáveis ​​para essas coisas.

Digamos que você precise escrever 5 drivers para dispositivos Modbus. Um deles usa o Modbus TCP, dois usam o Modbus no RS485 e os demais passam pelo RS232. Você não vai reimplementar o Modbus 5 vezes, porque está escrevendo 5 drivers. Além disso, você não vai reimplementar o Modbus 3 vezes, porque você tem 3 camadas físicas diferentes abaixo de você.

O que você faz é escrever um TCP Media Access, um RS485 Media Access e possivelmente um RS232 Media Access. É inteligente saber que haverá uma camada modbus acima, neste momento? Provavelmente não. O próximo driver que você implementará também pode usar Ethernet, mas usar HTTP-REST. Seria uma pena se você tivesse que reimplementar o Ethernet Media Access para se comunicar via HTTP.

Uma camada acima, você implementará o Modbus apenas uma vez. Essa camada Modbus, mais uma vez, não saberá dos drivers, que estão uma camada acima. Esses drivers, é claro, terão que saber que devem falar sobre modbus e que devem usar Ethernet. No entanto, como implementou a maneira como descrevi, você não apenas pode extrair uma camada e substituí-la. você poderia, é claro - e isso para mim é o maior benefício de todos, vá em frente e reutilize a camada Ethernet existente para algo absolutamente não relacionado ao projeto que originalmente causou sua criação.

Isso é algo que provavelmente vemos todos os dias como desenvolvedores e economiza muito tempo. Existem inúmeras bibliotecas para todos os tipos de protocolos e outras coisas. Eles existem devido a princípios como a direção da dependência, seguindo a direção do comando, o que nos permite criar camadas reutilizáveis ​​de software.

juwi
fonte
reutilização já foi explicitamente mencionado em uma resposta postada mais de meio ano atrás
mosquito