Ridurre la complessità di una classe


10

Ho cercato alcune risposte e cercato su Google, ma non sono riuscito a trovare nulla di utile (ovvero che non avrebbe effetti collaterali imbarazzanti).

Il mio problema, in astratto, è che ho un oggetto e ho bisogno di eseguire una lunga sequenza di operazioni su di esso; Lo considero una sorta di catena di montaggio, come costruire un'auto.

Credo che questi oggetti si chiamerebbero Method Method .

Quindi in questo esempio ad un certo punto avrei un CarWithoutUpholstery su cui avrei quindi bisogno di eseguire installBackSeat, installFrontSeat, installWoodenInserts (le operazioni non interferiscono l'una con l'altra e potrebbero anche essere eseguite in parallelo). Queste operazioni vengono eseguite da CarWithoutUpholstery.worker () e producono un nuovo oggetto che sarebbe un CarWithUpholstery, sul quale verrei eseguito forse cleanInsides (), verifichiamo NoUpholsteryDefects () e così via.

Le operazioni in una sola fase sono già indipendenti, ovvero sto già affrontando un sottoinsieme di esse che può essere eseguito in qualsiasi ordine (i sedili anteriori e posteriori possono essere installati in qualsiasi ordine).

La mia logica attualmente utilizza Reflection per semplicità di implementazione.

Cioè, una volta che ho un CarWithoutUpholstery, l'oggetto controlla se stesso per i metodi chiamati performSomething (). A quel punto esegue tutti questi metodi:

myObject.perform001SomeOperation();
myObject.perform002SomeOtherOperation();
...

controllando errori e cose. Mentre l'ordine di funzionamento non è importante, ho assegnato un ordine lessicografico nel caso in cui scoprissi che un ordine è importante dopo tutto. Ciò contraddice YAGNI , ma è costato pochissimo - un semplice ordinamento () - e potrebbe salvare un enorme metodo di ridenominazione (o introdurre qualche altro metodo per eseguire test, ad esempio una serie di metodi) lungo la linea.

Un esempio diverso

Diciamo che invece di costruire un'auto devo compilare un rapporto della polizia segreta su qualcuno e inviarlo al mio Evil Overlord . Il mio oggetto finale sarà un ReadyReport. Per costruirlo comincio raccogliendo informazioni di base (nome, cognome, coniuge ...). Questa è la mia fase A. A seconda che ci sia un coniuge o meno, potrei quindi procedere alle fasi B1 o B2 e raccogliere dati sulla sessualità su una o due persone. Questo è composto da diverse domande a diversi Minion malvagi che controllano la vita notturna, le videocamere di strada, le ricevute di vendita dei sex shop e quant'altro. E così via e così via.

Se la vittima non ha famiglia, non entrerò nemmeno nella fase GetInformationAboutFamily, ma se lo faccio, allora è irrilevante se prendo di mira prima il padre o la madre o i fratelli (se presenti). Ma non posso farlo se non ho eseguito FamilyStatusCheck, che quindi appartiene a una fase precedente.

Funziona tutto meravigliosamente ...

  • se ho bisogno di un'operazione aggiuntiva, devo solo aggiungere un metodo privato,
  • se l'operazione è comune a più fasi posso averla ereditata da una superclasse,
  • le operazioni sono semplici e indipendenti. Nessun valore da un'operazione è mai richiesto da tutti gli altri (operazioni che non vengono eseguiti in una fase diversa),
  • gli oggetti lungo la linea non devono eseguire molti test poiché non potrebbero nemmeno esistere se i loro oggetti creatore non avessero verificato quelle condizioni in primo luogo. Vale a dire, quando si posizionano gli inserti nella dashboard, si pulisce la dashboard e si verifica la dashboard, non è necessario verificare che una dashboard sia effettivamente presente .
  • consente un facile test. Posso facilmente deridere un oggetto parziale ed eseguire qualsiasi metodo su di esso, e tutte le operazioni sono scatole nere deterministiche.

...ma...

Il problema è sorto quando ho aggiunto un'ultima operazione in uno dei miei oggetti metodo, che ha causato il superamento di un indice di complessità obbligatorio da parte del modulo complessivo ("meno di N metodi privati").

