Estou quebrando a prática de POO com essa arquitetura?

23

Eu tenho um aplicativo da web. Não acredito que a tecnologia seja importante. A estrutura é um aplicativo de camada N, mostrado na imagem à esquerda. Existem 3 camadas.

UI (padrão MVC), Business Logic Layer (BLL) e Data Access Layer (DAL)

O problema que tenho é que meu BLL é enorme, pois possui a lógica e os caminhos através da chamada de eventos do aplicativo.

Um fluxo típico através do aplicativo pode ser:

Evento disparado na interface do usuário, vá para um método na BLL, execute a lógica (possivelmente em várias partes da BLL), eventualmente para o DAL, retorne à BLL (onde provavelmente há mais lógica) e retorne algum valor à interface do usuário.

O BLL neste exemplo está muito ocupado e estou pensando em como dividir isso. Eu também tenho a lógica e os objetos combinados dos quais não gosto.

insira a descrição da imagem aqui

A versão à direita é o meu esforço.

A lógica ainda é como o aplicativo flui entre a interface do usuário e o DAL, mas provavelmente não há propriedades ... Somente métodos (a maioria das classes nessa camada pode ser estática, pois não armazena nenhum estado). A camada Poco é onde existem classes que possuem propriedades (como uma classe Person, onde haveria nome, idade, altura etc.). Isso não teria nada a ver com o fluxo do aplicativo, eles apenas armazenam o estado.

O fluxo pode ser:

Mesmo acionado a partir da interface do usuário e passa alguns dados para o controlador de camada de interface do usuário (MVC). Isso traduz os dados brutos e os converte no modelo poco. O modelo poco é então passado para a camada lógica (que era o BLL) e, eventualmente, para a camada de consulta de comando, potencialmente manipulada no caminho. A camada de consulta de comando converte o POCO em um objeto de banco de dados (que é quase a mesma coisa, mas uma foi projetada para persistência e a outra para o front end). O item é armazenado e um objeto de banco de dados é retornado à camada de Consulta de Comando. Em seguida, é convertido em um POCO, onde retorna à camada Lógica, potencialmente processado posteriormente e, finalmente, volta à interface do usuário.

A lógica e as interfaces compartilhadas é onde podemos ter dados persistentes, como MaxNumberOf_X e TotalAllowed_X e todas as interfaces.

A lógica / interfaces compartilhadas e o DAL são a "base" da arquitetura. Eles não sabem nada sobre o mundo exterior.

Tudo sabe sobre o poco além da lógica / interfaces compartilhadas e do DAL.

O fluxo ainda é muito semelhante ao primeiro exemplo, mas tornou cada camada mais responsável por uma coisa (seja estado, fluxo ou qualquer outra coisa) ... mas estou rompendo a OOP com essa abordagem?

Um exemplo para demonstrar a lógica e o Poco pode ser:

public class LogicClass
{
    private ICommandQueryObject cmdQuery;
    public PocoA Method1(PocoB pocoB) 
    { 
        return cmdQuery.Save(pocoB); 
    }

    /*This has no state objects, only ways to communicate with other 
    layers such as the cmdQuery. Everything else is just function 
    calls to allow flow via the program */
    public PocoA Method2(PocoB pocoB) 
    {         
        pocoB.UpdateState("world"); 
        return Method1(pocoB);
    }

}

public struct PocoX
{
     public string DataA {get;set;}
     public int DataB {get;set;}
     public int DataC {get;set;}

    /*This simply returns something that is part of this class. 
     Everything is self-contained to this class. It doesn't call 
     trying to directly communicate with databases etc*/
     public int GetValue()
     {

         return DataB * DataC; 
     }

