Perché le funzioni getter e setter sono considerate contrarie al design OO? Perché dovrebbero essere evitati? [duplicare]

2

link

If you need to change the way the object is implemented in such a way that the type of X changes, you're in deep trouble.

If X was an int, but now must be a long, you'll get 1,000 compile errors. If you incorrectly fix the problem by casting the return value to int, the code will compile cleanly, but it won't work.

Se questa affermazione audace è vera, ciò significa che IMO non è stato ben pensato! Perché il "tipo" di una variabile dovrebbe improvvisamente cambiare in un programma?

Il caso presentato è davvero fattibile?

It follows then that in OO systems you should avoid getter and setter functions since they mostly provide access to implementation details.

Possiamo impostare le condizioni nel metodo setter che impedirà la scrittura di dati privi di senso sul campo? Non possiamo?

Se stiamo parlando di " forniscono principalmente accesso ai dettagli di implementazione. " allora non stiamo parlando di aree protette e pubbliche? Se lo siamo, allora si suppone che queste funzioni siano accessibili da classi esterne. Quindi, qual è il problema qui?

Perché una variabile privata dovrebbe avere funzioni pubbliche getter e setter? Dovrebbe? Se no, allora l'argomento sopra non è applicabile lì?

    
posta Aquarius_Girl 05.04.2016 - 11:53
fonte

4 risposte

6

Getter e setter non sono il male di per sé, sono malvagi quando sono usati per cose per cui non dovrebbero essere usati.

Poiché ci sono casi in cui l'uso di un getter è inevitabile, ci sono anche casi in cui si utilizza Tell Do not Ask il principio è molto più adatto come soluzione per il problema piuttosto che come metodo get* .

Perché i getter sono esattamente sfigati allora?

È abbastanza semplice. Interrompono l'incapsulamento dando accesso alle variabili protected e private di una classe, e l'incapsulamento è uno dei principali vantaggi della progettazione OO.

E quando l'incapsulamento viene interrotto e i getter vengono abusati, è molto probabile che tu passi dal progetto orientato agli oggetti, dove i componenti sono collegati, al concetto, in cui devi dividere un intero oggetto per get ting i suoi valori e facendo operazioni basate su questi valori, altrimenti privati, si ritorna alla programmazione procedurale.

Ma come con tutto, ci sono casi in cui i getter non sono solo una buona soluzione, ma in realtà l'unico.

Ad esempio un intero strato ORM: astrazione del puro database agli oggetti, che devono semplicemente avere getter. Sono semplici oggetti per il trasferimento dei dati la cui unica responsabilità è di fornire dati, non di fornire alcuna logica. Forniscono i dati tramite getter e setter mentre di solito mappano le loro proprietà su attributi concreti del database (colonne se lo si desidera). Usate queste strutture molto semplici per costruire entità di dominio (modelli di dominio).

Non è raccomandato che i modelli di dominio abbiano getter / setter

Dovresti stratificare la tua applicazione in un modo in cui c'è un livello di oggetti, che anche se hanno getter, usano solo i getter per fornire dati per i livelli di presentazione o persistenza (cioè se vuoi mostrare o mantenere un dominio modello, estrai i suoi attributi usando getter e inoltrali rispettivamente alla presentazione o al livello persistente). In nessun modo il getter deve essere usato per estrarre un valore dall'oggetto ed eseguire un'operazione su di esso.

Dai un'occhiata a questo esempio (scritto in PHP, la lingua con cui mi trovo più a mio agio):

class HarmlessMonster
{
    /** @var int */
    private $health;

    /** @var int */
    private $armor;

    /**
     * HarmlessMonster constructor.
     * @param int $health
     * @param int $armor
     */
    public function __construct($health, $armor)
    {
        $this->health = $health;
        $this->armor = $armor;
    }

    /**
     * @param int $health
     */
    public function setHealth($health)
    {
        $this->health = $health;
    }

    /**
     * @return int
     */
    public function getHealth()
    {
        return $this->health;
    }

    /**
     * @return int
     */
    public function getArmor()
    {
        return $this->armor;
    }
}

class Player
{
    /** @var int */
    private $damage;

    /** @var int */
    private $weaponWearPercentage;

    /**
     * Player constructor.
     * @param int $damage
     * @param int $weaponWearPercentage
     */
    public function __construct($damage, $weaponWearPercentage = 0)
    {
        $this->damage = $damage;
        $this->weaponWearPercentage = $weaponWearPercentage;
    }

