Por que é uma boa ideia que as camadas de aplicativos “inferiores” não estejam cientes das camadas “superiores”?

66

Em um aplicativo da 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 da exibição. (Eu imagino que você poderia até começar tão longe quanto o hardware, ou talvez até mais, e o padrão pode ser o mesmo.)

Indo na outra direção, você pode ir apenas uma camada para baixo. 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.)

Eu posso entender intuitivamente porque essa é uma boa ideia, mas não consigo articulá-la. Por que esse estilo unidirecional de camadas é uma boa ideia?

    
por Jason Swett 20.05.2013 / 20:02
fonte

13 respostas

120

Camadas, módulos, na verdade a arquitetura em si, são meios de tornar programas de computador mais fáceis de entender por humanos . O método numericamente ideal de resolver um problema é quase sempre uma bagunça emaranhada de código não-modular, auto-referência ou auto-modificável - seja um código assembler altamente otimizado em sistemas embarcados com restrições de memória incapacitantes ou sequências de DNA após milhões de anos de pressão de seleção. Tais sistemas não têm camadas, nenhuma direção discernível do fluxo de informações, na verdade nenhuma estrutura que possamos discernir de forma alguma. Para todos, menos para o autor, eles parecem funcionar por pura magia.

Na engenharia de software, queremos evitar isso. A boa arquitetura é uma decisão deliberada de sacrificar alguma eficiência para tornar o sistema compreensível por pessoas normais. Entender 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 devem chamar funções uns dos outros e as camadas devem ser criadas umas sobre as outras. Então, na prática, é sempre necessário construir sistemas para que algumas partes requeiram outras partes. O compromisso preferido é construí-los de tal forma que uma parte exija outra, mas essa parte não exige a primeira de volta. E é exatamente isso que a camada unidirecional nos oferece: é possível entender o esquema do banco de dados sem conhecer as regras de negócios e entender as regras de negócios sem saber sobre a interface do usuário. Seria bom ter independência em ambas as direções - permitindo que alguém programe uma nova interface do usuário sem conhecer qualquer coisa sobre as regras de negócios - mas, na prática, isso praticamente nunca é possível. Regras básicas como "Nenhuma dependência cíclica" 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.

    
por 20.05.2013 / 20:30
fonte
61

A motivação fundamental é esta: você quer ser capaz de rasgar uma camada inteira e substituir uma totalmente diferente (reescrita), e ninguém deve (ser capaz) de notar a diferença.