Ho già preso la questione al piano di sopra e suggerito che in questo caso, la ricchezza di metodi privati ​​non è una rivelazione del disastro. La complessità è lì, ma è lì perché l'operazione è complessa, e in realtà non è poi così complessa - è solo lunga .

Usando l'esempio di Evil Overlord, il mio problema è che l'Evil Overlord (aka Lui che non sarà negato ) dopo aver richiesto tutte le informazioni dietetiche, i miei Minion dietetici mi hanno detto che devo interrogare ristoranti, angoli cottura, venditori ambulanti, venditori ambulanti senza licenza, serra proprietari ecc., e il Male (sub) Overlord - noto come Colui che non verrà negato - lamentandosi del fatto che sto eseguendo troppe query nella fase GetDietaryInformation.

Nota : sono consapevole che da diversi punti di vista questo non è affatto un problema (ignorando possibili problemi di prestazioni, ecc.). Tutto ciò che sta accadendo è che una metrica specifica è infelice e c'è una giustificazione per questo.

Quello che penso di poter fare

A parte la prima, tutte queste opzioni sono fattibili e, credo, difendibili.

  • Ho verificato che posso essere subdolo e dichiarare metà dei miei metodi protected. Ma sfrutterei una debolezza nella procedura di test e, oltre a giustificarmi quando colto, non mi piace. Inoltre, è una misura di stopgap. Cosa succede se il numero di operazioni richieste raddoppia? Improbabile, ma allora?
  • Posso dividere arbitrariamente questa fase in AnnealedObjectAlpha, AnnealedObjectBravo e AnnealedObjectCharlie e avere un terzo delle operazioni eseguite in ogni fase. Ho l'impressione che ciò in realtà aggiunga complessità (N-1 più classi), senza alcun beneficio se non il superamento di un test. Ovviamente posso affermare che un CarWithFrontSeatsInstalled e un CarWithAllSeatsInstalled sono fasi logicamente successive . Il rischio che un metodo Bravo venga richiesto in seguito da Alpha è piccolo, e ancora più piccolo se lo gioco bene. Ma ancora.
  • Posso raggruppare diverse operazioni, da remoto simili, in una sola. performAllSeatsInstallation(). Questa è solo una misura di stopgap e aumenta la complessità della singola operazione. Se mai avessi bisogno di eseguire le operazioni A e B in un ordine diverso e le avessi imballate all'interno di E = (A + C) e F (B + D), dovrò separare E e F e mescolare il codice in giro .
  • Posso usare una serie di funzioni lambda e evitare accuratamente il controllo, ma lo trovo goffo. Questa è tuttavia la migliore alternativa finora. Si libererebbe della riflessione. I due problemi che ho è che probabilmente mi verrebbe chiesto di riscrivere tutti gli oggetti del metodo, non solo l'ipotetico CarWithEngineInstalled, e mentre sarebbe un'ottima sicurezza del lavoro, in realtà non fa molto appello; e che il controllo della copertura del codice ha problemi con lambdas (che sono risolvibili, ma comunque ).

Così...

  • Quale, secondo te, è la mia migliore opzione?
  • C'è un modo migliore che non ho preso in considerazione? ( forse è meglio che venga pulito e chieda direttamente che cos'è? )
  • Questo design è irrimediabilmente imperfetto, ed è meglio che ammetta la sconfitta e il fossato - questa architettura del tutto? Non va bene per la mia carriera, ma scrivere codice mal progettato sarebbe meglio a lungo termine?
  • La mia scelta attuale è davvero la One True Way e devo lottare per installare metriche (e / o strumentazione) di qualità migliore? Per quest'ultima opzione avrei bisogno di riferimenti ... Non posso semplicemente agitare la mia mano su @PHB mentre mormoro Queste non sono le metriche che stai cercando . Non importa quanto mi piacerebbe poterlo fare

3
In alternativa, è possibile ignorare l'avviso "superata la massima complessità".
Robert Harvey,

