Como criar um código OO melhor em um aplicativo orientado a banco de dados relacional em que o banco de dados é mal projetado

19

Estou escrevendo um aplicativo da web Java que consiste principalmente em várias páginas semelhantes nas quais todas as páginas têm várias tabelas e um filtro que se aplica a essas tabelas. Os dados nessas tabelas são provenientes de um banco de dados SQL.

Estou usando myBatis como ORM, o que pode não ser a melhor opção no meu caso, pois o banco de dados é mal projetado e o mybatis é uma ferramenta mais orientada ao banco de dados.

Estou descobrindo que estou escrevendo muito código duplicado porque, devido ao design inadequado do banco de dados, tenho que escrever consultas diferentes para coisas semelhantes, pois essas consultas podem ser muito diferentes. Ou seja, não posso parametrizar facilmente as consultas. Isso se propaga no meu código e, em vez de preencher as linhas nas colunas da minha tabela com um loop simples, tenho código como:

obter dados A (p1, ..., pi);

obter dados B (p1, ..., pi);

obter dados C (p1, ..., pi);

obter dados D (p1, ..., pi); ...

E isso logo explode quando temos tabelas diferentes com colunas diferentes.

Também adiciona à complexidade o fato de eu estar usando "wicket", que é, na verdade, um mapeamento de objetos para elementos html na página. Portanto, meu código Java se torna um adaptador entre o banco de dados e o front end, o que me fez criar muita fiação, código padrão com alguma lógica entrelaçada nele.

A solução correta seria agrupar os mapeadores ORM com uma extralayer que apresenta uma interface mais homogênea para o banco de dados ou existe uma maneira melhor de lidar com esse código de espaguete que estou escrevendo?

EDIT: Mais informações sobre o banco de dados

O banco de dados contém principalmente informações de chamadas telefônicas. O design deficiente consiste em:

Tabelas com um ID artificial como chave primária que não tem nada a ver com o conhecimento do domínio.

Nenhuma chave exclusiva, gatilhos, cheques ou chaves estrangeiras.

Campos com um nome genérico que correspondem a diferentes conceitos para diferentes registros.

Registros que podem ser categorizados apenas cruzando com outras tabelas com condições diferentes.

Colunas que devem ser números ou datas armazenadas como seqüências de caracteres.

Para resumir, um design bagunçado / preguiçoso ao redor.

DPM
fonte
7
A correção do design do banco de dados é uma opção?
RMalke
1
Por favor, explique como o banco de dados está mal projetado.
Tulains Córdova
@ Renan Malke Stigliani Infelizmente, não, pois existe um software legado que depende disso, no entanto, espelhei algumas das tabelas com um design um pouco diferente e as preenchi, o que simplifica o código. No entanto, eu não me orgulho disso e eu tinha tabelas em vez não duplicados indiscriminadamente
DPM
1
Este livro pode dar-lhe Som eideas de como você pode começar a corrigir o problesm datbase e manter em funcionamento o código legado: amazon.com/...
HLGEM
4
A maioria dos problemas que você lista. . . não são. O uso de chaves substitutas em vez de chaves naturais é atualmente uma recomendação bastante padrão atualmente; não é "design deficiente". A falta de restrições e o uso de tipos de colunas inadequados é um exemplo melhor no que diz respeito ao "design inadequado", mas na verdade não deve afetar o código do aplicativo (a menos que você planeje abusar desses problemas?).
Ruakh

Respostas:

53

A orientação a objetos é valiosa especificamente porque esses tipos de cenários surgem e fornece ferramentas para projetar razoavelmente abstrações que permitem encapsular a complexidade.

A verdadeira questão aqui é: onde você encapsula essa complexidade?

Então, deixe-me recuar um momento e falar a que "complexidade" estou me referindo aqui. Seu problema (como eu o entendo; corrija-me se estiver errado) é um modelo de persistência que não é um modelo efetivamente utilizável para as tarefas que você precisa concluir com os dados. Pode ser eficaz e utilizável para outras tarefas, mas não para suas tarefas.