     /*This simply sets something that is part of this class. 
     Everything is self-contained to this class. 
     It doesn't call trying to directly communicate with databases etc*/
     public void UpdateState(string input)
     {        
         DataA += input;  
     }
}
MyDaftQuestions
fonte
Não vejo nada de fundamentalmente errado com sua arquitetura, como você a descreveu atualmente.
Robert Harvey
19
Não há detalhes funcionais suficientes no seu exemplo de código para fornecer mais informações. Exemplos de Foobar raramente fornecem ilustração suficiente.
Robert Harvey
1
Enviado para sua consideração: Baruco 2012: Desconstruindo a estrutura, por Gary Bernhardt
Theraot
4
Podemos encontrar um título melhor para esta pergunta, para que possa ser encontrada online mais facilmente?
Soner Gönül 03/04
1
Só para ser pedante: uma camada e uma camada não são a mesma coisa. Uma "camada" fala sobre implantação, uma "camada" sobre lógica. Sua camada de dados será implantada nas camadas de código do lado do servidor e de banco de dados. Sua camada de interface do usuário será implantada nas camadas de código do cliente da Web e do lado do servidor. A arquitetura que você mostra é uma arquitetura de 3 camadas. Suas camadas são "Web client", "Server side code" e "Database".
Laurent LA RIZZA 04/04

Respostas:

54

Sim, é muito provável que você esteja quebrando os principais conceitos de OOP. No entanto, não se sinta mal, as pessoas fazem isso o tempo todo, isso não significa que sua arquitetura esteja "errada". Eu diria que é provavelmente menos sustentável do que um design OO adequado, mas isso é bastante subjetivo e, de qualquer maneira, não é sua pergunta. ( Aqui está um artigo meu criticando a arquitetura de várias camadas em geral).

Raciocínio : O conceito mais básico de POO é que dados e lógica formam uma única unidade (um objeto). Embora seja uma declaração muito simplista e mecânica, mesmo assim, ela não é realmente seguida no seu design (se eu entendi corretamente). Você está claramente separando a maioria dos dados da maior parte da lógica. Ter métodos sem estado (como estáticos), por exemplo, é chamado de "procedimentos" e geralmente é antitético ao POO.

É claro que sempre há exceções, mas esse design viola essas coisas como regra.

Mais uma vez, gostaria de enfatizar "viola OOP"! = "Errado", portanto isso não é necessariamente um julgamento de valor. Tudo depende das restrições de sua arquitetura, casos de uso de manutenção, requisitos, etc.

Robert Bräutigam
fonte
9
Tenha um voto positivo, esta é uma boa resposta, se eu estivesse escrevendo o meu próprio, copiaria e colaria isso, mas também acrescentaria que, se você não escrever código OOP, talvez deva considerar uma linguagem não OOP, pois vem com um monte de sobrecarga extra que você pode ficar sem se não estiver usando
TheCatWhisperer 02/04
2
@TheCatWhisperer: As arquiteturas empresariais modernas não descartam o OOP completamente, apenas seletivamente (por exemplo, para DTOs).
Robert Harvey
@RobertHarvey Concordou, eu quis dizer se você não usa o OOP em quase nenhum lugar do seu design
TheCatWhisperer
@TheCatWhisperer Muitas das vantagens de um oop como c # não estão necessariamente na parte oop do idioma, mas no suporte disponível como bibliotecas, estúdio visual, gerenciamento de memória etc.
@Orangesandlemons Tenho certeza de que existem muitos outros idiomas bem suportados por aí ...
TheCatWhisperer 08/04
31

Um dos princípios fundamentais da programação funcional são as funções puras.

Um dos princípios fundamentais da Programação Orientada a Objetos é reunir funções com os dados em que atuam.

Esses dois princípios básicos desaparecem quando seu aplicativo precisa se comunicar com o mundo exterior. Na verdade, você só pode ser fiel a esses ideais em um espaço especialmente preparado em seu sistema. Nem todas as linhas do seu código devem atender a esses ideais. Mas se nenhuma linha do seu código atender a esses ideais, não será possível afirmar que você está usando OOP ou FP.

Portanto, não há problema em ter apenas "objetos" de dados que você percorre, porque precisa cruzar um limite que você simplesmente não pode refatorar para mover o código interessado. Apenas saiba que não é OOP. Isso é realidade. OOP é quando, uma vez dentro desse limite, você reúne toda a lógica que atua nesses dados em um único local.

Não que você tenha que fazer isso também. OOP não é tudo para todas as pessoas. É o que é. Apenas não afirme que algo segue o OOP quando não acontece ou você vai confundir as pessoas que tentam manter seu código.

Seu POCO parece ter uma lógica de negócios muito bem, então eu não me preocuparia muito em ser anêmico. O que me preocupa é que todos parecem muito mutáveis. Lembre-se de que getters e setters não fornecem encapsulamento real. Se o seu POCO está indo para esse limite, tudo bem. Apenas entenda que isso não está oferecendo todos os benefícios de um objeto OOP encapsulado real. Alguns chamam isso de objeto de transferência de dados ou DTO.

