Reduzindo a complexidade de uma classe

10

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 .
  • 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
LSerni
fonte
3
Ou, você pode ignorar o aviso de "complexidade máxima excedida".
Robert Harvey
Se eu pudesse eu faria. De alguma forma, essa coisa de métricas adquiriu uma qualidade sagrada por si só. Continuo tentando convencer o PTB a aceitá-los como uma ferramenta útil, mas apenas uma ferramenta. Ainda posso ter sucesso ...
LSerni
As operações estão realmente construindo um objeto ou você está apenas usando a metáfora da linha de montagem porque compartilha a propriedade específica mencionada (muitas operações com uma ordem de dependência parcial)? O significado é que o primeiro sugere o padrão do construtor, enquanto o último pode sugerir algum outro padrão com mais detalhes preenchidos.
outis
2
A tirania das métricas. Métricas são indicadores úteis, mas reforçá-las e ignorar por que essa métrica é usada não é útil.
Jaydee
2
Explique às pessoas no andar de cima a diferença entre complexidade essencial e complexidade acidental. en.wikipedia.org/wiki/No_Silver_Bullet Se você tem certeza de que a complexidade que está quebrando a regra é essencial, então se você refatorar per programmers.stackexchange.com/a/297414/51948 você está possivelmente patinar em torno da regra e se espalhando a complexidade. Se encapsular coisas em fases não for arbitrário (as fases se relacionam com o problema) e o redesenho reduzir a carga cognitiva para os desenvolvedores que mantêm o código, faz sentido fazer isso.
Fuhrmanator

Respostas:

15

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 Carclasse:

public class Car
{
    public IChassis Chassis { get; private set; }
    public Dashboard { get; private set; }
    public IList<Seat> Seats { get; private set; }

    public Car()
    {
        Seats = new List<Seat>();
    }

    public void AddChassis(IChassis chassis)
    {
        Chassis = chassis;
    }

    public void AddDashboard(Dashboard dashboard)
    {
        Dashboard = dashboard;
    }
}

Vou deixar de fora as implementações de alguns dos componentes, como IChassis, Seate Dashboardpor uma questão de brevidade.

O Carnão é responsável por se construir. A linha de montagem é, então vamos resumir isso em uma classe:

public class CarAssembler
{
    protected List<IAssemblyPhase> Phases { get; private set; }

    public CarAssembler()
    {
        Phases = new List<IAssemblyPhase>()
        {
            new ChassisAssemblyPhase(),
            new DashboardAssemblyPhase(),
            new SeatAssemblyPhase()
        };
    }

    public void Assemble(Car car)
    {
        foreach (IAssemblyPhase phase in Phases)
        {
            phase.Assemble(car);
        }
    }
}

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 a Assemblepassagem no carro. Feito. A IAssemblyPhaseinterface também é simples, com apenas um único método público que pega um objeto Car:

public interface IAssemblyPhase
{
    void Assemble(Car car);
}

Agora precisamos de nossas classes concretas implementando essa interface para cada fase específica:

public class ChassisAssemblyPhase : IAssemblyPhase
{
    public void Assemble(Car car)
    {
        car.AddChassis(new UnibodyChassis());
    }
}

public class DashboardAssemblyPhase : IAssemblyPhase
{
    public void Assemble(Car car)
    {
        Dashboard dashboard = new Dashboard();

        dashboard.AddComponent(new Speedometer());
        dashboard.AddComponent(new FuelGuage());
        dashboard.Trim = DashboardTrimType.Aluminum;

        car.AddDashboard(dashboard);
    }
}

public class SeatAssemblyPhase : IAssemblyPhase
{
    public void Assemble(Car car)
    {
        car.Seats.Add(new Seat());
        car.Seats.Add(new Seat());
        car.Seats.Add(new Seat());
        car.Seats.Add(new Seat());
    }
}

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:

Car car = new Car();
CarAssembler assemblyLine = new CarAssembler();

assemblyLine.Assemble(car);

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.


Esse design é irremediavelmente falho, e é melhor eu admitir a derrota e a exclusão - essa arquitetura completamente? Não é bom para minha carreira, mas escrever código mal projetado seria melhor a longo prazo?

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.

