Come evitare getter e setter?


85

Sto facendo fatica a progettare le lezioni in un modo oo. Ho letto che gli oggetti espongono il loro comportamento, non i loro dati; pertanto, anziché utilizzare getter / setter per modificare i dati, i metodi di una data classe dovrebbero essere "verbi" o azioni che operano sull'oggetto. Ad esempio, in un oggetto "Account", avremmo i metodi Withdraw()e Deposit()piuttosto che setAmount()ecc. Vedi: Perché i metodi getter e setter sono cattivi .

Quindi, ad esempio, data una classe Customer che conserva molte informazioni sul cliente, ad esempio Nome, DOB, Tel, Indirizzo ecc., Come si eviterebbero getter / setter per ottenere e impostare tutti quegli attributi? Quale metodo di tipo 'Behavior' si può scrivere per popolare tutti quei dati?




8
Vorrei sottolineare che se si ha a che fare con la specifica Java Beans , si avranno getter e setter. Molte cose usano Java Beans (Expression Language in jsps) e cercare di evitarlo sarà probabilmente ... impegnativo.

4
... e, dal punto di vista opposto rispetto a MichaelT: se non stai usando le specifiche JavaBeans (e penso personalmente che dovresti evitarlo a meno che tu non stia usando il tuo oggetto in un contesto dove è necessario), allora non è necessario la verruca "get" su getter, specialmente per le proprietà che non hanno setter corrispondente. Credo un metodo chiamato name()sulla Customerè chiara, o più chiara, di un metodo chiamato getName().
Daniel Pryden,

2
@Daniel Pryden: name () può significare sia impostare che ottenere? ...
IntelliData

Risposte:


55