Um truque que usei com sucesso é criar objetos OOP que comem DTOs. Eu uso o DTO como um objeto de parâmetro . Meu construtor lê o estado a partir dele (lido como cópia defensiva ) e o joga de lado. Agora eu tenho uma versão totalmente encapsulada e imutável do DTO. Todos os métodos relacionados a esses dados podem ser movidos para cá, desde que estejam deste lado desse limite.

Eu não forneço getters ou setters. Eu sigo dizer, não pergunte . Você chama meus métodos e eles vão fazer o que precisa ser feito. Eles provavelmente nem lhe dizem o que fizeram. Eles apenas fazem isso.

Agora, eventualmente, algo, em algum lugar, vai esbarrar em outro limite e tudo isso se desfaz novamente. Isso é bom. Gire outro DTO e jogue-o por cima do muro.

Essa é a essência do objetivo da arquitetura de portas e adaptadores. Eu tenho lido sobre isso de uma perspectiva funcional . Talvez também lhe interesse.

candied_orange
fonte
5
" getters e setters não fornecem encapsulamento real " - sim!
Boris the Spider
3
@BoristheSpider - getters e setters fornecem absolutamente encapsulamento, eles simplesmente não se encaixam na sua definição restrita de encapsulamento.
Davor Ždralo 03/04
4
@ DavorŽdralo: Eles são ocasionalmente úteis como solução alternativa, mas por sua própria natureza, os getters e setters quebram o encapsulamento. Fornecer uma maneira de obter e definir alguma variável interna é o oposto de ser responsável por seu próprio estado e por agir sobre ele.
cHao 03/04
5
@cHao - você não entende o que é um getter. Isso não significa um método que retorna um valor de uma propriedade de objeto. É uma implementação comum, mas pode retornar um valor de um banco de dados, solicitá-lo por http, calculá-lo em tempo real, qualquer que seja. Como eu disse, getters e setters quebram o encapsulamento apenas quando as pessoas usam suas próprias definições estreitas (e incorretas).
Davor Ždralo 03/04
4
@cHao - encapsulamento significa que você está ocultando a implementação. Isso é o que é encapsulado. Se você tiver o getter "getSurfaceArea ()" em uma classe Square, não saberá se a área de superfície é um campo, se é calculado em tempo real (altura de retorno * largura) ou algum terceiro método, para que você possa alterar a implementação interna quando quiser, porque está encapsulado.
Davor Ždralo 03/04
1

Se eu leio sua explicação corretamente, seus objetos ficam mais ou menos assim: (complicado sem contexto)

public class LogicClass
{
    private ICommandQueryObject cmdQuery;
    public PocoA Method(PocoB pocoB) { ... }
}

public class PocoX
{
     public string DataA {get;set;}
     public int DataB {get;set;}
     ... etc
}

Na medida em que suas classes Poco contêm apenas dados e suas classes Logic contêm os métodos que atuam nesses dados; sim, você violou os princípios do "Classic OOP"

Novamente, é difícil dizer a partir de sua descrição generalizada, mas eu arriscaria que o que você escreveu pudesse ser classificado como Modelo de Domínio Anêmico.

Eu não acho que essa seja uma abordagem particularmente ruim, nem, se você considerar o seu Poco como uma estrutura, ele necessariamente quebrará a OOP no sentido mais específico. Agora seus Objetos são agora as LogicClasses. De fato, se você tornar seus Pocos imutáveis, o design poderá ser considerado bastante funcional.

No entanto, quando você faz referência à lógica compartilhada, aos Pocos quase iguais, e à estática, começo a me preocupar com os detalhes do seu design.

Ewan
fonte
Eu adicionei ao meu post, essencialmente copiando seu exemplo. Desculpe ti não estava claro para começar
MyDaftQuestions
1
o que quero dizer é que, se você nos dissesse o que o aplicativo faz, seria mais fácil escrever exemplos. Em vez de LogicClass, você pode ter o PaymentProvider ou o que for
Ewan
1

