Esporre le variabili membro vs Aggiungere funzioni membro per modificarle

5

Diciamo che abbiamo una classe Car che contiene oggetti di tipo Wheel

class Wheel {
public:

    void SetFriction(double f) {
        friction = f;
    }

private:
    double friction;
};

class Car {

private:
    std::array<Wheel, 4> _wheels;
}

Ora voglio offrire al proprietario di un'automobile l'accesso all'attrito delle ruote. Ci sono principalmente due modi in cui posso pensare di fare questo:

  • Dare accesso alle ruote:

    class Car {
    
        Wheel& GetWheel(int id) {
            return _wheels[id];
        }
    
    private:
        std::array<Wheel, 4> _wheels;
    }
    
    Car beetle;
    beetle.GetWheel(1).SetFriction(0.4);
    
  • Dare accesso direttamente all'attrito:

    class Car {
    
        void SetWheelFriction(int id, double friction) {
            _wheels[id].SetFriction(friction);
        }
    
    private:
        std::array<Wheel, 4> _wheels;
    }
    
    Car beetle;
    beetle.SetWheelFriction(1, 0.4);
    

Il problema con la prima versione è che dà accesso a un membro privato, potenzialmente rompendo l'incapsulamento. Forse una funzione viene aggiunta alla ruota (ad esempio "SetManufacturer") che non deve essere esposta al proprietario di un auto.

Dall'altro lato, la seconda versione porta a definire molte funzioni in Car che non sono di sua diretta responsabilità, violando così il principio di responsabilità singola.

class Car {

    void SetWheelFriction(int id, double friction) {
        _wheels[id].SetFriction(friction);
    }

    double GetEngineWeight() {
        return _engine.GetWeight();
    }

    double SetWindowsColor(Color& c) {
        for(auto& w : _windows){
            w.SetColor(c);
        }
    }

        // And so on...

Sono strongmente incline alla prima opzione, ma mi piacerebbe sentire l'opinione di qualcun altro.

Forse in qualche modo correlato: link

    
posta Fabio 22.12.2017 - 18:25
fonte

6 risposte

4

Non parli di quale sia la responsabilità principale della classe Car .

In una vera applicazione, la tua classe Car non rappresenterebbe solo un'auto fisica. Lo rappresenterebbe in un contesto specifico. E sapere il contesto è importante in quanto è ciò che modellerà l'API pubblica della tua classe.

Esempio di contesto 1 - Simulazioni di corse

Diciamo, per esempio, che questo contesto sta eseguendo simulazioni per ottimizzare le prestazioni di una macchina da corsa. L'API pubblica della tua classe Car potrebbe esporre dati e metodi per ottimizzare le caratteristiche di prestazione della tua auto. In questo contesto sarebbe abbastanza ragionevole per una classe Simulation o una classe PerformanceEvolver utilizzare l'API Car per modificare direttamente l'attrito ruota (ad es. car.setWheelFriction(WheelsEnum.FrontLeft, coefficient: 0.8) .

Esaminiamo ora un contesto diverso.

Esempio di contesto 2 - BOM

Immaginiamo che la tua classe Car sia utilizzata nel contesto di una distinta base. La responsabilità principale del tuo Car qui è di descrivere i componenti necessari per crearlo. Avrà sicuramente un riferimento a quattro ruote, ma è praticamente l'estensione dei dati e del comportamento relativi alle ruote inclusi nell'API pubblica Car .

Quindi, se ti interessassero i componenti necessari per costruire la ruota anteriore sinistra della macchina, non ti aspetteresti di fare qualcosa come car.getFrontLeftWheelBillOfMaterials() perché sarebbe una preoccupazione ruota piuttosto di una preoccupazione auto . Invece, dovresti fare qualcosa come car.Wheels[WheelsEnum.FrontLeft].GetBillOfMaterials() .

The Takeaway:

Quindi, tutto questo per dire questo:

