Object Design & Cohesion - Problema e potenziale refactoring

2

Riepilogo

Ho cercato di capire la coesione di alcune funzionalità nella nostra base di codice. Ho affrontato questo design in modi diversi, e ultimamente sono convinto di aver preso l'approccio sbagliato, poiché ho applicato in modo errato il Principio di Responsabilità Unica.

Problema

Il dominio del problema è un pozzo, altrimenti noto come un buco rotondo nel terreno. Il codice attuale è simile a questo ... Ho un set di dati in un oggetto solo dati ...

Nota: questo non è il codice completo, ma solo i nomi dei metodi per evitare problemi ...

public class WellData
{
    public string Name { get; set; }
    public ICollection<SurveyPoint> SurveyPoints { get; set; }
    public ICollection<GeometryItem> GeometryItems { get; set; }
    public ICollection<TemperaturePoint> WellTemperature { get; set; }
    public ICollection<FluidPoint> WellFluids { get; set; }
}

Ho rotto la funzionalità che opera su questi dati in diverse piccole classi. Come da sotto ...

public class ReferenceWellSurvey
{
   ICollection<SurveyPointCalculate> _calculatedSurveyPoints;
   double GetTvdAtDepth(double depth) {}
   double GetAzimuthAtDepthRad(double depth) {}
   double GetInclinationAtDepthRad(double depth){}
   double GetTortuosityPeriodAtDepth(double  depth ) {}
   double GetTortuosityAmplitudeAtDepth(double depth ) {}
}

public class ReferenceWellGeometry
{
   ICollection<GeometryItem> _geometryItems;
   double GetFrictionAtDepth(double depth) {}
   double GetHoleIdAtDepth(double depth) {}
}

public class ReferenceWellTemperature
{
   ICollection<TemperaturePoint> _wellTemperatures;
   double GetTemperatureAtDepth(double depth) {}
   double GetSurfaceTemperature(double depth) {}
}
Etc.

Mi sono reso conto ultimamente guardando il nostro codice che devo rimettere insieme tutto questo per renderlo utilizzabile dove ne ho bisogno nella logica dell'applicazione. Qualcosa come ....

public class ReferenceWellData
{
private IReferenceWellGeometry _refWellGeometry;
private IReferenceWellTemperature _refWellGeometry;
          private IReferenceWell _refWellGeometry;
          private IReferenceFluid _refWellFluid;
       //Proxy functions from each class to glue it all back together…
}

Alcune cose che mi hanno portato all'attuale situazione. La classe data well contiene diverse raccolte di informazioni e se tutto è in una classe la maggior parte delle funzioni funzionerà solo su un piccolo sottoinsieme di dati. All'epoca sentivo che avrebbe creato una bassa coesione, ora alcuni mesi dopo, sento che queste cose in realtà appartengono tutte insieme. Inoltre, nel mio codice sono presenti diverse classi che devono chiamare solo un metodo della classe ReferenceWellSurvey, GetTvdAtDepth (). In realtà quello che ho fatto non ha risolto questo problema.

Dopo aver riflettuto su questo argomento e dopo aver letto questo articolo di Wikipedia link , sto iniziando a vedere che ho commesso alcuni peccati di programmazione.

Modello di dominio anemico

• Bassa coesione

• Costruttore over injection in codice dove cerco di incollare le cose insieme

• Fornire una classe che dipende dall'accesso GetTvdAtDepth ad altri metodi di cui non ha bisogno, violando il principio di segregazione dell'interfaccia.

Soluzione

Sto cercando quel disegno Goldilox che non dà troppa responsabilità a nessuna classe (No God Objects), pur mantenendo un buon grado di coesione.
Le mie attuali riflessioni su come risolvere questo problema sono le seguenti. Spostare la funzionalità che opera sui dati del pozzo nella stessa classe. Questo dovrebbe aiutare a rendere questa funzionalità più vicina alla coesione comunicazionale / informativa.

public class WellData : IWellData 
{
    public string Name { get; set; }
    public ICollection<SurveyPoint> SurveyPoints { get; set; }
    public ICollection<GeometryItem> GeometryItems { get; set; }
    public ICollection<TemperaturePoint> WellTemperature { get; set; }
    public ICollection<FluidPoint> WellFluids { get; set; }

