Aplicando princípios do SOLID

13

Sou bastante novo nos princípios de design do SOLID . Entendo suas causas e benefícios, mas ainda não os aplico a um projeto menor que quero refatorar como um exercício prático para usar os princípios do SOLID. Sei que não há necessidade de alterar um aplicativo que funcione perfeitamente, mas quero refatorá-lo de qualquer maneira, para ganhar experiência em design para projetos futuros.

O aplicativo tem a seguinte tarefa (na verdade, muito mais do que isso, mas vamos simplificar): Ele deve ler um arquivo XML que contém definições de Tabela de banco de dados / coluna / exibição etc. e criar um arquivo SQL que pode ser usado para criar um esquema de banco de dados ORACLE.

(Observação: evite discutir por que eu preciso ou por que não uso o XSLT e assim por diante, há motivos, mas eles são fora de tópico.)

Para começar, escolhi apenas as tabelas e restrições. Se você ignorar colunas, poderá declarar da seguinte maneira:

Uma restrição faz parte de uma tabela (ou mais precisamente, parte de uma instrução CREATE TABLE) e uma restrição também pode fazer referência a outra tabela.

Primeiro, explicarei como é o aplicativo agora (sem aplicar o SOLID):

No momento, o aplicativo possui uma classe "Tabela" que contém uma lista de ponteiros para Restrições pertencentes à tabela e uma lista de ponteiros para Restrições que fazem referência a esta tabela. Sempre que uma conexão é estabelecida, a conexão reversa também é estabelecida. A tabela possui um método createStatement () que, por sua vez, chama a função createStatement () de cada restrição. O próprio método usará as conexões com a tabela do proprietário e a tabela referenciada para recuperar seus nomes.

Obviamente, isso não se aplica ao SOLID. Por exemplo, existem dependências circulares, que incharam o código em termos dos métodos "add" / "remove" necessários e alguns destruidores de objetos grandes.

Portanto, existem algumas perguntas:

  1. Devo resolver as dependências circulares usando a injeção de dependência? Nesse caso, suponho que a restrição deve receber a tabela do proprietário (e opcionalmente a referenciada) em seu construtor. Mas como eu poderia passar por cima da lista de restrições para uma única tabela?
  2. Se a classe Table armazena o estado de si mesmo (por exemplo, nome da tabela, comentário da tabela etc.) e os links para as restrições, essas são uma ou duas "responsabilidades", pensando no princípio de responsabilidade única?
  3. Caso o caso 2. esteja certo, devo apenas criar uma nova classe na camada lógica de negócios que gerencia os links? Nesse caso, 1. obviamente não seria mais relevante.
  4. Os métodos "createStatement" devem fazer parte das classes Tabela / Restrição ou devo removê-los também? Se sim, para onde? Uma classe de gerente por cada classe de armazenamento de dados (por exemplo, tabela, restrição, ...)? Ou melhor, criar uma classe de gerente por link (semelhante a 3.)?

Sempre que tento responder a uma dessas perguntas, me vejo correndo em círculos em algum lugar.

Obviamente, o problema fica muito mais complexo se você incluir colunas, índices e assim por diante, mas se vocês me ajudarem com a coisa simples Tabela / Restrição, talvez eu possa resolver o resto por conta própria.

Tim Meyer
fonte
3
Qual idioma você está usando? Você poderia postar pelo menos algum código esqueleto? É muito difícil discutir a qualidade do código e possíveis refatorações sem ver o código real.
Péter Török
Eu estou usando C ++, mas eu estava tentando mantê-lo fora da discussão como você poderia ter este problema em qualquer idioma
Tim Meyer
Sim, mas a aplicação de padrões e refatorações depende da linguagem. Por exemplo, @ back2dos sugeriu AOP em sua resposta abaixo, o que obviamente não se aplica ao C ++.
Péter Török
Consulte programmers.stackexchange.com/questions/155852/… para obter mais informações sobre os princípios do SOLID
LCJ

Respostas:

8