    /**
     * @return int
     */
    public function getDamage()
    {
        return $this->damage;
    }

    /**
     * @return int
     */
    public function getWeaponWearPercentage()
    {
        return $this->weaponWearPercentage;
    }

    /**
     * @param int $weaponWearPercentage
     */
    public function setWeaponWearPercentage($weaponWearPercentage)
    {
        $this->weaponWearPercentage = $weaponWearPercentage;

        if ($this->getWeaponWearPercentage() > 100) {
            $this->setWeaponWearPercentage(100);
        }
    }
}

Per rendere queste classi in qualche modo funzionanti, hai bisogno di un'altra classe per fornire la logica, una classe come questa:

class PlayerMonsterService
{
    public function attackMonsterByPlayer(Player $player, HarmlessMonster $monster)
    {
        if (($player->getDamage() * (100 - $player->getWeaponWearPercentage())) <= $monster->getArmor()) {
            $player->setWeaponWearPercentage($player->getWeaponWearPercentage() + 2);
            return;
        }

        $monster->setHealth($monster->getHealth() - ($player->getDamage() * (100 - $player->getWeaponWearPercentage()) - $monster->getArmor()));
        $player->setWeaponWearPercentage($player->getWeaponWearPercentage() + 1);
    }
}

Stai distruggendo completamente i modelli di dominio, ottenendo i valori degli attributi privati per usarli nei setter, perché c'è una chiara connessione tra loro. E questo è davvero brutto. Al contrario di un modello Tell Do not Ask, potrebbe apparire come questo:

class GameException extends RuntimeException { }
class ArmorTooHighException extends GameException { }

class HarmlessMonster
{
    /** @var int */
    private $health;

    /** @var int */
    private $armor;

    /**
     * HarmlessMonster constructor.
     * @param int $health
     * @param int $armor
     */
    public function __construct($health, $armor)
    {
        $this->health = $health;
        $this->armor = $armor;
    }

    public function reduceHealth($damage)
    {
        if ($damage <= $this->armor) {
            throw new ArmorTooHighException('Health was not recuded. Insufficient damage.');
        }

        $this->health -= ($damage - $this->armor);
    }
}

class Player
{
    /** @var int */
    private $damage;

    /** @var int */
    private $weaponWearPercentage;

    /**
     * Player constructor.
     * @param int $damage
     * @param int $weaponWearPercentage
     */
    public function __construct($damage, $weaponWearPercentage = 0)
    {
        $this->damage = $damage;
        $this->weaponWearPercentage = $weaponWearPercentage;
    }

    public function attack(HarmlessMonster $monsterToBeAttacked)
    {
        try {
            $monsterToBeAttacked->reduceHealth($this->damage * (100 - $this->weaponWearPercentage));
        } catch (ArmorTooHighException $e) {
            $this->increaseWeaponWearBy(2);
            return;
        }

        $this->increaseWeaponWearBy(1);
    }

    public function increaseWeaponWearBy($amountToBeIncreased)
    {
        $this->weaponWearPercentage += $amountToBeIncreased;

        if ($this->weaponWearPercentage > 100) {
            $this->weaponWearPercentage = 100;
        }
    }
}

Si può vedere, anche se sto usando variabili private di entrambe le classi, non ci sono getter cosa che mai. Perché non ne hai bisogno.

Ma anche qui ricordate che sia le classi Player che HarmlessMonster avrebbero probabilmente bisogno di getter, per i motivi detti prima, che presentano i dati all'utente o che persistono. Il punto di questo non è usare i getter per calcolare i valori e in base a questi valori fare operazioni.

Il livello di dominio è il livello che in un caso ideale dovrebbe seguire in modo abbastanza rigoroso il principio Tell Do not Ask, con i metodi tell, che generano eccezioni su un'azione non valida. Ciò pone tutta la logica relativa alla classe in cui viene eseguita un'operazione in un luogo molto specifico e esattamente a cui appartiene (solitamente la classe stessa), piuttosto che essere dispersa in molti punti della base di codice.

Ho scoperto, uno dei motivi per cui alcune persone preferiscono la combinazione getter / setter rispetto all'altro approccio è la pigrizia, perché arrivare alla soluzione tell richiede di solito qualche minuto in più rispetto a scrivere un semplice get and doing con il valutare ciò che vogliono E non è necessariamente male, se non ti dispiace l'approccio procedurale, ma se quello che cerchi è il design OO, evitare i Getters a meno che tu non ne abbia davvero bisogno è sicuramente un buon modo per farlo.

    
risposta data 05.04.2016 - 12:06
fonte
3