  • Non è possibile dare consigli su come modellare un'API su un esempio forzato .
  • La forma dei tuoi modelli, le loro API e la loro semantica (spero di usare correttamente questo termine qui) dipenderà molto dal contesto specifico.

Se stai cercando una regola generale dura e veloce, allora penso che sei sfortunato.

Se hai un esempio specifico a cui vorresti dare una mano, ti suggerisco di aggiungere tali informazioni alla tua domanda.

Buon Natale!

    
risposta data 25.12.2017 - 18:21
fonte
3

Principio di responsabilità singola (SRP)

L'SRP non riguarda la funzionalità offerta da una classe, ma sui motivi per cui cambiare :

  • L'opzione 1 è conforme a SRP, poiché una modifica del Wheel non richiederebbe di modificare Car .
  • L'opzione 2 potrebbe al contrario propagare le esigenze di cambiamento: qualsiasi modifica nell'interfaccia Wheel decisa da un esperto di rotazioni potrebbe richiedere anche una modifica nell'interfaccia Car .

Problema con l'opzione 1

Il problema principale con l'opzione 1 è che restituisce un riferimento a un oggetto. Questo riferimento potrebbe essere salvato da un utente di Car (ad esempio prendendo il suo indirizzo in un puntatore) e potrebbe quindi essere utilizzato in un secondo momento, anche se il Wheel è stato nel frattempo sostituito.

Un approccio più sicuro ma più pesante sarebbe quello di restituire un oggetto proxy che esporrebbe l'interfaccia Wheel , ma assicurerebbe che la ruota sia accessibile tramite Car .

Problema nascosto con tutti gli approcci

Un problema con il tuo approccio è che, nonostante il tuo tentativo di incapsulare le ruote, presumi che un'auto abbia 4 ruote, numerata da 0 a 3.

Tuttavia, si potrebbero immaginare alcune piccole auto futuristiche con 3 ruote. E al giorno d'oggi, ci sono già alcune grandi limousine con 6 ruote.

Quindi dovresti almeno offrire una funzione che dia il numero di ruote. O meglio, offri un iteratore che consente di scorrere tra le ruote secondo un ordine predefinito.

    
risposta data 23.12.2017 - 23:38
fonte
0

Qual è l'obiettivo principale del principio di incapsulamento? Come sarà il codice quando tutte le proprietà e i metodi saranno disponibili per ogni altra parte del codice? Prevedo che le proprietà e i metodi di una particolare classe verranno utilizzati da molte altre parti del sistema, vincolandoli agli elementi di quella classe. Cosa c'è di sbagliato in questo legame? È sbagliato perché alcuni di questi elementi possono essere cambiati durante i refactoring o il mantenimento di questa particolare classe. E quando cambi questo elemento, devi anche cambiare tutto il codice "client" ("codice client" intendo altro codice che usa questo particolare elemento della classe). Sarà un mal di testa per farlo.

Quindi quali parti della classe dovrebbero essere nascoste? Almeno quelli che possono essere cambiati in futuro senza cambiare il comportamento della classe. "Dovrei usare Set qui o List sarebbe meglio? Entrambi gli approcci funzionano", "Il mio algoritmo utilizzerà questo tipo / variabile o l'altro?". Tali domande dovrebbero avvertirti che questa parte della classe è solo un'implementazione interna, proprio come si sceglie di ottenere un comportamento. Nascondi questa parte dagli altri, perché potrebbe cambiare e non vuoi ridirigere il 50% dell'applicazione perché è stato modificato il tipo di una variabile.

Mi sono appena reso conto che le seguenti idee di Domain Driven Design aiutano nelle decisioni che cosa dovrebbe e cosa non dovrebbe essere esposto. Se la classe è una radice aggregata, puoi esporla. Se è solo un'entità all'interno di Aggregate - meglio di no.

Rispondere alla tua domanda: qual è la probabilità che la tua auto non disponga di ruote in futuro? Se non è elevato, puoi esporre Ruote, perché probabilmente non saranno cambiate in qualcos'altro. Ma l'esposizione dell'array che contiene Wheels non può essere una buona decisione, perché dopo qualche rimodellamento forse ti renderai conto della mancanza di idea di Chassis che raccoglie le ruote e ha alcuni comportamenti necessari (non sostengo che lo farai - è solo un esempio). Parlando in termini di DDD - Wheel sembra essere un Root Aggregate valido allo stato attuale del modello.

A proposito. l'attrito non è una proprietà di una ruota. Dipende dalla ruota stessa e dalla superficie su cui ruota la ruota. ;)

    
risposta data 22.12.2017 - 22:16
fonte
0

On the other side, the second version leads to a lot of functions being defined in Car that are not of its direct responsibility, thus violating the single responsibility principle.

Non sono d'accordo con la premessa che hai indicato sopra. La sola responsabilità della macchina è quella di ottenere il guidatore dal punto A al punto B. Questa responsabilità racchiude molti piccoli dettagli che l'utente dell'auto non ha bisogno di sapere su quanto carburante inviare al motore e quando . (L'utente richiede l'accelerazione, ma l'auto decide come produrlo.) Le ruote fanno sicuramente parte della singola responsabilità di portare la persona a destinazione. Per questo motivo, preferisco di più il tuo secondo esempio, anche se sono d'accordo sul fatto che gli utenti non debbano aver bisogno di impostare l'attrito di una ruota. Qualunque cosa si supponga di realizzare, vorrei fare un accessorio per esso, ma se ha cambiato l'attrito della ruota o ha fatto qualcos'altro per portare a termine il compito è fino alla classe. E scrivere l'interfaccia in questo modo ti lascia molto margine di manovra per cambiarlo in futuro se dovesse emergere un metodo migliore.

    
risposta data 23.12.2017 - 23:05
fonte
0