Então, o que fazemos quando temos dados que não apresentam um bom modelo para nossos meios?

Traduzir. É a única coisa que você pode fazer. Essa tradução é a 'complexidade' a que me refiro acima. Então, agora que aceitamos que vamos traduzir o modelo, precisamos decidir sobre alguns fatores.

Precisamos traduzir as duas direções? As duas direções serão traduzidas da mesma forma, como em:

(Tbl A, Tbl B) -> Obj X (leitura)

Obj X -> (Tbl A, Tbl B) (gravação)

ou as atividades de inserção / atualização / exclusão representam um tipo diferente de objeto, como você lê os dados como Obj X, mas os dados são inseridos / atualizados no Obj Y? Qual dessas duas maneiras você deseja seguir ou se nenhuma atualização / inserção / exclusão é possível são fatores importantes para onde você deseja colocar a tradução.


Para onde você traduz

De volta à primeira declaração que fiz nesta resposta; OO permite que você encapsule a complexidade, e o que eu me refiro aqui é o fato de que você não apenas deve, mas deve encapsular essa complexidade se quiser garantir que ela não vaze e se infiltre em todo o seu código. Ao mesmo tempo, é importante reconhecer que você não pode ter uma abstração perfeita; portanto, se preocupe menos com isso do que com uma muito eficaz e utilizável.

Novamente agora; seu problema é: onde você coloca essa complexidade? Bem, você tem escolhas.

Você pode fazer isso no banco de dados usando procedimentos armazenados. Isso tem a desvantagem de não jogar muito bem com ORMs, mas isso nem sempre é verdade. Os procedimentos armazenados oferecem alguns benefícios, incluindo o desempenho frequentemente. No entanto, os procedimentos armazenados podem exigir muita manutenção, mas cabe a você analisar seu cenário específico e dizer se a manutenção será mais ou menos que outras opções. Pessoalmente, sou muito habilidoso com procedimentos armazenados e, como tal, esse fato de talento disponível reduz a sobrecarga; Nunca subestime o valor de tomar decisões com base no que você não sabe. Às vezes, a solução abaixo do ideal pode ser mais ideal do que a solução correta, porque você ou sua equipe sabem como criá-la e mantê-la melhor do que a solução ideal.

Outra opção no banco de dados são visualizações. Dependendo do servidor do banco de dados, eles podem ser altamente ideais ou sub-ideais ou nem mesmo eficazes, uma das desvantagens pode ser o tempo de consulta, dependendo das opções de indexação disponíveis no seu banco de dados. As visualizações se tornam uma opção ainda melhor se você nunca precisar fazer nenhuma modificação de dados (inserir / atualizar / excluir).

Ultrapassando o banco de dados, você tem o modo de espera antigo de usar o padrão de repositório. Esta é uma abordagem testada pelo tempo, que pode ser muito eficaz. As desvantagens tendem a incluir a placa da caldeira, mas os repositórios bem-fatorados podem evitar parte disso, e mesmo quando resultam em quantidades infelizes da placa da caldeira, os repositórios tendem a ser um código simples, fácil de entender e manter, além de apresentar uma boa API /abstração. Além disso, os repositórios podem ser bons para a unidade de testabilidade, que você perde com as opções no banco de dados.

Existem ferramentas como o mapeador automático por aí que podem tornar plausível o uso de um ORM, onde é possível fazer a conversão entre o modelo de banco de dados do orm e os modelos utilizáveis, mas algumas dessas ferramentas podem ser complicadas para manter / entender o comportamento mais como mágica; embora eles criem um mínimo de código adicional, resultando em menos custo adicional de manutenção quando bem compreendidos.

A seguir, você está se afastando cada vez mais do banco de dados , o que significa que haverá uma quantidade maior de código que lidará com o modelo de persistência não traduzido, o que será genuinamente desagradável. Nesses cenários, você fala sobre colocar a camada de tradução na interface do usuário, o que parece que você está fazendo agora. Geralmente, é uma péssima idéia e decai terrivelmente com o tempo.


Agora vamos começar a falar loucamente .