Proteggere una classe dall'uso improprio è solo una delle ragioni per evitare ispettori / mutatori (getter / setter).

L'altro motivo è quello di rafforzare la coesione e la separazione delle preoccupazioni assicurandoci che il comportamento che logicamente appartiene a una classe sia definito in quella classe, e non altrove.

Ad esempio, considera la relazione tra Driver e il loro Vehicle ; ti aspetteresti che un metodo DriveToTheShops() sia in termini di cose che Driver comprende:

class Driver
{
    private Vehicle _vehicle;
    public void DriveToTheShops()
    {
        _vehicle.StartEngine();
        _vehicle.Accelerate();
        _vehicle.RotateSteeringWheel(90.0);
        _vehicle.RotateSteeringWheel(0.0);

        if(trafficLights.State == TrafficLight.Red)
        {
            _vehicle.ApplyBrake();
        }
    }
}

Ma se tutto in Vehicle è stato esposto pubblicamente, il metodo DriveToTheShops() potrebbe apparire come:

class Driver
{
    private Vehicle _vehicle;
    public void DriveToTheShops()
    {
        // Start Engine
        do {
            _vehicle.StarterMotor.Enabled = true;
            _vehicle.CrankShaft.Turn();

            if (_vehicle.Engine.State == Engine.Off)
            {
                _vehicle.StarterMotor.Enabled = false;
            }
        } while(_vehicle.Engine.State == Engine.Off);

        // Accelerate
        var throttleAmout = _vehicle.Accelerator.throttleAmount
        _vehicle.Engine.IncreaseFuelFrom(_vehicle.FuelTank, throttleAmount);

        // Turn Right
        _vehicle.FrontLeftWheel.Angle = 90.0;
        _vehicle.FrontRightWheel.Angle = 90.0; 

        // Straighten up
        _vehicle.FrontLeftWheel.Angle = 0.0;
        _vehicle.FrontRightWheel.Angle = 0.0; 

        if(trafficLights.State == TrafficLight.Red)
        {
            // Apply Brake
            var brakeAmount = _vehicle.BrakePedal.BrakeAmount;
            _vehicle.FrontLeftWheel.Brake = brakeAmount;
            _vehicle.FrontRightWheel.Brake = brakeAmount; 
        }
    }
}

In primo luogo, ciò rende il lavoro di Driver molto più complesso - il Driver ora deve sapere come funzionano i motori del veicolo, le ruote, i freni, il serbatoio del carburante e il motorino di avviamento. Ma un guidatore non dovrebbe aver bisogno di sapere nulla di tutto questo per poter guidare un veicolo.

In secondo luogo, la mancanza di astrazione dietro un'interfaccia pulita significa che se il Driver vuole guidare un diverso tipo di Veicolo, il conducente deve conoscere anche i meccanismi interni di quel particolare tipo di Veicolo.

La creazione di un diverso tipo di Vehicle implicherebbe alcuni refactoring per spostare il suo comportamento in un'altra classe, poiché la logica è vincolata in Driver .

    
risposta data 05.04.2016 - 13:33
fonte
2

Non sono . L'articolo che colleghi è basato su un grosso fraintendimento. Ciò che è disapprovato è rivelare i campi pubblici , poiché ciò lega i client ai dettagli di implementazione. Getter e setter sono la soluzione per questo problema, poiché incapsula l'implementazione sottostante e può cambiarla senza cambiare il client. L'articolo usa l'argomento contro i campi pubblici e lo applica in modo uguale a getter / setter, ma questo non tiene conto dell'intero punto, che getter / setter incapsula l'implementazione e puoi modificare la rappresentazione sottostante senza modificare l'interfaccia.

Ovviamente non si dovrebbero usare getter e setter per esporre dettagli di implementazione - ma questo è lo stesso per qualsiasi metodo. Getter e setter possono essere usati per esporre dati che dovrebbero essere veramente incapsulati, ma puoi fare lo stesso con qualsiasi metodo ordinario., Non diciamo che i metodi sono malvagi perché possono essere usati in modo improprio.

    
risposta data 05.04.2016 - 14:36
fonte
1