Come indicato in alcune risposte e commenti, i DTO sono appropriati e utili in alcune situazioni, in particolare nel trasferimento di dati oltre i confini (ad es. Serializzazione su JSON per l'invio tramite un servizio Web). Per il resto di questa risposta, lo ignorerò più o meno e parlerò delle classi di dominio e di come possono essere progettate per minimizzare (se non eliminare) getter e setter, e ancora essere utile in un grande progetto. Inoltre non parlerò del perché rimuovere getter o setter, o quando farlo, perché queste sono domande proprie.

Ad esempio, immagina che il tuo progetto sia un gioco da tavolo come Chess o Battleship. Potresti avere vari modi di rappresentarlo in un livello di presentazione (app console, servizio web, GUI, ecc.), Ma hai anche un dominio principale. Una classe che potresti avere è quella di Coordinaterappresentare una posizione sul tabellone. Il modo "cattivo" di scriverlo sarebbe:

public class Coordinate
{
    public int X {get; set;}
    public int Y {get; set;}
}

(Scriverò esempi di codice in C # piuttosto che Java, per brevità e perché ne ho più familiarità. Speriamo che non sia un problema. I concetti sono gli stessi e la traduzione dovrebbe essere semplice.)

Rimozione dei setter: immutabilità

Mentre getter e setter pubblici sono entrambi potenzialmente problematici, i setter sono il più "cattivo" dei due. Di solito sono anche più facili da eliminare. Il processo è semplice: imposta il valore all'interno del costruttore. Qualsiasi metodo che ha precedentemente modificato l'oggetto dovrebbe invece restituire un nuovo risultato. Così:

public class Coordinate
{
    public int X {get; private set;}
    public int Y {get; private set;}

    public Coordinate(int x, int y)
    {
        X = x;
        Y = y;
    }
}

Nota che questo non protegge da altri metodi della classe che mutano X e Y. Per essere più strettamente immutabili, potresti usare readonly( finalin Java). Ma in entrambi i casi, sia che tu renda le tue proprietà veramente immutabili o che prevenga semplicemente la mutazione pubblica diretta attraverso i setter, fa il trucco per rimuovere i tuoi setter pubblici. Nella stragrande maggioranza delle situazioni, funziona perfettamente.

Rimuovere Getters, Parte 1: Progettare per il comportamento

Quanto sopra va bene e va bene per i setter, ma in termini di getter, ci siamo effettivamente sparati al piede prima ancora di iniziare. Il nostro processo consisteva nel pensare a quale coordinata sono - i dati che rappresenta - e creare una classe attorno a ciò. Invece, avremmo dovuto iniziare con quale comportamento abbiamo bisogno da una coordinata. Questo processo, tra l'altro, è supportato da TDD, dove estraiamo classi come questa solo quando ne abbiamo bisogno, quindi iniziamo con il comportamento desiderato e lavoriamo da lì.

Diciamo quindi che il primo posto in cui ti sei trovato ad aver bisogno di un Coordinateera per il rilevamento delle collisioni: volevi verificare se due pezzi occupano lo stesso spazio sulla scacchiera. Ecco il modo "malvagio" (costruttori omessi per brevità):

public class Piece
{
    public Coordinate Position {get; private set;}
}

public class Coordinate
{
    public int X {get; private set;}
    public int Y {get; private set;}
}

    //...And then, inside some class
    public bool DoPiecesCollide(Piece one, Piece two)
    {
        return one.X == two.X && one.Y == two.Y;
    }

Ed ecco il buon modo:

public class Piece
{
    private Coordinate _position;
    public bool CollidesWith(Piece other)
    {
        return _position.Equals(other._position);
    }
}

public class Coordinate
{
    private readonly int _x;
    private readonly int _y;
    public bool Equals(Coordinate other)
    {
        return _x == other._x && _y == other._y;
    }
}

( IEquatableimplementazione abbreviata per semplicità). Progettando per il comportamento anziché modellare i dati, siamo riusciti a rimuovere i nostri getter.

Nota che questo è rilevante anche per il tuo esempio. È possibile che si stia utilizzando un ORM o che visualizzi le informazioni dei clienti su un sito Web o qualcosa del genere, nel qual caso un certo tipo di CustomerDTO avrebbe probabilmente senso. Ma solo perché il tuo sistema include i clienti e questi sono rappresentati nel modello di dati non significa automaticamente che dovresti avere una Customerclasse nel tuo dominio. Forse mentre progetti per il comportamento, ne emergerà uno, ma se vuoi evitare i vincitori, non crearne uno preventivamente.

Rimozione di Getters, parte 2: comportamento esterno

Così il sopra è un buon inizio, ma prima o poi si sarà probabilmente eseguito in una situazione in cui si ha un comportamento che è associato con una classe, che in qualche modo dipende dallo stato della classe, ma che non appartiene in classe. Questo tipo di comportamento è ciò che in genere vive nel livello di servizio dell'applicazione.

Prendendo il nostro Coordinateesempio, alla fine vorrai rappresentare il tuo gioco per l'utente, e questo potrebbe significare disegnare sullo schermo. Ad esempio, potresti avere un progetto UI che utilizza Vector2per rappresentare un punto sullo schermo. Ma sarebbe inappropriato che la Coordinateclasse si occupasse della conversione da una coordinata a un punto sullo schermo, il che porterebbe ogni tipo di problema di presentazione nel tuo dominio principale. Purtroppo questo tipo di situazione è inerente alla progettazione di OO.

La prima opzione , che è molto comunemente scelta, è solo esporre i dannati getter e dire all'inferno con esso. Questo ha il vantaggio della semplicità. Ma dal momento che stiamo parlando di evitare getter, diciamo per amor di discussione, rifiutiamo questo e vediamo quali altre opzioni ci sono.

Una seconda opzione è quella di aggiungere un qualche tipo di .ToDTO()metodo alla tua classe. Questo o simili potrebbero essere necessari comunque, ad esempio quando vuoi salvare il gioco devi catturare praticamente tutto il tuo stato. Ma la differenza tra fare questo per i tuoi servizi e solo accedere direttamente al getter è più o meno estetica. Ha ancora lo stesso "male".

Una terza opzione - che ho visto sostenuto da Zoran Horvat in un paio di video di Pluralsight - è quella di utilizzare una versione modificata del modello di visitatore. Questo è un uso piuttosto insolito e una variazione del modello e penso che il chilometraggio delle persone varierà enormemente sul fatto che stia aggiungendo complessità per nessun guadagno reale o se sia un buon compromesso per la situazione. L'idea è essenzialmente di utilizzare il modello visitatore standard, ma i Visitmetodi devono assumere lo stato di cui hanno bisogno come parametri, anziché la classe che stanno visitando. Esempi sono disponibili qui .

Per il nostro problema, una soluzione che utilizza questo modello sarebbe:

public class Coordinate
{
    private readonly int _x;
    private readonly int _y;

    public T Transform<T>(IPositionTransformer<T> transformer)
    {
        return transformer.Transform(_x,_y);
    }
}

public interface IPositionTransformer<T>
{
    T Transform(int x, int y);
}

//This one lives in the presentation layer
public class CoordinateToVectorTransformer : IPositionTransformer<Vector2>
{
    private readonly float _tileWidth;
    private readonly float _tileHeight;
    private readonly Vector2 _topLeft;

    Vector2 Transform(int x, int y)
    {
        return _topLeft + new Vector2(_tileWidth*x + _tileHeight*y);
    }
}

Come probabilmente puoi dire, _xe _ynon sono più realmente incapsulati. Potremmo estrarli creando un IPositionTransformer<Tuple<int,int>>che li restituisce direttamente. A seconda del gusto, potresti sentire che ciò rende inutile l'intero esercizio.

Tuttavia, con i getter pubblici, è molto facile fare le cose nel modo sbagliato, semplicemente estraendo i dati direttamente e utilizzandoli in violazione di Tell, Don't Ask . Considerando che usare questo modello è in realtà più semplice farlo nel modo giusto: quando vuoi creare un comportamento, inizierai automaticamente creando un tipo associato ad esso. Le violazioni della TDA saranno ovviamente molto maleodoranti e probabilmente richiederanno di aggirare una soluzione più semplice e migliore. In pratica, questi punti rendono molto più facile farlo nel modo giusto, OO, rispetto al modo "malvagio" che incoraggiano i getter.

Infine , anche se inizialmente non è ovvio, potrebbero in effetti esserci modi per esporre abbastanza ciò di cui hai bisogno come comportamento per evitare di dover esporre lo stato. Ad esempio, utilizzando la nostra versione precedente del Coordinatecui unico membro pubblico è Equals()(in pratica sarebbe necessaria IEquatableun'implementazione completa ), è possibile scrivere la seguente classe nel livello di presentazione:

public class CoordinateToVectorTransformer
{
    private Dictionary<Coordinate,Vector2> _coordinatePositions;

    public CoordinateToVectorTransformer(int boardWidth, int boardHeight)
    {
        for(int x=0; x<boardWidth; x++)
        {
            for(int y=0; y<boardWidth; y++)
            {
                _coordinatePositions[new Coordinate(x,y)] = GetPosition(x,y);
            }
        }
    }

    private static Vector2 GetPosition(int x, int y)
    {
        //Some implementation goes here...
    }

    public Vector2 Transform(Coordinate coordinate)
    {
        return _coordinatePositions[coordinate];
    }
}

Si scopre, forse sorprendentemente, che tutto il comportamento di cui avevamo veramente bisogno da una coordinata per raggiungere il nostro obiettivo era il controllo dell'uguaglianza! Naturalmente, questa soluzione è adattata a questo problema e fa ipotesi sull'uso / prestazioni della memoria accettabili. È solo un esempio che si adatta a questo particolare dominio problematico, piuttosto che un progetto per una soluzione generale.

E ancora, le opinioni varieranno se in pratica si tratta di una complessità inutile. In alcuni casi, tale soluzione non potrebbe esistere o potrebbe essere proibitiva o complessa in modo proibitivo, nel qual caso è possibile tornare ai tre precedenti.


Splendidamente risposto! Vorrei accettare, ma prima alcuni commenti: 1. Penso che toDTO () sia ottimo, perché non accedi al get / set, il che ti consente di cambiare i campi dati a DTO senza rompere il codice esistente. 2. Supponi che il Cliente abbia un comportamento sufficiente per giustificare la sua trasformazione in entità, come accederesti agli oggetti di scena per modificarli, ad esempio cambio indirizzo / tel ecc.
IntelliData,

@IntelliData 1. Quando dici "cambia i campi", intendi cambiare la definizione della classe o mutare i dati? Quest'ultimo può essere semplicemente evitato rimuovendo i setter pubblici ma lasciando i getter, quindi l'aspetto dto è irrilevante. Il primo non è in realtà la (intera) ragione per cui i vincitori pubblici sono "cattivi". Vedi ad esempio programmers.stackexchange.com/questions/157526/… .
Ben Aaronson,

@IntelliData 2. È difficile rispondere senza conoscere il comportamento. Ma probabilmente la risposta è: non lo faresti. Quale comportamento potrebbe avere una Customerclasse che richiede di poter mutare il suo numero di telefono? Forse il numero di telefono del cliente cambia e ho bisogno di insistere su quel cambiamento nel database, ma nulla di tutto ciò è responsabilità di un oggetto di dominio che fornisce comportamenti. Questo è un problema di accesso ai dati, e probabilmente verrebbe gestito con un DTO e, diciamo, un repository.
Ben Aaronson,

@IntelliData Mantenere i Customerdati dell'oggetto di dominio relativamente aggiornati (in sincronia con il db) è una questione di gestione del suo ciclo di vita, che non è anche sua responsabilità, e probabilmente finirebbe per vivere in un repository o in una fabbrica o in un contenitore IOC o qualunque cosa istanzia Customers.
Ben Aaronson,

2
Mi piace molto il concetto di Design for Behaviour . Ciò informa la "struttura di dati" sottostante e aiuta a evitare le classi anemiche e difficili da usare fin troppo comuni. più uno.
radarbob,

71

Il modo più semplice per evitare i setter è consegnare i valori al metodo del costruttore quando si newsolleva l'oggetto. Questo è anche il solito schema quando si desidera rendere immutabile un oggetto . Detto questo, le cose non sono sempre così chiare nel mondo reale.

È vero che i metodi dovrebbero riguardare il comportamento. Tuttavia, alcuni oggetti, come il Cliente, esistono principalmente per contenere informazioni. Questi sono i tipi di oggetti che traggono il massimo beneficio da getter e setter; se non ci fosse alcun bisogno di tali metodi, li elimineremmo del tutto.

Ulteriori letture
quando sono giustificati Getter e setter


3
quindi perché tutto l'hype su "Getter / Setters" è malvagio?
IntelliData,

46
Solo la solita pontificazione da parte di sviluppatori di software che dovrebbero conoscere meglio. Per l'articolo che hai collegato, l'autore sta usando la frase "getter e setter sono cattivi" per attirare la tua attenzione, ma ciò non significa che l'affermazione sia categoricamente vera.
Robert Harvey,

12
@IntelliData alcune persone ti diranno che Java stesso è malvagio.
null

18
@MetaFightsetEvil(null);

4
Certamente eval è il Male Incarnato. O un cugino vicino.
vescovo,

58

Va benissimo avere un oggetto che espone i dati piuttosto che il comportamento. Lo chiamiamo semplicemente "oggetto dati". Il modello esiste sotto nomi come Data Transfer Object o Value Object. Se lo scopo dell'oggetto è conservare i dati, allora i getter e i setter sono validi per accedere ai dati.

Quindi perché qualcuno dovrebbe dire "i metodi getter e setter sono cattivi"? Lo vedrai molto: qualcuno prende una linea guida che è perfettamente valida in un contesto specifico e quindi rimuove il contesto per ottenere un titolo più incisivo. Ad esempio, " favorire la composizione sull'eredità " è un ottimo principio, ma presto qualcuno rimuoverà il contesto e scriverà " Perché l'estensione è malvagia " (ehi, stesso autore, che coincidenza!) O "l' eredità è cattiva e deve essere distrutto ".

Se si guarda al contenuto dell'articolo in realtà ha alcuni punti validi, si estende solo il punto per creare un titolo di click-baity. Ad esempio, l'articolo afferma che i dettagli di implementazione non devono essere esposti. Questi sono i principi di incapsulamento e nascondimento dei dati che sono fondamentali in OO. Tuttavia, un metodo getter non espone per definizione i dettagli di implementazione. Nel caso di un oggetto dati cliente , le proprietà di Nome , Indirizzo ecc. Non sono dettagli di implementazione ma piuttosto l'intero scopo dell'oggetto e dovrebbero far parte dell'interfaccia pubblica.

Leggi la continuazione dell'articolo a cui ti colleghi, per vedere come suggerisce di impostare proprietà come "nome" e "stipendio" su un oggetto "Dipendente" senza l'uso dei setter malvagi. Si scopre che usa un modello con un 'Esportatore' che è popolato con metodi chiamati aggiungi nome, aggiungi stipendio che a sua volta imposta campi con lo stesso nome ... Quindi alla fine finisce usando esattamente il modello setter, solo con un convenzione di denominazione diversa.

È come pensare di evitare le insidie ​​dei singoli, rinominandoli in una sola volta mantenendo la stessa implementazione.


In oop, i cosiddetti esperti sembrano dire che non è così.
IntelliData,

8
Poi di nuovo, alcune persone hanno detto 'Non usare mai OOP': harmful.cat-v.org/software/OO_programming
JacquesB

7
FTR, penso che più "esperti" insegnino ancora a creare senza senso getter / setter per tutto , piuttosto che a non crearne affatto. IMO, quest'ultimo è un consiglio meno fuorviante.
lasciato circa

4
@leftaroundabout: OK, ma sto suggerendo una via di mezzo tra 'sempre' e 'mai' che è 'usa quando appropriato'.
Jacques B,

3
Penso che il problema sia che molti programmatori trasformano ogni oggetto (o troppi oggetti) in un DTO. A volte sono necessari, ma evitali il più possibile poiché separano i dati dal comportamento. (Supponendo che tu sia un OOPer devoto)
user949300,

11

Per trasformare la Customerclasse da un oggetto dati, possiamo porci le seguenti domande sui campi di dati:

Come vogliamo usare {campo dati}? Dove viene utilizzato {campo dati}? L'utilizzo di {data field} può e deve essere spostato nella classe?

Per esempio:

  • Qual è lo scopo di Customer.Name?

    Possibili risposte, visualizzare il nome in una pagina Web di accesso, utilizzare il nome negli invii al cliente.

    Il che porta a metodi:

    • Customer.FillInTemplate (...)
    • Customer.IsApplicableForMailing (...)
  • Qual è lo scopo di Customer.DOB?

    Convalida dell'età del cliente. Sconti per il compleanno del cliente. Mailing.

    • Customer.IsApplicableForProduct ()
    • Customer.GetPersonalDiscount ()
    • Customer.IsApplicableForMailing ()

Dati i commenti, l'oggetto di esempio Customer- sia come oggetto dati che come oggetto "reale" con le proprie responsabilità - è troppo ampio; cioè ha troppe proprietà / responsabilità. Ciò porta a molti componenti a seconda di Customer(leggendone le proprietà) o a Customerseconda di molti componenti. Forse esistono diversi punti di vista del cliente, forse ognuno dovrebbe avere la propria distinta classe 1 :

  • Il cliente nel contesto di Accounte transazioni monetarie è probabilmente utilizzato solo per:

    • aiutare gli umani a identificare che il loro trasferimento di denaro va alla persona corretta; e
    • gruppo Accounts.

    Questo cliente non ha bisogno di campi come DOB, FavouriteColour, Tel, e forse neppure Address.

  • Il cliente nel contesto di un utente che accede a un sito Web bancario.

    I campi rilevanti sono:

    • FavouriteColour, che potrebbe presentarsi sotto forma di temi personalizzati;
    • LanguagePreferences, e
    • GreetingName

    Invece di proprietà con getter e setter, questi potrebbero essere acquisiti in un unico metodo:

    • PersonaliseWebPage (pagina Modello);
  • Il cliente nel contesto del marketing e della corrispondenza personalizzata.

    Qui non fare affidamento sulle proprietà di un oggetto dati, ma invece a partire dalle responsabilità dell'oggetto; per esempio:

    • IsCustomerInterestedInAction (); e
    • GetPersonalDiscounts ().

    Il fatto che questo oggetto cliente abbia una FavouriteColourproprietà e / o una Addressproprietà diventa irrilevante: forse l'implementazione utilizza queste proprietà; ma potrebbe anche utilizzare alcune tecniche di apprendimento automatico e utilizzare le interazioni precedenti con il cliente per scoprire a quali prodotti potrebbe essere interessato il cliente.


1. Naturalmente, le classi Customere Accounterano esempi e, per un semplice esempio o esercizio di compiti a casa, dividere questo cliente potrebbe essere eccessivo, ma con l'esempio di divisione, spero di dimostrare che il metodo di trasformare un oggetto dati in un oggetto con le responsabilità funzioneranno.


4
Valorizza perché stai effettivamente rispondendo alla domanda :) Tuttavia è anche ovvio che le soluzioni proposte sono molto peggio del solo avere gettes / setter - ad esempio FillInTemplate rompe chiaramente il principio di separazione delle preoccupazioni. Il che dimostra che la premessa della domanda è errata.
Jacques B

@Kasper van den Berg: E quando hai molti attributi nel Cliente come di solito, come li imposteresti inizialmente?
IntelliData,

2
@IntelliData è probabile che i tuoi valori provengano da un database, XML, ecc. Il cliente, o un cliente, li legge, quindi li imposta usando (es. In Java) l'accesso privato / pacchetto / classe interna, o, (ick) riflessione. Non perfetto, ma di solito puoi evitare i setter pubblici. (Vedi la mia risposta per maggiori dettagli)
user949300

6
Non credo che la classe dei clienti dovrebbe conoscere nessuna di queste cose.
CodesInCos

Che ne dici di qualcosa del genere Customer.FavoriteColor?
Gabe,

8

TL; DR

  • Modellare il comportamento è buono.

  • Modellare per buone (!) Astrazioni è meglio.

  • A volte sono richiesti oggetti dati.


Comportamento e astrazione

Esistono diversi motivi per evitare getter e setter. Uno è, come hai notato, per evitare la modellazione dei dati. Questa è in realtà la ragione minore. Il motivo principale è fornire astrazione.

Nel tuo esempio con il conto bancario che è chiaro: un setBalance()metodo sarebbe davvero male perché l'impostazione di un saldo non è ciò per cui un conto dovrebbe essere usato. Il comportamento dell'account dovrebbe essere il più possibile astratto dal suo saldo attuale. Può prendere in considerazione il saldo quando decide se fallire un prelievo, può dare accesso al saldo corrente, ma la modifica dell'interazione con un conto bancario non dovrebbe richiedere all'utente di calcolare il nuovo saldo. Questo è ciò che l'account dovrebbe fare da solo.

Anche una coppia di deposit()e withdraw()metodi non è l'ideale per modellare un conto bancario. Un modo migliore sarebbe quello di fornire solo un transfer()metodo che prende un altro account e un importo come argomenti. Ciò consentirebbe alla classe di account di assicurarsi banalmente di non creare / distruggere accidentalmente denaro nel proprio sistema, fornirebbe un'astrazione molto utilizzabile e fornirebbe agli utenti maggiori informazioni perché costringerebbe l'uso di account speciali per guadagnati / investito / perso soldi (vedi doppio di contabilità ). Ovviamente, non tutti gli usi di un account richiedono questo livello di astrazione, ma vale sicuramente la pena considerare quanta astrazione possono fornire le tue classi.

Nota che fornire astrazioni e nascondere dati interni non è sempre la stessa cosa. Quasi tutte le applicazioni contengono classi che sono effettivamente solo dati. Tuple, dizionari e matrici sono esempi frequenti. Non vuoi nascondere all'utente la coordinata x di un punto. C'è pochissima astrazione che puoi / dovresti fare con un punto.


La classe del cliente

Un cliente è certamente un'entità nel tuo sistema che dovrebbe cercare di fornire astrazioni utili. Ad esempio, dovrebbe probabilmente essere associato a un carrello della spesa e la combinazione del carrello e del cliente dovrebbe consentire di effettuare un acquisto, il che potrebbe dare il via a azioni come l'invio dei prodotti richiesti, l'addebito di denaro (tenendo conto del pagamento selezionato metodo), ecc.

Il problema è che tutti i dati che hai citato non sono solo associati a un cliente, ma anche tutti i dati sono mutabili. Il cliente può spostarsi. Possono cambiare la compagnia della loro carta di credito. Possono cambiare il loro indirizzo e-mail e numero di telefono. Cavolo, possono anche cambiare il loro nome e / o sesso! Pertanto, una classe di clienti con funzionalità complete deve effettivamente fornire un accesso di modifica completo a tutti questi elementi di dati.

Tuttavia, i setter possono / dovrebbero fornire servizi non banali: possono assicurare il formato corretto degli indirizzi e-mail, la verifica degli indirizzi postali, ecc. Allo stesso modo, i "getter" possono fornire servizi di alto livello come fornire indirizzi e-mail nel Name <user@server.com>formato utilizzando i campi nome e l'indirizzo e-mail depositato, oppure fornire un indirizzo postale correttamente formattato, ecc. Naturalmente, quale delle funzionalità di alto livello ha senso dipende in larga misura dal caso d'uso. Potrebbe essere eccessivo o potrebbe richiedere un'altra classe per farlo bene. La scelta del livello di astrazione non è facile.


suona bene, anche se non sono d'accordo sulla parte sessuale ...;)
IntelliData

6

Cercando di espandere la risposta di Kasper, è più facile sfidare ed eliminare i setter. In un argomento piuttosto vago, fatto a mano (e spero umoristico):

Quando cambierebbe mai Customer.Name?

Raramente. Forse si sono sposati. O è andato nella protezione dei testimoni. Ma in quel caso dovresti anche controllare e possibilmente cambiare la loro residenza, i parenti più stretti e altre informazioni.

Quando cambierebbe mai il DOB?

Solo sulla creazione iniziale o su un errore di immissione dati. O se sono un giocatore di baseball di Domincan. :-)

Questi campi non dovrebbero essere accessibili con setter normali e di routine. Forse hai un Customer.initialEntry()metodo o un Customer.screwedUpHaveToChange()metodo che richiede autorizzazioni speciali. Ma non ho un Customer.setDOB()metodo pubblico .

Di solito un cliente viene letto da un database, un'API REST, alcuni XML, qualunque cosa. Avere un metodo Customer.readFromDB()o, se si è più severi su SRP / separazione delle preoccupazioni, si avrebbe un costruttore separato, ad esempio un CustomerPersisteroggetto con un read()metodo. Internamente, in qualche modo impostano i campi (preferisco usare l'accesso al pacchetto o una classe interna, YMMV). Ma ancora una volta, evitare i setter pubblici.