   // Operates on SurveyPoint collection 
   double GetTvdAtDepth(double depth) {}
   double GetAzimuthAtDepthRad(double depth) {}
   double GetInclinationAtDepthRad(double depth){}
   double GetTortuosityPeriodAtDepth(double  depth ) {}
   double GetTortuosityAmplitudeAtDepth(double depth ) {}
   //Operates on WellGeometry collection 
   double GetFrictionAtDepth(double depth) {}
   double GetHoleIdAtDepth(double depth) {}
   //Operates on WellTemperature collection
   double GetTemperatureAtDepth(double depth) {}
   double GetSurfaceTemperature(double depth) {}
}

Successivamente per affrontare il problema di diverse classi che necessitano di una sola funzione, potrei trasformare questa funzione in un'interfaccia separata IGetTvdAtDepth. In questo modo, le mie classi che hanno bisogno di quella funzionalità possono dipendere solo da questa interfaccia, che posso ereditare nella mia interfaccia WellData, e implementarla lì, ma non legare le altre mie classi a tutte le funzionalità della mia classe WellData.

Riflessioni su questo approccio rispetto all'approccio originale? Una cosa che mi preoccupa di fare le cose è che nella mia API sto usando un'iniezione bastard per soddisfare le dipendenze richieste di questa classe ... In questo momento ho tre dipendenze richieste, un calcolatore Survey e una classe speciale che fa riferimento ai dati a diverse profondità misurate. Mi piacerebbe andare via da questo, ma questo sembra puntare a far sì che i servizi facciano più del lifting ...

    
posta GetFuzzy 12.07.2014 - 00:02
fonte

6 risposte

3

Inizia con la definizione profonda delle tue classi invece di un'interfaccia di ostruzione di Uber

Penso che definisca le mie strutture dati "dominio del pozzo petrolifero".

L'idea è di scoprire i bit che hanno un significato indipendente / discreto. Man mano che le definizioni di classe si evolvono, estrai le parti che possono di per sé essere classi. Quindi composito secondo necessità in seguito. Modifica sempre le classi man mano che le cose si sviluppano. Questo approccio dovrebbe permetterci di evolvere classi di livello superiore che affrontano preoccupazioni trasversali.

  • È un ReferenceWell a Well o un derivato Well ? Statico o "riferimento" dati / stato lo rende solo un altro Well .
  • HoleID identifica in modo univoco un oggetto Well

Potrebbe essere ciò che è SurveyPoint ?

public class SurveyPoint
{
    // Each property is typed. This is intentional.
    // It gives us a place to encapsulate properties and
    // behaviors/methods that are "atomic" to each.

    public int              HoleID      { get; set; }
    public Geometry         Geometry    {get; set;}
    public Depth            Depth       {get; set;}
    public TemperaturePoint Temperature { get; set; }
    public Inclination      Inclination { get; set; }
    public Tortousity       Torture     { get; set; }
    public Asmuth           Asmuth      { get; set; }
    public Tvd              Tvd         { get; set; }
}

E quindi TemperaturePoint , FluidPoint , GeometryItem , ecc. sono solo sottoinsiemi (non sotto classi) di SurveyPoint ? In altre parole, aspetti diversi. Quindi ..

public class SurveyPoint
{
    //. . .

    public Temperature GetTemperature() {return this.Temperature;}
    public FluidPoint GetFluidPoint () {return this.FluidPoint;}
    // etc. and overload as needed.
}

public class SurveyPointCollection : Collection<SurveyPoint> {
    public TemperatureCollection GetTemperatures() {}
    public FluidPointCollection GetFluidPoints() {}

    public GetTemperature(Depth depth) {
       // iterate collection to find the SurveyPoint with that depth
     }
    //etc.
}

public class Temperature {
    public HoleID {get; set;}
    public double Temperature {get; set;}
    // any other properties help *define* a temperature for a well

    // add any useful methods that operate only on properties defined in this class
}

// a similar class for each of the other SurveyPoint aspects.

Vedo un aspetto della coesione che si evolve e ruota attorno alle strutture dati principali. Una volta che le classi di dati sono descritte in (abbastanza) dettagli, allora puoi iniziare a "mappare" il comportamento ai livelli appropriati delle incapsulazioni di dati, rendendo quindi molto coese le classi orientate agli oggetti (sì @FrankHileman è corretto, stai pensando funzionalmente non oggettivamente).