Um problema em potencial que eu vi no seu design (e é muito comum) - alguns dos piores códigos "OO" que eu já encontrei foram causados ​​por uma arquitetura que separava os objetos "Data" dos objetos "Code". Isso é coisa de nível de pesadelo! O problema é que, em qualquer parte do código comercial, quando você deseja acessar seus objetos de dados, você Tende a codificá-lo imediatamente (não é necessário), você pode criar uma classe de utilitário ou outra função para lidar com isso, mas é isso que Eu já vi acontecer repetidamente ao longo do tempo).

O código de acesso / atualização geralmente não é coletado, então você acaba com funcionalidades duplicadas em todos os lugares.

Por outro lado, esses objetos de dados são úteis, por exemplo, como persistência do banco de dados. Eu tentei três soluções:

Copiar valores para dentro e para fora para objetos "reais" e jogar fora seu objeto de dados é entediante (mas pode ser uma solução válida se você quiser seguir esse caminho).

Adicionar métodos de organização de dados aos objetos de dados pode funcionar, mas pode resultar em um grande objeto de dados confuso que está fazendo mais de uma coisa. Também pode dificultar o encapsulamento, já que muitos mecanismos de persistência querem acessadores públicos ... Eu não amei quando o fiz, mas é uma solução válida

A solução que melhor funcionou para mim é o conceito de uma classe "Wrapper" que encapsula a classe "Data" e contém toda a funcionalidade de manipulação de dados - então eu não exponho a classe de dados de maneira alguma (nem mesmo setters e getters a menos que sejam absolutamente necessários). Isso remove a tentação de manipular o objeto diretamente e obriga a adicionar funcionalidades compartilhadas ao wrapper.

A outra vantagem é que você pode garantir que sua classe de dados esteja sempre em um estado válido. Aqui está um exemplo rápido de psuedocode:

// Data Class
Class User {
    String name;
    Date birthday;
}

Class UserHolder {
    final private User myUser // Cannot be null or invalid

    // Quickly wrap an object after getting it from the DB
    public UserHolder(User me)
    {
        if(me == null ||me.name == null || me.age < 0)
            throw Exception
        myUser=me
    }

    // Create a new instance in code
    public UserHolder(String name, Date birthday) {
        User me=new User()
        me.name=name
        me.birthday=birthday        
        this(me)
    }
    // Methods access attributes, they try not to return them directly.
    public boolean canDrink(State state) {
        return myUser.birthday.year < Date.yearsAgo(state.drinkingAge) 
    }
}

Observe que você não tem a verificação de idade espalhada por todo o código em áreas diferentes e também não se sente tentada a usá-la porque não consegue nem descobrir qual é o aniversário (a menos que precise de algo mais, em Nesse caso, você pode adicioná-lo).

Eu tendem a não apenas estender o objeto de dados porque você perde esse encapsulamento e a garantia de segurança - nesse ponto, você também pode adicionar os métodos à classe de dados.

Dessa forma, sua lógica de negócios não possui um monte de junk / iteradores de acesso a dados espalhados por ela, torna-se muito mais legível e menos redundante. Também recomendo adquirir o hábito de agrupar sempre as coleções pelo mesmo motivo - mantendo as construções de loop / pesquisa fora da lógica de negócios e garantindo que elas estejam sempre em bom estado.

Bill K
fonte
1

Nunca mude seu código porque você pensa ou alguém lhe diz que não é isso ou aquilo. Altere seu código se houver problemas e você descobriu uma maneira de evitar esses problemas sem criar outros.

Portanto, além de você não gostar das coisas, você quer investir muito tempo para fazer uma mudança. Anote os problemas que você tem agora. Anote como seu novo design resolveria os problemas. Descubra o valor da melhoria e o custo de fazer suas alterações. Então - e isso é mais importante - verifique se você tem tempo para concluir essas alterações, ou você terminará metade nesse estado, metade nesse estado, e essa é a pior situação possível. (Certa vez, trabalhei em um projeto com 13 tipos diferentes de cadeias de caracteres e três esforços identificáveis ​​e semi-arsados ​​para padronizar um tipo)

gnasher729
fonte
0

A categoria "OOP" é muito maior e mais abstrata do que o que você está descrevendo. Não se importa com tudo isso. Preocupa-se com responsabilidade clara, coesão, acoplamento. Portanto, no nível que você está perguntando, não faz muito sentido perguntar sobre a "prática OOPS".