Se potessi lo farei. In qualche modo questa cosa delle metriche ha acquisito una qualità sacra tutta sua. Sto continuando a cercare di convincere il PTB ad accettarli come uno strumento utile, ma solo uno strumento. Potrei ancora riuscire ...
LSerni il

Le operazioni stanno effettivamente costruendo un oggetto o stai semplicemente usando la metafora della catena di montaggio perché condivide la proprietà specifica che menzioni (molte operazioni con un ordine di dipendenza parziale)? Il significato è che il primo suggerisce il modello del costruttore, mentre il secondo potrebbe suggerire qualche altro modello con maggiori dettagli compilati.
Outis

2
La tirannia delle metriche. Le metriche sono indicatori utili, ma non è utile applicarle ignorando il motivo per cui viene utilizzata quella metrica.
Jaydee,

2
Spiega alle persone al piano di sopra la differenza tra complessità essenziale e complessità accidentale. it.wikipedia.org/wiki/No_Silver_Bullet Se sei sicuro che la complessità che sta infrangendo la regola è essenziale, quindi se fai il refactoring per programmers.stackexchange.com/a/297414/51948 probabilmente stai pattinando sulla regola e diffondendo fuori dalla complessità. Se l'incapsulamento delle cose in fasi non è arbitrario (le fasi si riferiscono al problema) e la riprogettazione riduce il carico cognitivo per gli sviluppatori che mantengono il codice, ha senso farlo.
Fuhrmanator,

Risposte:


15

La lunga sequenza di operazioni sembra che dovrebbe essere la sua classe, che quindi ha bisogno di oggetti aggiuntivi per svolgere i suoi compiti. Sembra che tu sia vicino a questa soluzione. Questa lunga procedura può essere suddivisa in più passaggi o fasi. Ogni passaggio o fase potrebbe essere la propria classe che espone un metodo pubblico. Vorresti che ogni fase implementasse la stessa interfaccia. Quindi la tua catena di montaggio tiene traccia di un elenco di fasi.

Per sfruttare l'esempio dell'assemblaggio della tua auto, immagina la 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;
    }
}

Tralascerò le implementazioni di alcuni componenti, come IChassis, Seate Dashboardper brevità.

Non Carè responsabile della costruzione stessa. La catena di montaggio è, quindi incapsuliamola in una 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);
        }
    }
}

La distinzione importante qui è che CarAssembler ha un elenco di oggetti della fase di assemblaggio che implementano IAssemblyPhase. Ora stai trattando esplicitamente di metodi pubblici, il che significa che ogni fase può essere testata da sola, e il tuo strumento di qualità del codice è più felice perché non stai inserendo così tanto in una singola classe. Anche il processo di assemblaggio è semplicissimo. Basta passare sopra le fasi e chiamare Assemblepassando in macchina. Fatto. L' IAssemblyPhaseinterfaccia è anche semplicissima con un solo metodo pubblico che accetta un oggetto Car:

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

Ora abbiamo bisogno che le nostre classi concrete implementino questa interfaccia per ogni fase specifica:

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());
    }
}

Ogni fase individualmente può essere piuttosto semplice. Alcuni sono solo una fodera. Alcuni potrebbero semplicemente accecare aggiungendo quattro posti e un altro deve configurare il cruscotto prima di aggiungerlo all'auto. La complessità di questo lungo processo è suddivisa in più classi riutilizzabili che sono facilmente verificabili.

Anche se hai detto che l'ordine non ha importanza in questo momento, potrebbe in futuro.

Per finire, diamo un'occhiata al processo di assemblaggio di un'auto:

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

assemblyLine.Assemble(car);

È possibile eseguire la sottoclasse CarAssembler per assemblare un tipo specifico di auto o camion. Ogni fase è un'unità all'interno di un insieme più grande, facilitando il riutilizzo del codice.


Questo design è irrimediabilmente imperfetto, ed è meglio che ammetta la sconfitta e il fossato - questa architettura del tutto? Non va bene per la mia carriera, ma scrivere codice mal progettato sarebbe meglio a lungo termine?