Quindi presumo che una Well possa iniziare ad apparire come questa

public class Well
{
    public string Name {get; set;}
    public int HoleID {get; set;}
    public SurveyPointCollection SurveyPoints {get; set;}

    public Well () { }

    public TemperatureCollection GetTemperatures () { 
         SurveyPoints.GetTemperatures();
    }

    public Temperature GetTemperature ( Depth depth ) { }

    // etc. for all other aspects.

}

Esplorazione degli aspetti del comportamento

public class Surveyer
{
    protected Well MyWell { get; set; }
    protected DepthCalculator Calculator { get; set; }

    public Surveyer ( Well theWell ) {
        MyWell = theWell;
        Calculator = new DepthCalculator (MyWell.GetSurveyPoints());
    } 
}

"Depth" sembra essere la cosa unificante nei nomi dei metodi ReferenceWellSurvey , quindi ho immaginato un DepthCalculator . Tuttavia potremmo avere un calcolo che corrisponde ai nostri vari aspetti dei dati. Quindi potremmo avere questo:

public class DepthCalculator {
    protected TemperatureCalculator {get; set;}
    protected InclinationCalculator {get; set;}
    protected FrictionCalculator    {get; set;}
    // etc.

    public DepthCalculator (SurveyPointCollection dataPoints) {
        TemperatureCalculator = new TemperatureCalculator (dataPoints.GetTemperatures());
        FrictionCalculator = new FrictionCalculator(dataPoints.GetFrictionPoints());
    }
}

Non ne so abbastanza del "dominio del pozzo petrolifero" per sapere dove andare con questo. Tuttavia potresti dare un'occhiata al Builder Pattern per l'ispirazione.

    
risposta data 18.07.2014 - 20:28
fonte
2

A volte i modelli di progettazione e le linee guida di progettazione possono intralciare la semplicità. Il problema che descrivi (il problema originale risolto dal software, non i problemi di progettazione) suona come se fosse risolto al meglio da un linguaggio personalizzato composto da funzioni componibili. Uso la parola "lingua" nel senso più ampio, non implicando la necessità di un parser, ecc. I tuoi oggetti contengono dati e le funzioni forniscono le operazioni su quei dati. Questa non è un'analisi orientata agli oggetti, ma un'analisi funzionale. Se poi desideri essere più orientato agli oggetti, puoi provare ad assegnare alcune funzioni ad alcuni oggetti che incapsulano anche i dati. Probabilmente il design funzionale dovrebbe venire prima, quindi il design orientato agli oggetti.

    
risposta data 18.07.2014 - 01:04
fonte
2

Penso che tu abbia commesso un peccato molto peggiore nel tuo lavoro: non hai scritto alcun test. Da un modo molto più astratto, non hai guardato il tuo codice dal punto di vista del codice che lo userà. È molto più facile vedere le astrazioni e i possibili modi per dividere il comportamento se si scrive effettivamente il codice che utilizza ciò che si sta tentando di progettare. Quindi porta a un design davvero migliore.

La prossima cosa da notare è che mentre SRP è bello, non dovrebbe essere usato senza il resto dei principi OLID. SRP ci ricorda che c'è qualcosa chiamato responsabilità e che dovremmo esserne consapevoli. Ma è estremamente pessima la regola su cui basare il tuo progetto, semplicemente perché la responsabilità può significare qualsiasi cosa, dalla "responsabilità di aggiungere due numeri" alla "responsabilità di risolvere tutti i problemi del cliente". Questo rende SRP abbastanza ambiguo. La progettazione del software è una disciplina complessa e basarla su un solo principio è, nella migliore delle ipotesi, sciocca.

    
risposta data 18.07.2014 - 08:22
fonte
0

Le classi funzionali hanno uno stato interno reale o possibili variazioni / implementazioni alternative? In caso contrario, potresti riscrivere ReferenceWellGeometry come classe di utilità con tutti i metodi static che richiedono un parametro aggiuntivo per la raccolta? per es.

static double GetFrictionAtDepth( ICollection<GeometryItem> _geometryItems, double depth) {}

Questo stile ha degli svantaggi, ma nel tuo caso potrebbe semplificare tutti i problemi di dipendenza / iniezione della classe.

    
risposta data 18.07.2014 - 08:35
fonte
0