A Objectnão é a única abstração completa que existe. Houve uma profundidade de abstrações desenvolvidas ao longo dos muitos anos em que a ciência da computação foi estudada e mesmo antes disso a partir do estudo da matemática. Se vamos começar a ser criativos, vamos começar a falar sobre abstrações conhecidas disponíveis que foram estudadas.

Existe o modelo do ator.Essa é uma abordagem interessante, pois diz que tudo o que você faz é enviar mensagens para outro código que efetivamente delega todo o trabalho para esse outro código, o que é muito eficaz para encapsular a complexidade de todo o seu código. Isso pode funcionar desde que você envie uma mensagem a um ator dizendo "Eu preciso que o Obj X seja enviado para Y" e você tenha um receptáculo aguardando uma resposta no local Y que processa o Obj X. Você pode até enviar uma mensagem que instrua "Preciso do Obj X e da computação Y, Z executados" e então você nem precisa esperar; a tradução ocorre do outro lado da mensagem e você pode seguir em frente se não precisar ler o resultado. Isso pode ser um pequeno abuso do modelo de ator para seus propósitos, mas tudo depende;

Outro limite de encapsulamento são os limites do processo. Eles podem ser usados ​​para segregar a complexidade de maneira muito eficaz. Você pode criar o código de conversão como um serviço da Web em que a comunicação é HTTP simples, usando SOAP, REST ou se você realmente deseja seu próprio protocolo (não sugerido). O STOMP não é um protocolo totalmente novo e ruim. Ou use um serviço daemon normal com um canal de memória divulgada localmente no sistema para se comunicar muito rapidamente novamente usando o protocolo que você escolher. Na verdade, isso tem alguns benefícios muito bons:

  • Você pode ter vários processos em execução que fazem a conversão para suporte a versões mais antigas e mais recentes ao mesmo tempo, permitindo atualizar o serviço de tradução para divulgar um modelo de objeto V2 e, em seguida, atualizar posteriormente o código de consumo para trabalhar com o novo objeto. modelo.
  • Você pode fazer coisas interessantes, como fixar o processo em um núcleo para desempenho, além de obter uma certa segurança de segurança nessa abordagem, tornando esse o único processo em execução com os privilégios de segurança para tocar esses dados.
  • Você terá um limite muito forte ao falar sobre os limites do processo que permanecerão fixos, garantindo um vazamento mínimo de sua abstração por um longo tempo, porque escrever código no espaço de tradução não poderá ser chamado fora do espaço de tradução, pois eles não compartilhará o escopo do processo, garantindo um conjunto fixo de cenários de uso por contrato.
  • Capacidade de atualizações assíncronas / sem bloqueio sendo mais simples.

Obviamente, os inconvenientes são mais manutenção do que o normalmente necessário, a sobrecarga da comunicação afetando o desempenho e a manutenção.


Há uma grande variedade de maneiras de encapsular a complexidade que podem permitir que essa complexidade seja colocada em lugares cada vez mais estranhos e curiosos em seu sistema. Usando formas de funções de ordem superior (muitas vezes falsificadas usando padrão de estratégia ou várias outras formas estranhas de padrões de objetos), você pode fazer algumas coisas muito interessantes.

É isso mesmo, vamos começar a falar sobre uma mônada. Você pode criar essa camada de tradução de maneira muito independente de pequenas funções específicas que executam as traduções independentes necessárias, mas oculta todas essas funções de tradução que não são visíveis e, portanto, dificilmente são acessíveis ao código externo. Isso tem o benefício de reduzir a dependência deles, permitindo que eles mudem facilmente sem afetar muito o código externo. Em seguida, você cria uma classe que aceita funções de ordem superior (funções anônimas, funções lambda, objetos de estratégia, porém é necessário estruturá-las) que funcionam em qualquer um dos bons objetos do tipo de modelo OO. Você então deixa o código subjacente que aceita essas funções executar literalmente usando os métodos de conversão apropriados.

Isso cria um limite em que toda a tradução não existe apenas do outro lado do limite, longe de todo o seu código; ele é usado apenas nesse lado, permitindo que o restante do seu código nem saiba nada sobre ele, exceto onde está o ponto de entrada para esse limite.