Se decidere che quello che ho scritto ha bisogno di essere riscritto fosse un segno nero nella mia carriera, allora lavorerei come scavatrice di fossati in questo momento e non dovresti seguire nessuno dei miei consigli. :)

L'ingegneria del software è un processo eterno di apprendimento, applicazione e quindi riapprendimento. Solo perché decidi di riscrivere il tuo codice non significa che verrai licenziato. Se così fosse, non ci sarebbero ingegneri del software.

La mia scelta attuale è davvero la One True Way e devo lottare per installare metriche (e / o strumentazione) di qualità migliore?

Quello che hai descritto non è il modo vero Uno, tuttavia tenere presente che le metriche del codice sono anche non la vera via One. Le metriche del codice devono essere visualizzate come avvertenze. Possono indicare problemi di manutenzione in futuro. Sono linee guida, non leggi. Quando il tuo strumento di metriche del codice indica qualcosa, vorrei prima esaminare il codice per vedere se implementa correttamente i principi SOLID . Molte volte il refactoring del codice per renderlo più SOLID soddisferà gli strumenti di metrica del codice. A volte no. Devi prendere queste metriche caso per caso.


1
Sì, molto meglio di avere una divinità.
BЈовић,

Questo approccio funziona, ma soprattutto perché puoi ottenere gli elementi per l'auto senza parametri. E se avessi bisogno di passarli? Quindi puoi buttare tutto fuori dal finestrino e ripensare completamente la procedura, perché non hai davvero una fabbrica di automobili con 150 parametri, che è fuori di testa.
Andy,

@DavidPacker: anche se devi parametrizzare un sacco di cose, puoi incapsularle anche in una sorta di classe Config che passi nel costruttore del tuo oggetto "assembler". In questo esempio di auto, è possibile personalizzare il colore della vernice, il colore e i materiali degli interni, il pacchetto finiture, il motore e molto altro, ma non si avranno necessariamente 150 personalizzazioni. Probabilmente ne avrai una dozzina, il che si presta a un oggetto opzioni.
Greg Burghardt,

Ma solo l'aggiunta della configurazione sfiderebbe lo scopo del bellissimo ciclo for, il che è un vero peccato, perché dovresti analizzare la configurazione e assegnarla a metodi adeguati che in cambio ti darebbero oggetti adeguati. Quindi probabilmente finiresti con qualcosa di abbastanza simile a quello che era il design originale.
Andy,

Vedo. Invece di avere una classe e cinquanta metodi, avrei effettivamente cinquanta classi lean assembler e una classe orchestrante. Alla fine il risultato sembra lo stesso, anche in termini di prestazioni, ma il layout del codice è più pulito. Mi piace. Sarà un po 'più complicato aggiungere operazioni (dovrò impostarle nella loro classe, oltre a informare la classe di orchestrazione; non vedo una via d'uscita facile da questa necessità), ma ancora mi piace molto. Concedimi un paio di giorni per esplorare questo approccio.
LSerni,

1

Non so se questo è possibile per te, ma stavo pensando di utilizzare un approccio più orientato ai dati. L'idea è di catturare un'espressione (dichiarativa) di regole e vincoli, operazioni, dipendenze, stato, ecc ... Quindi le classi e il codice sono più generici, orientati a seguire le regole, inizializzando lo stato attuale delle istanze, eseguendo operazioni per cambia stato, utilizzando l'acquisizione dichiarativa di regole e cambiamenti di stato anziché utilizzare il codice in modo più diretto. Si potrebbero usare dichiarazioni di dati nel linguaggio di programmazione, o alcuni strumenti DSL, o un motore di regole o di logica.


Alcune operazioni vengono effettivamente implementate in questo modo (come elenchi di regole). Per continuare con l'esempio dell'auto, quando installo una scatola di fusibili, luci e spine, in realtà non uso tre metodi diversi, ma solo uno, che si adatta genericamente a un dispositivo elettrico a due pin, e prende l'elenco di tre elenchi di fusibili , luci e spine. La stragrande maggioranza degli altri metodi non è purtroppo facilmente suscettibile a questo approccio.
LSerni,

1