(L'addendum come domanda è leggermente cambiato ...)

Supponiamo che la tua applicazione faccia ampio uso di database relazionali. Sarebbe sciocco avere Customer.saveToMYSQL()o Customer.readFromMYSQL()metodi. Ciò crea un accoppiamento indesiderabile a un'entità concreta, non standard e suscettibile di cambiare entità. Ad esempio, quando si modifica lo schema o si passa a Postgress o Oracle.

Tuttavia, IMO, è perfettamente accettabile per accoppiare clienti ad un livello astratto , ResultSet. Un oggetto helper separato (lo chiamerò CustomerDBHelper, che probabilmente è una sottoclasse di AbstractMySQLHelper) conosce tutte le complesse connessioni al tuo DB, conosce i dettagli di ottimizzazione difficili, conosce le tabelle, le query, i join, ecc ... (o usa un ORM come Hibernate) per generare il ResultSet. Il tuo oggetto parla con ResultSet, che è uno standard astratto , che probabilmente non cambierà. Quando si modifica il database sottostante o si modifica lo schema, il Cliente non cambia , ma CustomerDBHelper lo fa. Se sei fortunato, è solo AbstractMySQLHelper che cambia che apporta automaticamente le modifiche per Cliente, Commerciante, Spedizione ecc ...

In questo modo è possibile (forse) evitare o ridurre la necessità di getter e setter.

E, il punto principale dell'articolo di Holub, confronta e confronta quanto sopra con come sarebbe se tu usassi getter e setter per tutto e cambi il database.

Allo stesso modo, supponiamo che tu usi molto XML. IMO, va bene accoppiare il tuo cliente a uno standard astratto, come un Python xml.etree.ElementTree o un Java org.w3c.dom.Element . Il cliente ottiene e si imposta da quello. Ancora una volta, puoi (forse) ridurre la necessità di getter e setter.


diresti che è consigliabile utilizzare un Builder Pattern?
IntelliData,

Builder è utile per rendere la costruzione di un oggetto più semplice e più robusta e, se lo si desidera, consentire all'oggetto di essere immutabile. Tuttavia, espone ancora (parzialmente) il fatto che l'oggetto sottostante ha un campo DOB, quindi non è la fine dell'ape.
user949300,

1

Il problema di disporre di getter e setter può dipendere dal fatto che una classe può essere utilizzata nella logica aziendale in un modo, ma si possono anche avere classi di supporto per serializzare / deserializzare i dati da un database o file o altro archivio persistente.

A causa del fatto che esistono molti modi per archiviare / recuperare i dati e si desidera disaccoppiare gli oggetti dati dal modo in cui sono archiviati, l'incapsulamento può essere "compromesso" rendendo pubblici questi membri o rendendoli accessibili tramite getter e setter che è quasi cattivo quanto renderli pubblici.

Ci sono vari modi per aggirare questo. Un modo è rendere i dati disponibili a un "amico". Sebbene l'amicizia non sia ereditata, ciò può essere superato da qualsiasi serializzatore che richieda le informazioni all'amico, ovvero il serializzatore di base che "inoltra" le informazioni.

La tua classe potrebbe avere un metodo generico "fromMetadata" o "toMetadata". I metadati costruiscono un oggetto, quindi potrebbe essere un costruttore. Se si tratta di un linguaggio tipizzato in modo dinamico, i metadati sono piuttosto standard per un tale linguaggio e probabilmente sono il modo principale per costruire tali oggetti.

Se la tua lingua è specificamente C ++, un modo per aggirare questo è avere una "struttura" pubblica di dati e quindi per la tua classe avere un'istanza di questa "struttura" come membro e in effetti tutti i dati che intendi archiviare / recuperare per essere memorizzati in esso. È quindi possibile scrivere facilmente "wrapper" per leggere / scrivere i dati in più formati.

Se il tuo linguaggio è C # o Java che non hanno "struct", puoi farlo in modo simile ma la tua struttura ora è una classe secondaria. Non esiste un vero concetto di "proprietà" dei dati o della costanza, quindi se si distribuisce un'istanza della classe contenente i propri dati ed è tutto pubblico, qualunque cosa venga acquisita può modificarli. Potresti "clonarlo" anche se può essere costoso. In alternativa, puoi fare in modo che questa classe disponga di dati privati ​​ma utilizza gli accessori. Ciò offre agli utenti della tua classe un modo indiretto per accedere ai dati, ma non è l'interfaccia diretta con la tua classe ed è davvero un dettaglio nella memorizzazione dei dati della classe che è anche un caso d'uso.


0

OOP riguarda il comportamento incapsulante e hidding all'interno degli oggetti. Gli oggetti sono scatole nere. Questo è un modo di progettare qualcosa. L'asset è in molti casi non è necessario conoscere lo stato interno di un altro componente ed è meglio non conoscerlo. È possibile applicare quell'idea principalmente con interfacce o all'interno di un oggetto con visibilità e avendo cura che solo i verbi / azioni consentiti siano disponibili per il chiamante.

Questo funziona bene per qualche tipo di problema. Ad esempio nelle interfacce utente per modellizzare i singoli componenti dell'interfaccia utente. Quando si interrompe con una casella di testo, si è interessati solo a impostare il testo, a ottenerlo o ad ascoltare l'evento di modifica del testo. In genere non si viene interrotti da dove si trova il cursore, il carattere utilizzato per disegnare il testo o come viene utilizzata la tastiera. L'incapsulamento fornisce molto qui.

Al contrario, quando chiami un servizio di rete, fornisci un input esplicito. Di solito c'è una grammatica (come in JSON o XML) e tutte le opzioni per chiamare il servizio non hanno motivo di essere nascoste. L'idea è che è possibile chiamare il servizio nel modo desiderato e il formato dei dati è pubblico e pubblicato.

In questo caso, o in molti altri (come l'accesso a un database), lavori davvero con dati condivisi. Come tale non c'è motivo di nasconderlo, al contrario si desidera renderlo disponibile. Può esserci preoccupazione per l'accesso in lettura / scrittura o la coerenza del controllo dati, ma in questo nucleo, il concetto di base se questo è pubblico.

Per tale requisito di progettazione in cui si desidera evitare l'incapsulamento e rendere le cose pubbliche e in chiaro, si desidera evitare oggetti. Ciò di cui hai veramente bisogno sono le tuple, i costrutti C o il loro equivalente, non gli oggetti.

Ma succede anche in linguaggi come Java, le uniche cose che puoi modellare sono oggetti o matrici di oggetti. Gli oggetti stessi possono contenere alcuni tipi di nativi (int, float ...) ma questo è tutto. Ma gli oggetti possono anche comportarsi come una semplice struttura con soli campi pubblici e tutto il resto.

Quindi, se modellizzi i dati, puoi farlo solo con campi pubblici all'interno degli oggetti perché non hai bisogno di altro. Non usi l'incapsulamento perché non ti serve. Questo viene fatto in questo modo in molte lingue. In java, storicamente, una rosa standard in cui con getter / setter si poteva almeno avere il controllo di lettura / scrittura (non aggiungendo setter per esempio) e che gli strumenti e il framework usando l'API di instrospection avrebbero cercato i metodi getter / setter e usarlo per compilare automaticamente il contenuto o visualizzare le tesi come campi modificabili nell'interfaccia utente generata automaticamente.

C'è anche l'argomento che potresti aggiungere un po 'di logica / controllo nel metodo setter.

In realtà non c'è quasi alcuna giustificazione per getter / setter poiché sono spesso usati per modellare dati puri. Framework e sviluppatori che usano i tuoi oggetti si aspettano che il getter / setter non faccia altro che impostare / ottenere i campi comunque. Con getter / setter non stai effettivamente facendo di più di quello che si potrebbe fare con i campi pubblici.

Ma quelle vecchie abitudini e vecchie abitudini sono difficili da rimuovere ... Potresti anche essere minacciato dai tuoi colleghi o insegnanti se non metti ciechi / setter ciecamente ovunque se non hanno lo sfondo per capire meglio cosa sono e cosa sono non.

Probabilmente avresti bisogno di cambiare la lingua per ottenere il passaggio di tutti questi codici getter / setter della caldaia. (Come C # o lisp). Per me getter / setter sono solo un altro errore da un miliardo di dollari ...


6
Le proprietà C # non implementano realmente l'incapsulamento più di quanto non facciano i getter e i setter ...
IntelliData,

1
Il vantaggio di [gs] etters è che puoi fare ciò che di solito non fai (controllare i valori, informare gli osservatori), simulare campi inesistenti ecc., E soprattutto che puoi cambiarlo in seguito . Con Lombok , il boilerplate è andato: @Getter @Setter class MutablePoint3D {private int x, y, z;}.
maaartinus,

1
@maaartinus Sicuramente [gs] etter può fare qualsiasi cosa come qualsiasi altro metodo può fare. Ciò significa che qualsiasi codice chiamante deve essere consapevole che qualsiasi valore impostato può generare un'eccezione, essere modificato o inviare una notifica di modifica ... o qualsiasi altra cosa. Quel setter più o meno [gs] non fornisce accesso a un campo ma fa codice arbitrario.
Nicolas Bousquet,

Le proprietà di C # di @IntelliData consentono di non scrivere 1 singolo carattere inutile di boilerplate e di non preoccuparsi di tutte queste cose getter / setter ... Questo è già un risultato migliore rispetto al progetto lombok. Anche per me un POJO con solo getter / setter non è qui per fornire l'incapsulamento ma piuttosto per pubblicare un formato di dati che si è liberi di leggere o scrivere come scambio con un servizio. L'incapsulamento è quindi l'opposto del requisito di progettazione.
Nicolas Bousquet,

Non penso proprio che le proprietà siano davvero buone. Certo, si salva il prefisso e le parentesi, cioè 5 caratteri per chiamata, ma 1. sembrano campi, il che è confuso. 2. sono una cosa aggiuntiva per la quale hai bisogno di supporto nella riflessione. Nessun grosso problema, ma nemmeno un grande vantaggio (rispetto a Java + Lombok; Java puro è in evidente perdita).
maaartinus,

0

Quindi, ad esempio, data una classe Customer che mantiene molte informazioni sul cliente, ad esempio Nome, DOB, Tel, Indirizzo ecc., Come si eviterebbero getter / setter per ottenere e impostare tutti quegli attributi? Quale metodo di tipo 'Behavior' si può scrivere per popolare tutti quei dati?

Penso che questa domanda sia pungente perché sei preoccupato per i metodi di comportamento per il popolamento dei dati, ma non vedo alcuna indicazione del comportamento che la Customerclasse di oggetti intende incapsulare.

Non confondere Customercome classe di oggetti con "Cliente" come utente / attore che esegue diverse attività utilizzando il software.

Quando dici una classe di clienti che tiene molto se le informazioni sul cliente , per quanto riguarda il comportamento, sembra che la tua classe di clienti abbia poco la distingue da una roccia. A Rockpuò avere un colore, potresti dargli un nome, potresti avere un campo per memorizzare il suo indirizzo attuale ma non ci aspettiamo alcun tipo di comportamento intelligente da una roccia.

Dall'articolo collegato sui getter / setter che sono cattivi:

Il processo di progettazione OO è incentrato su casi d'uso: un utente esegue attività autonome che hanno alcuni risultati utili. (L'accesso non è un caso d'uso perché manca di un risultato utile nel dominio problematico. Disegnare una busta paga è un caso d'uso.) Un sistema OO, quindi, implementa le attività necessarie per riprodurre i vari scenari che comprendono un caso d'uso.

Senza alcun comportamento definito, riferirsi a un rock come a Customernon cambia il fatto che è solo un oggetto con alcune proprietà che vorresti tracciare e non importa quali trucchi vuoi giocare per allontanarti dai vincitori e setter. A un rock non importa se ha un nome valido e un rock non dovrebbe sapere se un indirizzo è valido o meno.

Il tuo sistema di ordini potrebbe associare Rocka un ordine di acquisto e purché Rocksia definito un indirizzo, una parte del sistema può assicurarsi che un articolo venga consegnato a una roccia.

In tutti questi casi si Rocktratta solo di un oggetto dati e continuerà ad esserlo fino a quando non definiremo comportamenti specifici con esiti utili anziché ipotetici.


Prova questo:

Quando si evita di sovraccaricare la parola "cliente" con 2 significati potenzialmente diversi, si dovrebbe rendere le cose più facili da concettualizzare.

Un Rockoggetto inserisce un Ordine o è qualcosa che fa un essere umano facendo clic sugli elementi dell'interfaccia utente per attivare azioni nel tuo sistema?


Il cliente è un attore che fa molte cose, ma ha anche molte informazioni associate; ciò giustifica la creazione di 2 classi separate, 1 come attore e 1 come oggetto dati?
IntelliData,

@IntelliData che passa oggetti ricchi attraverso i livelli è dove le cose si complicano. Se si invia l'oggetto da un controller a una vista, la vista deve comprendere il contratto generale dell'oggetto (ad esempio lo standard JavaBeans). Se stai inviando l'oggetto attraverso il filo, JaxB o simili devono poterlo reinserire in un oggetto stupido (perché non hai dato loro l'intero oggetto ricco). Gli oggetti ricchi sono meravigliosi per la manipolazione dei dati - sono poveri per il trasferimento dello stato. Al contrario, gli oggetti stupidi sono scarsi per la manipolazione dei dati e ok per il trasferimento dello stato.

0

Aggiungo qui i miei 2 centesimi citando l' approccio agli oggetti che parlano SQL .

Questo approccio si basa sulla nozione di oggetto autonomo. Ha tutte le risorse necessarie per implementare il suo comportamento. Non ha bisogno di essere detto come fare il suo lavoro - la richiesta dichiarativa è sufficiente. E un oggetto sicuramente non deve contenere tutti i suoi dati come proprietà di classe. Non importa - e non dovrebbe - importare da dove vengono.

Parlare di un aggregato , l'immutabilità non è un problema. Supponiamo che tu abbia una sequenza di stati che possono contenere aggregati: è Radice aggregata come saga del tutto corretto implementare ogni stato come oggetto autonomo. Probabilmente potresti andare ancora oltre: fai una chiacchierata con il tuo esperto di dominio. È probabile che lui o lei non vedano questo aggregato come un'entità unificata. Probabilmente ogni stato ha il suo significato, merita il suo oggetto.

Infine, vorrei notare che il processo di ricerca degli oggetti è molto simile alla decomposizione del sistema in sottosistemi . Entrambi si basano sul comportamento, non su altro.

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.