Você pode começar de um ponto de vista diferente para aplicar o "Princípio de responsabilidade única" aqui. O que você nos mostrou é (mais ou menos) apenas o modelo de dados do seu aplicativo. O SRP aqui significa: verifique se o seu modelo de dados é responsável apenas por manter os dados - nem menos, nem mais.

Portanto, quando você estiver lendo seu arquivo XML, crie um modelo de dados e escreva SQL, o que você não deve fazer é implementar algo em sua Tableclasse que seja XML ou SQL específico. Você deseja que seu fluxo de dados fique assim:

[XML] -> ("Read XML") -> [Data model of DB definition] -> ("Write SQL") -> [SQL]

Portanto, o único local em que o código específico XML deve ser colocado é uma classe chamada, por exemplo Read_XML,. O único local para código específico do SQL deve ser uma classe como Write_SQL. É claro que talvez você divida essas 2 tarefas em mais subtarefas (e divida suas classes em várias classes de gerenciador), mas seu "modelo de dados" não deve assumir nenhuma responsabilidade dessa camada. Portanto, não inclua a createStatementem nenhuma das suas classes de modelo de dados, pois isso dá ao seu modelo de responsabilidade a responsabilidade pelo SQL.

Não vejo nenhum problema ao descrever que uma tabela é responsável por armazenar todas as suas partes (nome, colunas, comentários, restrições ...), essa é a ideia por trás de um modelo de dados. Mas você descreveu "Tabela" também é responsável pelo gerenciamento de memória de algumas de suas partes. Esse é um problema específico do C ++, que você não enfrentaria tão facilmente em linguagens como Java ou C #. A maneira C ++ de se livrar dessas responsabilidades é usar ponteiros inteligentes, delegando propriedade a uma camada diferente (por exemplo, a biblioteca de reforço ou a sua própria camada de ponteiros "inteligentes"). Mas cuidado, suas dependências cíclicas podem "irritar" algumas implementações de ponteiros inteligentes.

Algo mais sobre o SOLID: aqui está um bom artigo

http://cre8ivethought.com/blog/2011/08/23/software-development-is-not-a-jenga-game

explicando o SOLID por um pequeno exemplo. Vamos tentar aplicar isso ao seu caso:

  • você precisará não apenas de classes Read_XMLe Write_SQL, mas também de uma terceira classe que gerencia a interação dessas duas classes. Vamos chamá-lo de ConversionManager.

  • Aplicação do princípio DI poderia significar aqui: ConversionManager não deve criar instâncias de Read_XMLe Write_SQLpor si mesmo. Em vez disso, esses objetos podem ser injetados através do construtor. E o construtor deve ter uma assinatura como esta

    ConversionManager(IDataModelReader reader, IDataModelWriter writer)

onde IDataModelReaderé uma interface da qual Read_XMLherda, e IDataModelWritero mesmo para Write_SQL. Isso ConversionManagerabre as extensões (você facilmente fornece leitores ou gravadores diferentes) sem precisar alterá-las - por isso, temos um exemplo para o princípio Aberto / Fechado. Pense nisso: o que você precisará alterar quando desejar oferecer suporte a outro fornecedor de banco de dados - idealmente, não precisará alterar nada no modelo de dados - basta fornecer outro gravador de SQL.

Doc Brown
fonte
Embora esse seja um exercício bastante razoável do SOLID, observe que ele viola a "velha escola Kay / Holub OOP" ao exigir getters e setters para um modelo de dados bastante anêmico. Isso também me lembra o infame discurso de Steve Yegge .
user949300
2

Bem, você deve aplicar o S do SOLID neste caso.

Uma tabela contém todas as restrições definidas nela. Uma restrição contém todas as tabelas às quais faz referência. Modelo puro e simples.

O que você mantém nisso é a capacidade de realizar pesquisas inversas, ou seja, descobrir por quais restrições alguma tabela é referenciada.
Então, o que você realmente deseja é um serviço de indexação. Essa é uma tarefa completamente diferente e, portanto, deve ser realizada por um objeto diferente.