In qualche modo il problema mi ricorda le dipendenze. Vuoi un'auto in una configurazione specifica. Come Greg Burghardt, andrei con oggetti / classi per ogni passaggio / oggetto /….

Ogni passaggio dichiara ciò che aggiunge al mix (che cosa provides) (può essere più cose), ma anche ciò che richiede.

Quindi, definisci la configurazione finale richiesta da qualche parte e disponi di un algoritmo che esamina ciò di cui hai bisogno e decide quali passaggi devono essere eseguiti / quali parti devono essere aggiunte / quali documenti devono essere raccolti.

Gli algoritmi di risoluzione delle dipendenze non sono così difficili. Il caso più semplice è quello in cui ogni passaggio fornisce una cosa, e non ci sono due cose che forniscono la stessa cosa, e le dipendenze sono semplici (nessun 'o' nelle dipendenze). Per i casi più complessi: basta guardare uno strumento come debian apt o qualcosa del genere.

Infine, la costruzione della tua auto si riduce a quali possibili passaggi ci sono e lasciando che CarBuilder capisca i passaggi necessari.

Una cosa importante, tuttavia, è che devi trovare un modo per far sapere a CarBuilder tutti i passaggi. Prova a farlo usando un file di configurazione. L'aggiunta di un passaggio aggiuntivo richiederebbe solo un'aggiunta al file di configurazione. Se hai bisogno di logica, prova ad aggiungere un DSL nel file di configurazione. Se tutto il resto fallisce, sei bloccato a definirli nel codice.


0

Un paio di cose che menzionate spiccano come "cattive" per me

"La mia logica attualmente utilizza Reflection per semplicità di implementazione"

Non ci sono davvero scuse per questo. stai davvero usando il confronto delle stringhe dei nomi dei metodi per determinare cosa eseguire ?! se cambi l'ordine dovrai cambiare il nome del metodo ?!

"Le operazioni in un'unica fase sono già indipendenti"

Se sono veramente indipendenti l'uno dall'altro, ciò suggerisce che la tua classe ha assunto più di una responsabilità. Per me, entrambi i tuoi esempi sembrano prestarsi a una sorta di singolo

MyObject.AddComponent(IComponent component) 

approccio che ti consentirebbe di dividere la logica di ciascuna opposizione nella propria classe o classi.

In effetti immagino che non siano veramente indipendenti, immagino che ciascuno esamina lo stato dell'oggetto e lo modifica, o almeno esegue una sorta di validazione prima di iniziare.

In questo caso puoi:

  1. Butta OOP fuori dalla finestra e disponi di servizi che operano sui dati esposti nella tua classe, ad es.

    Service.AddDoorToCar (Car car)

  2. Avere una classe builder che costruisce il tuo oggetto assemblando prima i dati necessari e restituendo l'oggetto completo, ad es.

    Car = CarBuilder.WithDoor (). WithDoor (). WithWheels ();

(la logica withX può essere nuovamente suddivisa in subbuilder)

  1. Adotta l'approccio funzionale e passa le funzioni / i delgati in un metodo di modifica

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


Ewan disse: "Getta OOP fuori dalla finestra e disponi di servizi che operano sui dati esposti nella tua classe" --- In che modo questo non è orientato agli oggetti?
Greg Burghardt,

?? non è, è un'opzione non OO
Ewan

Stai usando oggetti, il che significa che è "orientato agli oggetti". Solo perché non puoi identificare un modello di programmazione per il tuo codice non significa che non sia orientato agli oggetti. I principi SOLID sono una buona regola da seguire. Se vengono implementati alcuni o tutti i principi SOLID, è orientato agli oggetti. In realtà, se stai usando oggetti e non stai semplicemente passando strutture a diverse procedure o metodi statici, è orientato agli oggetti. C'è una differenza tra "codice orientato agli oggetti" e "buon codice". È possibile scrivere codice orientato agli oggetti errato.
Greg Burghardt,

è "non OO" perché stai esponendo i dati come se fossero una struttura e operandoli in una classe separata come in una procedura
Ewan

tbh ho solo aggiunto il commento principale per provare a fermare l'inevitabile "non è OOP!" commenti
Ewan,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.