Eu olhei para algumas respostas e procurei no Google, mas não encontrei nada útil (isto é, que não teria efeitos colaterais estranhos).
Meu problema, em resumo, é que tenho um objeto e preciso executar uma longa sequência de operações nele; Eu penso nisso como uma espécie de linha de montagem, como construir um carro.
Eu acredito que esses objetos seriam chamados de objetos de método .
Portanto, neste exemplo, em algum momento, eu teria um CarWithoutUpholstery no qual precisaria executar installBackSeat, installFrontSeat, installWoodenInserts (as operações não interferem umas com as outras e podem até ser feitas em paralelo). Essas operações são executadas por CarWithoutUpholstery.worker () e produzem um novo objeto que seria um CarWithUpholstery, no qual eu executaria talvez cleanInsides (), confirmNoUpholsteryDefects () e assim por diante.
As operações em uma única fase já são independentes, ou seja, eu já estou lidando com um subconjunto delas que pode ser executado em qualquer ordem (os bancos dianteiro e traseiro podem ser instalados em qualquer ordem).
Minha lógica atualmente usa o Reflection para simplificar a implementação.
Ou seja, uma vez que eu tenho um CarWithoutUpholstery, o objeto se inspeciona para métodos chamados performSomething (). Nesse ponto, ele executa todos esses métodos:
myObject.perform001SomeOperation();
myObject.perform002SomeOtherOperation();
...
enquanto verifica erros e outras coisas. Embora a ordem de operação não seja importante, atribuí uma ordem lexicográfica, caso eu descubra que alguma ordem é importante, afinal. Isso contradiz o YAGNI , mas custou muito pouco - um tipo simples () - e poderia salvar um método massivo renomeando (ou introduzindo algum outro método para executar testes, por exemplo, uma variedade de métodos) mais adiante.
Um exemplo diferente
Digamos que, em vez de construir um carro, eu tenha que compilar um relatório da Polícia Secreta sobre alguém e enviá-lo ao meu Overlord do Mal . Meu objeto final será um ReadyReport. Para construí-lo, começo reunindo informações básicas (nome, sobrenome, cônjuge ...). Esta é minha Fase A. Dependendo se existe ou não um cônjuge, talvez eu precise seguir para as fases B1 ou B2 e reunir dados de sexualidade de uma ou duas pessoas. Isso é feito de várias consultas diferentes para diferentes Minions do Mal controlando a vida noturna, câmeras de rua, recibos de vendas de sex shops e outras coisas. E assim por diante.
Se a vítima não tiver família, nem entrarei na fase GetInformationAboutFamily, mas, se tiver, será irrelevante se eu visar primeiro o pai, a mãe ou os irmãos (se houver). Mas não posso fazer isso se não tiver realizado um FamilyStatusCheck, que, portanto, pertence a uma fase anterior.
Tudo funciona maravilhosamente ...
- se precisar de alguma operação adicional, preciso apenas adicionar um método privado,
- se a operação é comum a várias fases, posso herdá-la de uma superclasse,
- operações são simples e independentes. Nenhum valor de uma operação é sempre exigido por qualquer um dos outros (operações que não são realizadas numa fase diferente),
- os objetos abaixo da linha não precisam executar muitos testes, pois nem sequer poderiam existir se seus objetos criadores não tivessem verificado essas condições em primeiro lugar. Ou seja, ao colocar inserções no painel, limpar o painel e verificar o painel, não preciso verificar se um painel está realmente lá .
- permite testes fáceis. Posso facilmente zombar de um objeto parcial e executar qualquer método nele, e todas as operações são caixas-pretas determinísticas.
...mas...
O problema surgiu quando adicionei uma última operação em um dos meus objetos de método, o que fez com que o módulo geral excedesse um índice de complexidade obrigatório ("menos que N métodos privados").
Eu já levei o assunto para cima e sugeri que, neste caso, a riqueza de métodos privados não é um indicador de desastre. A complexidade é lá, mas está lá porque a operação é complexa, e realmente não é tudo que o complexo - é apenas longa .
Usando o exemplo do Overlord Maligno, meu problema é que o Overlord Maligno (também conhecido como Quem Não Deve Ser Negado ) solicitou todas as informações sobre a dieta, meus Minions Dietéticos me dizendo que eu preciso consultar restaurantes, cozinhas americanas, vendedores ambulantes, vendedores ambulantes sem licença, estufa proprietários etc., e o (sub) Overlord do Mal - conhecido como Aquele Que Também Não Deve Ser Negado - reclamando que estou realizando muitas consultas na fase GetDietaryInformation.
Nota : Estou ciente de que, sob vários pontos de vista, isso não é um problema (ignorando possíveis problemas de desempenho etc.). Tudo o que está acontecendo é que uma métrica específica é infeliz e há justificativa para isso.
O que eu acho que poderia fazer
Além da primeira, todas essas opções são factíveis e, acho, defensáveis.
- Eu verifiquei que posso ser sorrateiro e declarar metade dos meus métodos
protected
. Mas eu estaria explorando uma fraqueza no procedimento de teste e, além de me justificar quando pego, não gosto disso. Além disso, é uma medida paliativa. E se o número de operações necessárias dobrar? Improvável, mas e então? - Eu posso dividir arbitrariamente essa fase em AnnealedObjectAlpha, AnnealedObjectBravo e AnnealedObjectCharlie, e ter um terço das operações sendo executadas em cada estágio. Tenho a impressão de que isso realmente adiciona complexidade (mais N-1 classes), sem nenhum benefício, exceto para passar no teste. É claro que posso sustentar que CarWithFrontSeatsInstalled e CarWithAllSeatsInstalled são estágios logicamente sucessivos . O risco de um método Bravo ser posteriormente requerido pelo Alpha é pequeno e ainda menor se eu o jogar bem. Mas ainda.
- Posso agrupar operações diferentes, remotamente semelhantes, em uma única.
performAllSeatsInstallation()
. Essa é apenas uma medida paliativa e aumenta a complexidade da operação única. Se eu precisar executar as operações A e B em uma ordem diferente, e eu as empacotar dentro de E = (A + C) e F (B + D), precisarei desmembrar E e F e embaralhar o código . - Posso usar uma matriz de funções lambda e evitar a verificação completamente, mas acho isso desajeitado. Esta é, no entanto, a melhor alternativa até agora. Livraria-se da reflexão. Os dois problemas que tenho é que provavelmente seria solicitado a reescrever todos os objetos de método, não apenas os hipotéticos
CarWithEngineInstalled
, e embora isso fosse uma segurança de trabalho muito boa, ele realmente não é tão atraente; e que o verificador de cobertura de código tem problemas com lambdas (que são solucionáveis, mas ainda assim ).
Então...
- Qual, você acha, é a minha melhor opção?
- Existe uma maneira melhor de não considerar? ( talvez seja melhor eu ficar limpo e perguntar diretamente o que é isso? )
- Esse design é irremediavelmente falho, e é melhor eu admitir derrota e abandonar - essa arquitetura completamente? Não é bom para minha carreira, mas escrever código mal projetado seria melhor a longo prazo?
- Minha escolha atual é realmente a única maneira verdadeira e preciso lutar para instalar métricas (e / ou instrumentação) de melhor qualidade? Para esta última opção, eu precisaria de referências ... Não posso apenas acenar com a mão no @PHB enquanto murmuro Essas não são as métricas que você está procurando . Não importa o quanto eu gostaria de poder
Respostas:
A longa sequência de operações parece ser sua própria classe, que precisa de objetos adicionais para desempenhar suas funções. Parece que você está perto desta solução. Este longo procedimento pode ser dividido em várias etapas ou fases. Cada etapa ou fase pode ser sua própria classe que expõe um método público. Você deseja que cada fase implemente a mesma interface. Em seguida, sua linha de montagem controla uma lista de fases.
Para criar um exemplo de montagem de carro, imagine a
Car
classe:Vou deixar de fora as implementações de alguns dos componentes, como
IChassis
,Seat
eDashboard
por uma questão de brevidade.O
Car
não é responsável por se construir. A linha de montagem é, então vamos resumir isso em uma classe:A distinção importante aqui é que o CarAssembler possui uma lista de objetos da fase de montagem que são implementados
IAssemblyPhase
. Agora você está lidando explicitamente com métodos públicos, o que significa que cada fase pode ser testada por si mesma, e sua ferramenta de qualidade de código é mais feliz porque você não está inserindo muito em uma única classe. O processo de montagem também é simples demais. Basta percorrer as fases e chamar aAssemble
passagem no carro. Feito. AIAssemblyPhase
interface também é simples, com apenas um único método público que pega um objeto Car:Agora precisamos de nossas classes concretas implementando essa interface para cada fase específica:
Cada fase individualmente pode ser bastante simples. Alguns são apenas um forros. Alguns podem ser ofuscantes ao adicionar quatro assentos, e outro deve configurar o painel antes de adicioná-lo ao carro. A complexidade desse longo processo é dividida em várias classes reutilizáveis e facilmente testáveis.
Mesmo que você tenha dito que o pedido não importa no momento, isso pode acontecer no futuro.
Para finalizar, vejamos o processo de montagem de um carro:
Você pode subclassificar o CarAssembler para montar um tipo específico de carro ou caminhão. Cada fase é uma unidade dentro de um todo maior, facilitando a reutilização de código.
Se decidir que o que escrevi precisa ser reescrito é uma marca negra em minha carreira, eu estaria trabalhando como escavador de valas agora e você não deve seguir nenhum dos meus conselhos. :)
A engenharia de software é um processo eterno de aprendizado, aplicação e reaprendizagem. Só porque você decide reescrever seu código não significa que você será demitido. Se fosse esse o caso, não haveria engenheiros de software.
O que você descreveu não é a única maneira verdadeira, no entanto, lembre-se de que as métricas de código também não são a única maneira verdadeira. As métricas de código devem ser vistas como avisos. Eles podem apontar para problemas de manutenção no futuro. São diretrizes, não leis. Quando sua ferramenta de métricas de código aponta algo, primeiro investigaria o código para verificar se ele implementa adequadamente os princípios do SOLID . Muitas vezes, a refatoração do código para torná-lo mais SOLID satisfará as ferramentas de métricas de código. Às vezes não. Você deve tomar essas métricas caso a caso.
fonte
Não sei se isso é possível para você, mas estaria pensando em usar uma abordagem mais orientada a dados. A idéia é que você capture alguma expressão (declarativa) de regras e restrições, operações, dependências, estado, etc ... Então suas classes e códigos são mais genéricos, orientados para seguir as regras, inicializando o estado atual das instâncias, executando operações para change state, usando a captura declarativa de regras e alterações de estado, em vez de usar o código mais diretamente. Pode-se usar declarações de dados na linguagem de programação ou em algumas ferramentas DSL ou em um mecanismo de regras ou lógica.
fonte
De alguma forma, o problema me lembra de dependências. Você quer um carro em uma configuração específica. Como Greg Burghardt, eu usaria objetos / classes para cada etapa / item /….
Cada etapa declara o que adiciona à mistura (o que
provides
) (pode ser várias coisas), mas também o que é necessário.Em seguida, você define a configuração final necessária em algum lugar e possui um algoritmo que analisa o que você precisa e decide quais etapas precisam ser executadas / quais partes precisam ser adicionadas / quais documentos precisam ser reunidos.
Os algoritmos de resolução de dependência não são tão difíceis. O caso mais simples é o de que cada etapa fornece uma coisa, e duas não fornecem a mesma coisa, e as dependências são simples (não 'ou' nas dependências). Para os casos mais complexos: basta olhar para uma ferramenta como o debian apt ou algo assim.
Por fim, a construção do seu carro reduz as etapas possíveis e permite que o CarBuilder descubra as etapas necessárias.
Uma coisa importante, porém, é que você deve encontrar uma maneira de informar o CarBuilder sobre todas as etapas. Tente fazer isso usando um arquivo de configuração. Adicionar uma etapa extra precisaria apenas de uma adição ao arquivo de configuração. Se você precisar de lógica, tente adicionar uma DSL no arquivo de configuração. Se tudo mais falhar, você está preso ao defini-los no código.
fonte
Algumas coisas que você menciona se destacam como 'ruins' para mim
"Minha lógica atualmente usa o Reflection para simplificar a implementação"
Realmente não há desculpa para isso. você realmente está usando comparação de strings de nomes de métodos para determinar o que executar ?! se você mudar a ordem, terá que mudar o nome do método ?!
"As operações em uma única fase já são independentes"
Se eles são verdadeiramente independentes um do outro, sugere que sua classe assumiu mais de uma responsabilidade. Para mim, seus dois exemplos parecem se prestar a algum tipo de
abordagem que permitiria dividir a lógica de cada operação em sua própria classe ou classes.
Na verdade, imagino que eles não sejam verdadeiramente independentes, imagino que cada um examina o estado do objeto e o modifica, ou pelo menos realiza algum tipo de validação antes de começar.
Nesse caso, você pode:
Jogue OOP pela janela e tenha serviços que operam com os dados expostos na sua classe, ou seja.
Service.AddDoorToCar (automóvel)
Tenha uma classe de construtor que constrói seu objeto reunindo os dados necessários primeiro e retornando o objeto concluído, ou seja.
Car = CarBuilder.WithDoor (). WithDoor (). WithWheels ();
(a lógica withX pode ser dividida novamente em sub-construtores)
Adote a abordagem funcional e passe funções / delgates para um método de modificação
Car.Modify ((c) => c.doors ++);
fonte