Para dividi-lo em uma versão muito simplificada:

class Table {
      void addConstraint(Constraint constraint) { ... }
      bool removeConstraint(Constraint constraint) { ... }
      Iterator<Constraint> getConstraints() { ... }
}
class Constraint {
      //actually I am not so sure these two should be exposed directly at all
      void addReference(Table to) { ... }
      bool removeReference(Table to) { ... }
      Iterator<Table> getReferencedTables() { ... }
}
class Database {
      void addTable(Table table) { ... }
      bool removeTable(Table table) { ... }
      Iterator<Table> getTables() { ... }
}
class Index {
      Iterator<Constraint> getConstraintsReferencing(Table target) { ... }
}

Quanto à implementação do índice, existem três maneiras de seguir:

  • o getContraintsReferencingmétodo poderia realmente rastrear o todo Databasepara Tableinstâncias e rastrear seus Constraints para obter o resultado. Dependendo do custo e da frequência com que você precisa, pode ser uma opção.
  • também poderia usar um cache. Se seu modelo de banco de dados puder mudar uma vez definido, você poderá manter o cache disparando sinais dos respectivos Tablee Constraintinstâncias, quando eles mudarem. Uma solução um pouco mais simples seria Indexcriar um "índice de captura instantânea" do todo Databasepara trabalhar, que você descartaria. Obviamente, isso só é possível se o seu aplicativo fizer uma grande distinção entre "tempo de modelagem" e "tempo de consulta". Se é bem provável que os dois sejam executados ao mesmo tempo, isso não é viável.
  • Outra opção seria usar o AOP para interceptar todas as chamadas de criação e manter o índice de acordo.
back2dos
fonte
Resposta muito detalhada, eu gosto da sua solução até agora! O que você pensaria se eu executasse DI para a classe Table, fornecendo uma lista de restrições durante a construção? De qualquer maneira, tenho uma classe TableParser, que poderia funcionar como uma fábrica ou trabalhar em conjunto com uma fábrica nesse caso.
Tim Meyer
@ Tim Meyer: DI não é necessariamente injeção de construtor. A DI também pode ser feita por funções membro. Se a Tabela deve obter todas as suas peças através do construtor, depende se você deseja que essas peças sejam adicionadas apenas no momento da construção e nunca sejam alteradas posteriormente, ou se você deseja criar uma tabela passo a passo. Essa deve ser a base da sua decisão de design.
Doc Brown
1

A cura para as dependências circulares é prometer que você nunca as criará. Acho que a codificação do teste primeiro é um forte impedimento.

De qualquer forma, dependências circulares sempre podem ser desfeitas introduzindo uma classe base abstrata. Isso é típico para representações gráficas. Aqui, as tabelas são nós e as restrições de chave estrangeira são arestas. Portanto, crie uma classe abstrata da tabela e uma classe abstrata da restrição e talvez uma classe abstrata da coluna. Todas as implementações podem depender das classes abstratas. Essa pode não ser a melhor representação possível, mas é uma melhoria em relação às classes mutuamente acopladas.

Mas, como você suspeita, a melhor solução para esse problema pode não exigir nenhum rastreamento dos relacionamentos dos objetos. Se você deseja converter apenas XML para SQL, não precisa de uma representação na memória do gráfico de restrição. O gráfico de restrição seria bom se você quisesse executar algoritmos de gráfico, mas você não mencionou isso, então eu assumirei que não é um requisito. Você só precisa de uma lista de tabelas e uma lista de restrições e um visitante para cada dialeto SQL que deseja oferecer suporte. Gere as tabelas e gere as restrições externas às tabelas. Até que os requisitos mudassem, eu não teria nenhum problema em acoplar o gerador SQL ao XML DOM. Economize amanhã para amanhã.

Kevin Cline
fonte
É aqui que "(na verdade, muito mais que isso, mas vamos simplificar)" entra em jogo. Por exemplo, há casos em que preciso excluir uma tabela, portanto, preciso verificar se há alguma restrição referente a essa tabela.
Tim Meyer