A minha escolha atual é realmente a única maneira verdadeira e preciso lutar para instalar métricas (e / ou instrumentação) de melhor qualidade?

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.

Greg Burghardt
fonte
1
Sim, muito melhor do que ter uma classe divina.
BЈовић
Essa abordagem funciona, mas principalmente porque você pode obter os elementos para o carro sem parâmetros. E se você precisasse passar por eles? Então você pode jogar tudo isso pela janela e repensar completamente o procedimento, porque você não está realmente tendo uma fábrica de carros com 150 parâmetros, isso é loucura.
Andy
@ DavidPacker: Mesmo que você precise parametrizar um monte de coisas, você pode encapsular isso também em algum tipo de classe Config que você passa para o construtor do seu objeto "assembler". Neste exemplo de carro, a cor da pintura, a cor e os materiais internos, o pacote de acabamentos, o mecanismo e muito mais podem ser personalizados, mas você não terá necessariamente 150 personalizações. Você provavelmente terá uma dúzia, o que se presta a um objeto de opções.
Greg Burghardt
Mas apenas adicionar a configuração desafiaria o propósito do loop for bonito, o que é uma tremenda vergonha, porque você precisaria analisar a configuração e atribuí-la a métodos adequados que, em troca, forneceriam objetos adequados. Então você provavelmente acabaria com algo bastante parecido com o design original.
Andy
Eu vejo. Em vez de ter uma classe e cinquenta métodos, eu teria, na verdade, cinquenta classes lean de assembler e uma classe de orquestração. No final, o resultado parece o mesmo, também em termos de desempenho, mas o layout do código é mais limpo. Eu gosto disso. Será um pouco mais complicado adicionar operações (vou ter que configurá-las na classe delas, além de informar a classe de orquestração; não vejo uma maneira fácil de sair dessa necessidade), mas ainda gosto muito. Permita-me alguns dias para explorar essa abordagem.
precisa saber é o seguinte
1

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.

Erik Eidt
fonte
Algumas das operações são realmente implementadas dessa maneira (como listas de regras). Para acompanhar o exemplo do carro, ao instalar uma caixa de fusíveis, luzes e plugues, na verdade, não utilizo três métodos diferentes, mas apenas um, que genericamente encaixa coisas elétricas de dois pinos no lugar e leva a lista de três listas de fusíveis , luzes e plugues. Infelizmente, a grande maioria dos outros métodos não é facilmente acessível a essa abordagem.
precisa saber é o seguinte
1

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.

Sjoerd Job Postmus
fonte
0

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

MyObject.AddComponent(IComponent component) 

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:

  1. Jogue OOP pela janela e tenha serviços que operam com os dados expostos na sua classe, ou seja.

    Service.AddDoorToCar (automóvel)

  2. 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)

  1. Adote a abordagem funcional e passe funções / delgates para um método de modificação

    Car.Modify ((c) => c.doors ++);

Ewan
fonte
Ewan disse: "Jogue OOP pela janela e tenha serviços que operam com os dados expostos em sua classe" --- Como isso não é orientado a objetos?
Greg Burghardt 23/09
?? seu não, a sua opção OO não um
Ewan
Você está usando objetos, o que significa que é "orientado a objetos". Só porque você não pode identificar um padrão de programação para o seu código, não significa que ele não seja orientado a objetos. Os princípios do SOLID são uma boa regra a seguir. Se você implementou alguns ou todos os princípios do SOLID, eles são orientados a objetos. Realmente, se você estiver usando objetos e não apenas passando estruturas para diferentes procedimentos ou métodos estáticos, ele será orientado a objetos. Há uma diferença entre "código orientado a objeto" e "código bom". Você pode escrever código incorreto orientado a objetos.
Greg Burghardt 23/09
seu 'não OO' porque você está expondo os dados como se fosse um struct e operar sobre ele em uma classe separada como em um procedimento
Ewan
Eu apenas adicionei o comentário principal para tentar parar o inevitável "isso não é OOP!" comentários
Ewan