Nota

Qualsiasi risposta sarà influenzata dalla descrizione del problema / modello che ci è stato fornito; ci possono essere altri modi di guardare il dominio che sono più utili

Avvertenze a parte

Sembra che tu abbia una buona classe e diverse raccolte di misure correlate su un pozzo. Se le misure possono essere ripetute in momenti diversi, allora ogni singolo pozzetto può avere diverse serie di misure, anche dello stesso tipo

questo porta a una struttura semplice: un oggetto e un insieme di misure / calcoli sull'oggetto -

Well - a concrete class for an instance of a well, including properties intrinsic to the well (like HoleID or Name)
WellOperation - an abstract class for a set of related Measurements on a specific Well
    ReferenceWellTemperature - a bunch of temperature measurements
    ReferenceWellGeometry - a bunch of geometry measurements
    etc.

Ciascuna sottoclasse operativa contiene dati in riferimento a un particolare pozzo. Un singolo pozzetto può avere zero o più di questi, possibilmente multipli dello stesso tipo prelevati in diversi momenti temporali

Quindi non hai bisogno - e non vuoi - di una classe overarching come l'esempio ReferenceWellData; il numero e i tipi di operazioni di misurazione possono cambiare, il che causerà la modifica dell'interfaccia di classe generale, e non è necessario

    
risposta data 24.07.2014 - 02:02
fonte
0

Se non avevi bisogno di ReferenceWellSurvey , ReferenceWellGeometry , ecc ... per comunicare, allora aveva senso ( dal punto di vista dell'implementazione ) per fare questa ripartizione. Tuttavia non è consigliabile fare affidamento sui dettagli del livello di implementazione per stabilire la struttura della tua classe.

"Ultimamente mi sono reso conto guardando il nostro codice che devo rimettere insieme tutto questo per renderlo utilizzabile dove ne ho bisogno nella logica dell'applicazione." Vuoi dire, perché l'utente di questa classe non si cura (e quindi, sarebbe meglio non essere esposto a) sulla decomposizione del livello di implementazione? Ma incollare le cose in questo modo non è necessariamente male.

"Alcune cose che mi hanno portato all'attacco attuale" ... Che casino attuale? Il fatto che una raccolta di classi, ognuna responsabile di un piccolo insieme di dati correlati, sostenga l'implementazione di un'altra classe, non costituisce un disastro. Ora, se hai bisogno di importare ReferenceWellSurvey da ReferenceWellGeometry (o qualsiasi dipendenza simile tra queste classi di supporto), beh questo sarebbe un disastro e minerebbe la tua ripartizione orientata all'implementazione .

  • WellData dovrebbe sopravvivere (esiste un motivo per non nominare questo "Bene"? Esistono modelli concorrenti di un pozzo nei dati) se si mantiene la ripartizione orientata al servizio o meno. Una classe ben progettata è una classe che viene chiamata in modo intuitivo ed espone funzionalità utili.
  • IWellData può sopravvivere. Supponendo che qualcosa lo stia usando.
  • Se la tua suddivisione in classi orientate al servizio ha già iniziato a sentirsi a disagio, sbarazzarsi di esso. Prova a guardare questo dal punto di un principiante cercando di mantenere il tuo codice. Un povero oggetto Well Well sarà un piacere per loro.
  • IGetTvdAtDepth (rinomina in ITvdCalculator ? Tvd è un acronimo?) sembra ok, ma assicurati che nascondere la restante funzionalità sia legittimo (se qualcuno volesse accedere a un altro metodo Well in futuro, sarebbe corretto In altre parole, stai nascondendo questi metodi perché non sono necessari o perché non dovrebbero essere accessibili?)

"Una cosa che mi infastidisce nel fare le cose è che nella mia API sto usando un'iniezione bastard per soddisfare le dipendenze richieste di questa classe ... In questo momento ho tre dipendenze richieste" È un problema? In tal caso, ciò potrebbe fornire la logica per definire IWellData .

In conclusione, penso che la tua soluzione proposta possa andare bene. Questo andrebbe contro il principio di responsabilità? Forse, ma penso che un design che approva la definizione di "Bene" nel tuo dominio sia un design migliore rispetto alla creazione di classi di livello di implementazione.

    
risposta data 24.07.2014 - 12:02
fonte