Una ragione per preferire il secondo approccio è di conformarsi alla Legge di Demetra. Se scegli il primo approccio, potresti finire con queste lunghe chiamate che aumentano l'accoppiamento tra le classi. Ad esempio il tuo codice potrebbe sembrare region.getDealership().getBestSellingCar().getWheel().setFriction(0.25) mentre se fosse conforme alla legge di Demeter sarebbe qualcosa come region.setWheelFrictionOnBestSellingCar(0.25)

Nel primo caso la classe deve conoscere la classe Dealership, la classe Car e la classe Wheel, quindi un cambiamento in uno di essi richiede una modifica del codice di chiamata. In quest'ultimo caso il codice deve solo conoscere la classe Dealership. C'è anche un argomento per brevità o leggibilità tra i due.

    
risposta data 27.12.2017 - 14:53
fonte
-1

La regola generale è di esporre gli oggetti componente e lasciare che l'utente imposti cose come friction e color su di essi direttamente. Tuttavia, tutte le regole generali sono fatte per essere infrante. Ci sono casi in cui potresti voler avere funzioni come SetWheelFriction . Il caso principale è quando non ci si aspetta che un utente sia a conoscenza dei dettagli della classe Wheel . A volte è un dettaglio di implementazione. Altre volte la classe del componente è pensata per essere una funzione avanzata che si tende a non usare perché i nuovi sviluppatori che usano il Car non hanno bisogno di sapere circa Wheels fino a molto più tardi.

Si può scegliere di esporre solo alcune funzioni dell'API principale. Cose come checkTirePressure e checkOilLevel potrebbero apparire su Car , mentre l'API completa per la modifica dell'olio potrebbe richiedere l'accesso alle classi OilPan e OilFilter .

Queste funzioni sono spesso chiamate "zucchero sintattico". Possono essere molto utili dal punto di vista della convenienza, ma come tutti gli zuccheri, li usano con moderazione. Possono marcire i denti!

    
risposta data 24.12.2017 - 17:48
fonte

Leggi altre domande sui tag