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.