O exemplo mais óbvio é extrair a camada inferior e substituí-la por outra diferente. Isso é o que você faz quando desenvolve a (s) camada (s) superior (es) contra uma simulação do hardware e, em seguida, 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 pelo RS-232. Um dia, você tem que mudar completamente a codificação do protocolo, porque "algo mudou". (Exemplo: alternar 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 a Marina Del Rey e agora está trabalhando em um link de rádio do centro de Los Angeles para uma sonda orbital Europa , uma das luas de Júpiter, e esse link precisa de uma correção de erros 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 irá operar precisamente de acordo com sua interface definida. 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 NOTIFICAÇÃO.

A camada RS-232 não sabe se está executando ASCII, Reed-Solomon, Unicode (página de código em árabe, página de código em japonês, página de código Rigellian Beta) ou o que. Ele apenas sabe que está obtendo uma seqüência de bytes e está gravando esses bytes em uma porta. Na próxima semana, ele pode estar recebendo uma seqüência completamente diferente de bytes 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 O sistema de multiprogramação ". É necessário ler neste negócio.

    
por 20.05.2013 / 20:46
fonte
8

Porque os níveis mais altos podem mudarem.

Quando isso acontece, seja por causa de mudanças de requisitos, novos usuários, tecnologias diferentes, um aplicativo modular (ou seja, unidirecionalmente em camadas) deve exigir menos manutenção e ser mais facilmente adaptado para atender às novas necessidades.

    
por 20.05.2013 / 20:25
fonte
4

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

Aqui está um trecho:

Disadvantages

Tightly coupled systems tend to exhibit the following developmental characteristics, which are often seen as disadvantages: A change in one module usually forces a ripple effect of changes in other modules. Assembly of modules might require more effort and/or time due to the increased inter-module dependency. A particular module might be harder to reuse and/or test because dependent modules must be included.

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

    
por 20.05.2013 / 20:59
fonte
4

IMO, é muito simples. Você não pode reutilizar algo que faz referência ao contexto em que é usado.

    
por 21.05.2013 / 04:10
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ê deve ser capaz de 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ê deve ser capaz de refatorar ou substituir a camada inferior sem alterar a camada superior

Estas 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

O motivo pelo qual preferimos uma estrutura de dependências de cima para baixo é porque os principais objetos criam e usam os objetos inferiores . Uma dependência é basicamente uma relação que significa "A depende de B se A não puder funcionar sem B". Então, se os objetos em A usam os objetos em B, é assim que as dependências devem ir.

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. No entanto, normalmente ainda é preferível ter dependências de cima para baixo, 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 chamada de método, de baixo para cima (normalmente) usamos eventos. Os eventos permitem que as dependências caiam de cima para baixo, mesmo quando o controle flui no sentido contrário. Os objetos da camada superior assinam eventos na camada inferior. A camada inferior não sabe nada sobre a camada superior, que funciona como um plug-in.

Existem também outras formas de manter uma única direção, por exemplo:

  • continuations (passando um lambda ou um método para ser chamado e um evento para um método assíncrono)
  • subclasse (crie uma subclasse em A de uma classe pai em B que é então injetada na camada inferior, um pouco como um plugin)
por 25.05.2013 / 00:13
fonte
3

Eu 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, reduzindo assim a complexidade. Portanto, essa dependência unidirecional é uma boa ideia, porque facilita a compreensão do software e o gerenciamento da complexidade é uma das questões mais importantes no desenvolvimento de software.

Assim, em uma arquitetura em camadas, as camadas inferiores são caixas-pretas que implementam camadas de abstração sobre as quais as camadas superiores são construídas. Se uma camada inferior (digamos, 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 conteúdo (sua implementação) é irrelevante para o usuário!

    
por 20.05.2013 / 20:55
fonte
3

Apenas por diversão.

Pense em uma pirâmide de líderes de torcida. A linha inferior está apoiando as linhas acima deles.

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

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

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

    
por 21.05.2013 / 19:08
fonte
3

Embora a facilidade de compreensão e até certo ponto 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 de manutenção de software. A linha inferior é que dependências causam o potencial de quebrar as coisas.

Por exemplo, suponha que A dependa de B. Como nada depende de A, os desenvolvedores estão livres para alterar A para o conteúdo de seus corações sem se preocupar em quebrar algo diferente de A. No entanto, se o desenvolvedor quiser alterar B então qualquer alteração em B que seja feita poderia potencialmente quebrar A. Este era um problema freqüente nos primeiros dias de computação (pense em desenvolvimento estruturado) onde os desenvolvedores solucionariam um bug em uma parte do programa e aumentariam bugs em partes aparentemente não relacionadas do sistema. programa em outro lugar. Tudo por causa de dependências.

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

Então, na sua pergunta original, se você está em uma pequena equipe para um projeto pequeno, tudo isso é muito exagerado, porque você pode alterar os módulos livremente ao seu capricho. No entanto, se você estiver em um projeto considerável, se todos os módulos dependessem dos outros, toda vez que uma alteração fosse necessária, poderia potencialmente quebrar os outros módulos. Em um projeto grande, saber todos os impactos pode ser difícil de determinar, então você provavelmente perderá alguns impactos.

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

Mas se você depender de dependências de A depende de B, B depende de C, então, apenas pessoas de camada C precisam coordenar suas alterações para ambas as equipes. A camada B precisa apenas coordenar as mudanças com a equipe da Camada A e a equipe da camada A é livre para fazer o que quiser, pois seu código não afeta a camada B ou C. Então, 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 da mudança.

    
por 06.06.2013 / 16:32
fonte
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 em seu sistema Linux, mas eles chamam a mesma função malloc da biblioteca C. Então, 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 através do mundo externo por meio de alguns drivers de dispositivo. O sistema operacional está no meio .

O sistema operacional não depende de detalhes dentro dos aplicativos nem dos drivers de dispositivo. Existem muitos tipos de drivers de dispositivo do mesmo tipo e eles compartilham a mesma estrutura de driver de dispositivo. Às vezes, os hackers do kernel têm que colocar em algum caso especial manipulação na estrutura por causa de um hardware ou dispositivo em particular (exemplo recente me deparei com: código específico da PL2303 na estrutura usb-serial do Linux). Quando isso acontece, eles costumam colocar comentários sobre o quanto isso suga e deve ser removido. Mesmo que o sistema operacional chame funções nos drivers, as chamadas passam por ganchos que fazem os drivers parecerem os mesmos, enquanto que, quando os drivers chamam o SO, eles geralmente usam funções específicas diretamente pelo nome.

Assim, 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 trocados para percorrer os caminhos apropriados. Ele 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 hacker específico de dispositivo ou aplicativo para o hub.

    
por 21.05.2013 / 05:40
fonte
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 interesses confere a capacidade de portabilidade e, em algumas arquiteturas mais complexas, oferece vantagens de escala e desempenho independentes da plataforma.

Nesse contexto, se você pensar em uma arquitetura de 5 camadas (client, presentation, bussiness, integration e resource tier), um nível inferior de arquitetura não deve estar ciente da lógica e do bussiness dos níveis mais altos e vice-versa. Refiro-me ao nível inferior como níveis de integração e recursos. As interfaces de integração de banco de dados fornecidas na integração e no banco de dados real e nos serviços da Web (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 MangoDB em termos de escalabilidade ou qualquer outra coisa.

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

    
por 22.05.2013 / 00:52
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 um descompasso entre o que o cliente precisa e o que ele recebe. À medida que o cliente se comunica com o sistema por meio da interface do usuário e obtém resultado por meio da interface do usuário (a interface do usuário significa literalmente 'interface do usuário'), os bugs também são reportados em termos de interface do usuário. Então, como desenvolvedor, você não tem muita escolha a não ser começar a ver a interface do usuário também, para descobrir o que aconteceu.

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

Bem, você tem três cenários de como esse bug pode 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.

Pode ocorrer em outras partes do sistema como resultado de chamadas feitas a partir da interface do usuário. O que é moderadamente difícil, você rastreia uma árvore de chamadas, localiza um local onde o erro ocorre e corrige-o.

E isso pode ocorrer como resultado de uma chamada em seu código da interface do usuário. O que é difícil, você tem que pegar a chamada, encontrar sua fonte e descobrir onde o erro ocorre. Considerando que um ponto em que você começa 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, você tem sua depuração removida para você.

Para eliminar ao máximo o caso mais difícil, as dependências circulares são strongmente desencorajadas, as camadas se conectam principalmente na forma de cima para baixo. Mesmo quando uma conexão é necessária, 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 que está sendo chamado em retorno normalmente fornece esse retorno de chamada, implementando uma espécie de "opt-in" para conexões reversas e limitando seu impacto na compreensão de um sistema.

O Layering é uma ferramenta e destina-se principalmente a desenvolvedores que suportam um sistema existente. Bem, as conexões entre as camadas refletem isso também.

    
por 06.06.2013 / 15:23
fonte
-1

Outra razão que gostaria de ver explicitamente mencionada aqui é reusabilidade de código . Nós já tivemos o exemplo da mídia RS232 que é substituída, então vamos um passo além ...

Imagine que você está desenvolvendo drivers. É o seu trabalho e você escreve bastante. Provavelmente, os protocolos 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 várias vezes - é escrever camadas reutilizáveis para essas coisas.

Digamos que você tenha que escrever 5 drivers para dispositivos Modbus. Um deles usa o Modbus TCP, dois usam o Modbus no RS485 e o restante passa pelo RS232. Você não vai reimplementar o Modbus 5 vezes, porque você 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 de modbus acima, neste momento? Provavelmente não. O próximo driver que você vai 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 vai saber dos drivers, que são uma camada para cima. Esses drivers, é claro, terão que saber que devem falar sobre modbus, e devem saber que estão usando Ethernet. No entanto, implementado da maneira que acabei de descrever, você pode não apenas extrair uma camada e substituí-la. você poderia, claro - e para mim é o maior benefício de todos, vá em frente e reutilize essa 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 isso nos poupa muito tempo. Existem inúmeras bibliotecas para todos os tipos de protocolos e outras coisas. Eles existem por causa de princípios como a direção da dependência seguindo a direção do comando, o que nos permite construir camadas reutilizáveis de software.

    
por 02.02.2014 / 21:20
fonte