Dito isto, para o seu exemplo:

Parece-me que há um mal-entendido sobre o que significa MVC. Você está chamando sua interface do usuário de "MVC", separadamente da lógica de negócios e do controle de "back-end". Mas para mim, o MVC inclui todo o aplicativo da web:

  • Modelo - contém os dados corporativos + lógica
    • Camada de dados como detalhes de implementação do modelo
  • Ver - código da interface do usuário, modelos HTML, CSS etc.
    • Inclui aspectos do lado do cliente, como JavaScript, ou as bibliotecas para aplicativos da Web "de uma página" etc.
  • Controle - a cola do servidor entre todas as outras partes
  • (Existem extensões como ViewModel, Lote etc., nas quais não vou entrar aqui)

Existem algumas suposições básicas extremamente importantes aqui:

  • Uma classe / objetos Model nunca tem nenhum conhecimento sobre qualquer uma das outras partes (View, Control, ...). Ele nunca os chama, não assume que seja chamado por eles, não recebe atributos / parâmetros da sessão ou qualquer outra coisa ao longo desta linha. Está completamente sozinho. Nos idiomas que suportam isso (por exemplo, Ruby), você pode acionar uma linha de comando manual, instanciar classes Model, trabalhar com elas no conteúdo do seu coração e pode fazer tudo o que faz sem nenhuma instância de Control ou View ou qualquer outra categoria. Não tem conhecimento sobre sessões, usuários, etc., o mais importante.
  • Nada toca a camada de dados, exceto através de um modelo.
  • A visualização possui apenas um leve toque no modelo (exibindo itens etc.) e nada mais. (Observe que uma boa extensão é "ViewModel", que são classes especiais que processam mais substancialmente para renderizar dados de maneira complicada, que não se encaixariam bem no Model ou no View - este é um bom candidato para remover / evitar inchaço no Modelo puro).
  • O controle é o mais leve possível, mas é responsável por reunir todos os outros jogadores e transferir coisas entre eles (por exemplo, extrair entradas do usuário de um formulário e encaminhá-lo para o modelo, encaminhar exceções da lógica de negócios para um útil mensagens de erro para o usuário etc.). Para APIs Web / HTTP / REST etc., toda a autorização, segurança, gerenciamento de sessões, gerenciamento de usuários etc. acontece aqui (e somente aqui).

Importante: a interface do usuário faz parte do MVC. Não o contrário (como no seu diagrama). Se você aceitar isso, os modelos gordos são realmente muito bons - desde que eles realmente não contenham coisas que não deveriam.

Observe que "modelos de gordura" significa que toda a lógica de negócios está na categoria Modelo (pacote, módulo, seja qual for o nome no idioma de sua escolha). Obviamente, as classes individuais devem ser estruturadas em OOP de acordo com as diretrizes de codificação fornecidas (isto é, algumas linhas de código máximas por classe ou método, etc.).

Observe também que como a camada de dados é implementada tem consequências muito importantes; especialmente se a camada do modelo é capaz de funcionar sem uma camada de dados (por exemplo, para testes de unidade ou para DBs baratos na memória no laptop do desenvolvedor, em vez de DBs caros da Oracle ou o que você tiver). Mas esse é realmente um detalhe de implementação no nível da arquitetura que estamos analisando agora. Obviamente, aqui você ainda deseja fazer uma separação, ou seja, eu não gostaria de ver código que tenha lógica de domínio pura entrelaçada diretamente com o acesso a dados, acoplando-o intensamente. Um tópico para outra pergunta.

Voltando à sua pergunta: Parece-me que há uma grande sobreposição entre sua nova arquitetura e o esquema MVC que descrevi, para que você não esteja completamente errado, mas parece que está reinventando algumas coisas, ou utilizá-lo porque o seu ambiente / bibliotecas de programação atual sugere isso. Difícil de dizer para mim. Portanto, não posso lhe dar uma resposta exata sobre se o que você pretende é particularmente bom ou ruim. Você pode descobrir verificando se cada "coisa" tem exatamente uma classe responsável por isso; se tudo é altamente coeso e com baixo acoplamento. Isso fornece uma boa indicação e, na minha opinião, é suficiente para um bom design de OOP (ou uma boa referência do mesmo, se você desejar).

AnoE
fonte