Ok, sim, isso realmente está falando loucura, mas quem sabe; você pode ser tão louco (sério, não faça mônadas com uma classificação de loucura abaixo de 88%, existe um risco real de lesão corporal).

Jimmy Hoffa
fonte
4
Uau, que resposta extraordinariamente abrangente. Eu votaria nisso mais de uma vez se apenas a SE me permitisse.
Marjan Venema
11
Quando será lançada a versão do filme?
yannis
3
@JimmyHoffa Bravo sir !!! Vou marcar esta resposta como favorito e mostrar à minha filha quando ela ficar mais velha.
Tombatron
4

Minha sugestão:

Crie visualizações de banco de dados que:

  1. Atribua nomes significativos às colunas
  2. Faça o "cruzamento com outras tabelas com condições diferentes" para que você possa esconder essa complexidade.
  3. Converta números ou datas armazenados como seqüências de caracteres em números e datas, respectivamente.
  4. Crie exclusividade onde não houver, de acordo com alguns critérios.

A idéia é criar uma fachada que emule um design melhor em cima do ruim.

Em seguida, faça o ORM se relacionar com essa fachada em vez das tabelas reais.

Isso não simplifica as inserções.

Tulains Córdova
fonte
Usar visualizações de banco de dados parece uma ótima idéia e o curso de ações mais elegante que abstrai a feiúra no nível mais baixo, por algum motivo que eu não tinha considerado. Obrigado.
DPM
3

Eu posso ver como o esquema do banco de dados existente faz com que você escreva códigos e consultas mais específicos para tarefas que, de outra forma, poderiam ser abstraídas com um esquema mais bem projetado, mas isso não deve prejudicar sua capacidade de escrever um bom código orientado a objetos.

  • Lembre-se dos princípios do SOLID .
  • Escreva um código que possa ser facilmente testado em unidade (que geralmente ocorre seguindo os princípios do SOLID).
  • Mantenha sua lógica de negócios separada da lógica de exibição.
  • Leia a documentação do Apache Wicket e os exemplos - essa estrutura provavelmente pode economizar mais código padrão do que você pensa, então aprenda como usá-lo de maneira eficaz.
  • Mantenha a lógica que precisa lidar com o banco de dados em uma camada separada que forneça uma interface limpa com a qual sua lógica de negócios possa trabalhar. Dessa forma, se você (ou um futuro mantenedor) tiver a chance de melhorar o esquema, ele poderá fazê-lo sem muitas alterações na lógica de negócios.

Quando você se encontra trabalhando com um esquema de banco de dados que não é perfeito, é fácil entender todas as maneiras que dificultam seu trabalho, mas em algum momento você precisa anular essas reclamações e tirar o melhor proveito possível.

Pense nisso como uma oportunidade de usar sua criatividade para escrever códigos limpos, reutilizáveis ​​e de fácil manutenção, apesar do esquema imperfeito.

Mike Partridge
fonte
1

Respondendo à sua pergunta inicial sobre um melhor código orientado a objetos, sugiro o uso de objetos que falam SQL . O ORM contraria intrinsecamente os princípios orientados a objetos, uma vez que opera sobre um objeto, e o objeto no OOP é uma entidade auto-suficiente, que possui todos os recursos para atingir seu objetivo. Tenho certeza de que essa abordagem pode simplificar seu código.

Falando sobre espaço problemático, ou seja, seu domínio, eu tentaria identificar raízes agregadas . Esses são os limites de consistência do seu domínio. Limites que absolutamente devem manter sua consistência o tempo todo. Os agregados se comunicam por meio de eventos do domínio. Se você tem um sistema grande o suficiente, provavelmente deve começar a dividi-lo em subsistemas (chame SOA, Microservice, sistemas , etc.)

Eu também consideraria o uso do CQRS - ele pode simplificar bastante o lado de gravação e de leitura. Certifique-se de ler o artigo de Udi Dahan sobre este tópico.

Zapadlo
fonte