Ricordo alcuni argomenti piuttosto scottanti quando è uscito. Questo articolo è sempre stato molto controverso nella comunità Java. Personalmente, penso che l'articolo sia uno di una vasta serie di articoli sul design in cui l'autore ti dice di non fare le cose in un certo modo ma fornisce pochissimi dettagli su come farlo in modo diverso. Consiglierei di prendere questi articoli con un granello di sale. Non è sempre chiaro che questi autori abbiano effettivamente costruito sistemi reali di notevole complessità.

Detto questo, i getter non sono "cattivi" di per sé (per qualche ragione c'era un certo numero di motivi per cui X è male articoli in quel periodo) ma c'è un problema con il loro uso che penso sia molto problematico. Per spiegare questo, dobbiamo tornare al modo in cui le cose sono fatte in un linguaggio procedurale puro. In quel mondo, si creano record di dati e quindi si scrivono metodi che operano su questi dati. In sostanza, si passano i puntatori / riferimenti a quei record di dati ai metodi in cui si scrive la logica.

Uno dei concetti principali di OO è quello di accenderlo. Invece di passare i dati al metodo, il metodo è legato concettualmente a un record specifico. Questa combinazione di metodi e dati è chiamata un oggetto. Il grande vantaggio di questo è che mantenendo i dati incapsulati puoi controllare come vengono gestiti i dati e capirli senza guardare attraverso l'intero programma per tutto ciò che potrebbe toccarlo.

Il problema con getter e setter che ho visto è che spesso gli sviluppatori creano una classe, dichiarano un gruppo di variabili, generano getter e setter per ogni variabile nell'oggetto e poi mettono tutta la logica interessante altrove. Fondamentalmente, questo tipo di struttura del programma sembra proprio il puro approccio procedurale. I getter e i setter sono semplicemente una facciata su un disco senza alcun comportamento.

È decisamente meglio avere getter e setter piuttosto che esporre le variabili. Fornisce almeno un livello di astrazione che è possibile utilizzare se è necessario modificare le cose in un secondo momento, ma in realtà non si otterrà il valore di OO senza prendere la logica specifica per tali dati e spostandola nella classe.

Fornirò un esempio che dovrebbe eventualmente fornire una certa chiarezza. NOTA: questo è un design terribile. Non farlo :

class ImExposed
{
   private int[] values = {};

   public int[] getValues()
   {
       return values;
   }

   public void setValues(int[] values)
   {
       this.values = values;
   }
}

class Calculator
{
    public static long sum(ImExposed data)
    {
        long total = 0;

        for (int value : data.getValues()) {
            total += value;
        }

        return total;
    }
}

class ValueUpdater
{
    public static void add(int value, ImExposed data)
    {
        int[] oldValues = data.getValues();
        int[] newValues = new int[oldValues.length + 1]

        for (int i = 0; i < oldValues.length; i++) {
          newValues[i] = oldValues[i];
        }

        newValues[oldValues.length] = value;

        data.setValues(newValues);
    }
}

Speriamo che i problemi con questo siano evidenti. Anche se hai "incapsulato" i dati con getter e setter, è una facile astrazione. Per capire come vengono aggiornati e acceduti i valori dei dati, è necessario cercare l'intera base di codici. E poiché gli aggiornamenti e le letture a questo array non sono coordinati dalla classe del proprietario, ci sono innumerevoli modi in cui questo codice potrebbe fallire ed è molto difficile sapere se il tuo codice funzionerà correttamente. Ogni classe che interagisce con l'array di dati deve farlo nel modo giusto. Cercare di fare in modo che quanto sopra funzioni correttamente con la concorrenza è oltre ogni limite.

class NotExposed
{
   private List<Integer> values = new ArrayList<>();

   public int[] getValues()
   {
       return list.toArray(new int[list.size()]);
   }

   public void add(int value)
   {
       values.add(value);
   }

   public long sum()
   {
       long total = 0;

       for (Integer value : list) {
           total += value;
       }

       return total;
   }

}

Non restare bloccato sull'implementazione qui. Il punto è che ho spostato tutta la logica verso la classe. Mantengo l'elenco privato in modo che ora possa controllare come viene modificato e acceduto. Ora, se ho bisogno di aggiungere il supporto di concorrenza, posso farlo in un punto. Posso modificare l'implementazione di come vengono memorizzati anche i dati e nessuna delle altre classi nell'applicazione deve essere modificata.

    
risposta data 05.04.2016 - 17:25
fonte

Leggi altre domande sui tag