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:
- 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?
- 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?
- 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.
- 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.
fonte
Respostas:
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
Table
classe que seja XML ou SQL específico. Você deseja que seu fluxo de dados fique assim: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 comoWrite_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 acreateStatement
em 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_XML
eWrite_SQL
, mas também de uma terceira classe que gerencia a interação dessas duas classes. Vamos chamá-lo deConversionManager
.Aplicação do princípio DI poderia significar aqui: ConversionManager não deve criar instâncias de
Read_XML
eWrite_SQL
por si mesmo. Em vez disso, esses objetos podem ser injetados através do construtor. E o construtor deve ter uma assinatura como estaConversionManager(IDataModelReader reader, IDataModelWriter writer)
onde
IDataModelReader
é uma interface da qualRead_XML
herda, eIDataModelWriter
o mesmo paraWrite_SQL
. IssoConversionManager
abre 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.fonte
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:
Quanto à implementação do índice, existem três maneiras de seguir:
getContraintsReferencing
método poderia realmente rastrear o todoDatabase
paraTable
instâncias e rastrear seusConstraint
s para obter o resultado. Dependendo do custo e da frequência com que você precisa, pode ser uma opção.Table
eConstraint
instâncias, quando eles mudarem. Uma solução um pouco mais simples seriaIndex
criar um "índice de captura instantânea" do todoDatabase
para 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.fonte
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ã.
fonte