Jak unikać getterów i seterów?

85

Mam trochę trudności z projektowaniem zajęć w sposób oo. Czytałem, że obiekty ujawniają swoje zachowanie, a nie dane; dlatego zamiast używać getter / setters do modyfikowania danych, metodami danej klasy powinny być „czasowniki” lub akcje działające na obiekcie. Na przykład, w „Konto” obiektu, musielibyśmy metody Withdraw()i Deposit()zamiast setAmount()itd. Patrz: Dlaczego getter i setter są złe .

Na przykład, biorąc pod uwagę klasę klienta, która przechowuje wiele informacji o kliencie, np. Imię, nazwisko, numer telefonu, adres itp., W jaki sposób można uniknąć getter / setters w celu uzyskania i ustawienia wszystkich tych atrybutów? Jaką metodą typu „zachowanie” można zapisać, aby wypełnić wszystkie te dane?

IntelliData
źródło
8
Chciałbym podkreślić, że jeśli mamy do czynienia ze specyfikacją Java Beans , to będzie mieć mając pobierające i ustawiające. Wiele rzeczy używa Java Beans (język wyrażeń w jsps) i próba uniknięcia takich będzie prawdopodobnie ... trudna.
4
... i, z przeciwnej strony niż MichaelT: jeśli nie używasz specyfikacji JavaBeans (i osobiście uważam, że powinieneś jej unikać, chyba że używasz swojego obiektu w kontekście, w którym jest to konieczne), to nie ma potrzeby brodawka „get” na getterach, szczególnie w przypadku nieruchomości, które nie mają żadnego odpowiedniego setera. Myślę, że metoda nazywa name()on Customerjest jasne, czy wyraźniejsze, niż metody zwanej getName().
Daniel Pryden
2
@Daniel Pryden: name () może oznaczać zarówno ustawienie, jak i pobranie? ...
IntelliData

Odpowiedzi:

55

Jak stwierdzono w kilku odpowiedziach i komentarzach, DTO odpowiednie i przydatne w niektórych sytuacjach, szczególnie w przesyłaniu danych ponad granicami (np. Przesyłanie seriali do JSON w celu wysłania za pośrednictwem usługi internetowej). W pozostałej części tej odpowiedzi z grubsza to zignoruję i porozmawiam o klasach domen oraz o tym, jak można je zaprojektować, aby zminimalizować (jeśli nie wyeliminować) funkcje pobierające i ustawiające, i nadal będą przydatne w dużym projekcie. Nie będę też mówić o tym, dlaczego usuwaj osoby pobierające lub ustawiające, ani kiedy to robić, ponieważ są to pytania same w sobie.

Jako przykład wyobraź sobie, że twój projekt to gra planszowa, taka jak Szachy lub Pancernik. Możliwe są różne sposoby przedstawienia tego w warstwie prezentacji (aplikacja konsoli, usługa internetowa, GUI itp.), Ale masz także domenę podstawową. Jedną z klas, które możesz mieć, jest Coordinatereprezentowanie pozycji na planszy. „Złym” sposobem napisania byłoby:

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

(Zamierzam pisać przykłady kodu w języku C # zamiast w Javie, dla zwięzłości i ponieważ jestem bardziej zaznajomiony z tym. Mam nadzieję, że to nie jest problem. Koncepcje są takie same, a tłumaczenie powinno być proste.)

Usuwanie seterów: niezmienność

Podczas gdy publiczni zdobywcy i seterzy są potencjalnie problematyczni, setery są znacznie bardziej „złymi” z tych dwóch. Zazwyczaj są one również łatwiejsze do wyeliminowania. Proces ten jest prosty - ustaw wartość w konstruktorze. Wszelkie metody, które wcześniej zmutowały obiekt, powinny zamiast tego zwrócić nowy wynik. Więc:

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;
    }
}

Zauważ, że to nie chroni przed innymi metodami w klasie mutującymi X i Y. Aby być bardziej niezmiennym, możesz użyć readonly( finalw Javie). Ale tak czy inaczej - niezależnie od tego, czy uczynisz swoje nieruchomości naprawdę niezmiennymi, czy po prostu zapobiegniesz bezpośredniej mutacji publicznej przez seterów - robi to sztuczkę polegającą na usunięciu twoich publicznych seterów. W zdecydowanej większości przypadków działa to dobrze.

Usuwanie getterów, część 1: Projektowanie pod kątem zachowania

Wszystko to jest dobre i dobre dla seterów, ale jeśli chodzi o gettery, faktycznie strzeliliśmy sobie w stopę, zanim jeszcze zaczęliśmy. Nasz proces polegał na wymyśleniu współrzędnych - reprezentowanych przez nie danych - i stworzeniu wokół nich klasy. Zamiast tego powinniśmy zacząć od tego, jakiego zachowania potrzebujemy od współrzędnych. Nawiasem mówiąc, proces ten jest wspomagany przez TDD, w którym klasy takie wyodrębniamy tylko wtedy, gdy są potrzebne, więc zaczynamy od pożądanego zachowania i stamtąd.

Powiedzmy, że pierwsze miejsce, w którym znalazłeś potrzebę, to Coordinatewykrywanie kolizji: chciałeś sprawdzić, czy dwa elementy zajmują to samo miejsce na planszy. Oto „zły” sposób (konstruktorów pominięto dla zwięzłości):

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;
    }

A oto dobry sposób:

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;
    }
}

( IEquatableimplementacja w skrócie dla uproszczenia). Projektując zachowanie, a nie modelując dane, udało nam się usunąć nasze programy pobierające.

Uwaga: dotyczy to również twojego przykładu. Być może używasz ORM lub wyświetlasz informacje o kliencie na stronie internetowej lub czymś, w którym to przypadku pewna Customermetoda DTO prawdopodobnie miałaby sens. Ale to, że w systemie są klienci i są reprezentowani w modelu danych, nie oznacza automatycznie, że powinieneś mieć Customerklasę w swojej domenie. Być może podczas projektowania zachowań pojawi się jeden, ale jeśli chcesz uniknąć getterów, nie twórz go z wyprzedzeniem.

Usuwanie getterów, część 2: Zachowanie zewnętrzne

Więc powyższe to dobry początek, ale prędzej czy później będzie prawdopodobnie działać w sytuacji, gdy masz problem, który jest skojarzony z klasą, która w jakiś sposób zależy od stanu klasy, ale które nie należą na klasy. Tego rodzaju zachowanie zwykle występuje w warstwie usługowej aplikacji.

Biorąc nasz Coordinateprzykład, ostatecznie będziesz chciał przedstawić swoją grę użytkownikowi, a to może oznaczać rysowanie na ekranie. Możesz na przykład mieć projekt interfejsu użytkownika, który Vector2reprezentuje punkt na ekranie. Ale byłoby niewłaściwe, gdyby Coordinateklasa przejmowała konwersję ze współrzędnych na punkt na ekranie - to wprowadzałoby wszelkie obawy związane z prezentacją do twojej podstawowej domeny. Niestety tego rodzaju sytuacja jest nieodłącznie związana z projektowaniem OO.

Pierwszą opcją , która jest bardzo często wybierana, jest po prostu odsłonić tych cholernych łapaczy i powiedzieć z nią do diabła. Ma to tę zaletę prostoty. Ale ponieważ mówimy o unikaniu getterów, powiedzmy, że ze względu na argumenty odrzucamy ten i sprawdzamy, jakie są inne opcje.

Drugą opcją jest dodanie .ToDTO()metody do swojej klasy. To lub podobne może i tak być potrzebne, na przykład, jeśli chcesz zapisać grę, musisz uchwycić prawie cały swój stan. Ale różnica między robieniem tego dla twoich usług a samym bezpośrednim dostępem do gettera jest mniej lub bardziej estetyczna. Nadal ma tyle samo „zła”.

Trzecią opcją - którą widziałem zalecaną przez Zorana Horvata w kilku filmach Pluralsight - jest użycie zmodyfikowanej wersji wzorca odwiedzającego. Jest to dość nietypowe zastosowanie i odmiana wzoru i myślę, że przebieg ludzi będzie się znacznie różnił w zależności od tego, czy zwiększy złożoność bez rzeczywistego zysku, czy też jest to miły kompromis w tej sytuacji. Chodzi przede wszystkim o użycie standardowego wzorca odwiedzającego, ale Visitmetody powinny przyjmować stan, którego potrzebują, jako parametry, zamiast klasy, którą odwiedzają. Przykłady można znaleźć tutaj .

W przypadku naszego problemu rozwiązaniem wykorzystującym ten wzorzec byłoby:

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);
    }
}

Jak zapewne wiesz, _xi _ytak naprawdę nie są już zamknięte. Możemy je wyodrębnić, tworząc taki, IPositionTransformer<Tuple<int,int>>który zwraca je bezpośrednio. W zależności od gustu może to powodować, że całe ćwiczenie jest bezcelowe.

Jednak w przypadku publicznych programów pobierających bardzo łatwo jest robić rzeczy w niewłaściwy sposób, po prostu wyciągając dane bezpośrednio i używając ich z naruszeniem instrukcji Tell, Don't Ask . Zważywszy, że za pomocą tego wzoru to faktycznie prościej zrobić to we właściwy sposób: gdy chcesz utworzyć zachowanie, będziesz automatycznie rozpocząć tworząc rodzaj z nim związane. Naruszenia TDA będą bardzo śmierdzące i prawdopodobnie będą wymagać obejścia prostszego, lepszego rozwiązania. W praktyce punkty te znacznie ułatwiają robienie tego we właściwy sposób, niż „zły” sposób, który zachęcają osoby pobierające.

Wreszcie , nawet jeśli początkowo nie jest to oczywiste, mogą istnieć sposoby na ujawnienie wystarczającej ilości tego, czego potrzebujesz jako zachowanie, aby uniknąć konieczności ujawnienia stanu. Na przykład, używając naszej poprzedniej wersji, Coordinatektórej jedynym członkiem publicznym jest Equals()(w praktyce wymagałoby to pełnej IEquatableimplementacji), możesz napisać następującą klasę w warstwie prezentacji:

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];
    }
}

Okazuje się, być może zaskakujące, że wszystkie zachowania, których naprawdę potrzebowaliśmy od współrzędnych do osiągnięcia naszego celu, to sprawdzanie równości! Oczywiście to rozwiązanie jest dostosowane do tego problemu i przyjmuje założenia dotyczące akceptowalnego wykorzystania / wydajności pamięci. To tylko przykład, który pasuje do tej konkretnej domeny problemowej, a nie plan ogólnego rozwiązania.

I znowu, opinie będą się różnić, czy w praktyce jest to niepotrzebna złożoność. W niektórych przypadkach takie rozwiązanie może nie istnieć lub może być nadmiernie dziwne lub złożone, w którym to przypadku możesz powrócić do powyższych trzech.

Ben Aaronson
źródło
Pięknie odpowiedział! Chciałbym zaakceptować, ale najpierw kilka komentarzy: 1. Myślę, że toDTO () jest świetne, ponieważ nie masz dostępu do get / set, co pozwala ci zmienić pola podane do DTO bez łamania istniejącego kodu. 2. Powiedz, że Klient ma wystarczające zachowanie, aby uzasadnić uczynienie go bytem, ​​w jaki sposób uzyskasz dostęp do rekwizytów w celu ich modyfikacji, np. Zmiana adresu / tel. Itp.
IntelliData
@IntelliData 1. Kiedy mówisz „zmień pola”, masz na myśli zmianę definicji klasy lub mutację danych? Tego drugiego można po prostu uniknąć, usuwając ustawiających publicznych, ale pozostawiając getterów, więc aspekt dto jest nieistotny. To pierwsze nie jest tak naprawdę (całym) powodem, dla którego publiczni zdobywcy są „źli”. Zobacz na przykład programmers.stackexchange.com/questions/157526/ ...
Ben Aaronson
@IntelliData 2. Trudno jest odpowiedzieć bez znajomości zachowania. Ale prawdopodobnie odpowiedź brzmi: nie zrobiłbyś tego. Jakie zachowanie może Customermieć klasa, która wymaga mutacji swojego numeru telefonu? Być może zmienia się numer telefonu klienta i muszę zachować tę zmianę w bazie danych, ale to nie jest odpowiedzialność za obiekt domeny zapewniający zachowanie. Jest to problem z dostępem do danych i prawdopodobnie zostałby rozwiązany za pomocą DTO i, powiedzmy, repozytorium.
Ben Aaronson
@IntelliData Utrzymywanie Customerwzględnie świeżych danych obiektu domeny (w synchronizacji z bazą danych) jest kwestią zarządzania jego cyklem życia, co również nie jest jego własną odpowiedzialnością, i prawdopodobnie ponownie trafiłoby do repozytorium, fabryki lub kontenera MKOl lub cokolwiek wystąpi Customers.
Ben Aaronson
2
I bardzo podoba mi się projekt za zachowanie koncepcji. Informuje to podstawową „strukturę danych” i pomaga uniknąć zbyt często anemicznych, trudnych w użyciu klas. plus jeden.
radarbob
71

Najprostszym sposobem uniknięcia seterów jest przekazanie wartości do metody konstruktora, gdy obiekt jest newpodniesiony. Jest to również zwykły wzór, gdy chcesz uczynić obiekt niezmiennym. To powiedziawszy, w prawdziwym świecie rzeczy nie zawsze są tak jasne.

Prawdą jest, że metody powinny dotyczyć zachowania. Jednak niektóre obiekty, takie jak Klient, istnieją przede wszystkim do przechowywania informacji. Są to obiekty, które najbardziej korzystają z pobierających i ustawiających; gdyby takie metody nie były wcale potrzebne, po prostu całkowicie je wyeliminowalibyśmy.

Dalsza lektura,
kiedy Gettery i Settery są uzasadnione

Robert Harvey
źródło
3
więc dlaczego cały szum o „Getter / Setters” jest zły?
IntelliData
46
Tylko zwykła pontyfikacja przez programistów, którzy powinni wiedzieć lepiej. W artykule, który podlinkowałeś, autor używa frazy „pobierający i ustawiający są źli”, aby zwrócić twoją uwagę, ale to nie znaczy, że stwierdzenie jest kategorycznie prawdziwe.
Robert Harvey
12
@IntelliData niektórzy powiedzą ci, że sama java jest zła.
zera
18
@MetaFightsetEvil(null);
4
Na pewno ewaluacja jest Zła Wcielona. Lub bliski kuzyn.
biskup
58

Zupełnie dobrze jest mieć obiekt, który ujawnia dane, a nie zachowanie. Nazywamy to po prostu „obiektem danych”. Wzorzec istnieje pod nazwami, takimi jak Obiekt transferu danych lub Obiekt wartości. Jeśli celem obiektu jest przechowywanie danych, pobierające i ustawiające są ważne, aby uzyskać dostęp do danych.

Dlaczego więc ktoś miałby mówić „metody pobierające i ustawiające są złe”? Zobaczysz to bardzo często - ktoś bierze wytyczną, która jest całkowicie poprawna w określonym kontekście, a następnie usuwa kontekst, aby uzyskać bardziej uderzający nagłówek. Na przykład „ faworyzowanie kompozycji zamiast dziedziczenia ” jest dobrą zasadą, ale wkrótce ktoś usunie kontekst i napisze „ Dlaczego rozszerzenie jest złe ” (hej, ten sam autor, co za zbieg okoliczności!) Lub „ dziedzictwo jest złe i musi być zniszczone ”.

Jeśli spojrzysz na treść artykułu, to ma on pewne ważne punkty, po prostu rozciąga punkt, aby utworzyć nagłówek przynęty kliknięcia. Na przykład w artykule stwierdzono, że szczegóły implementacji nie powinny być ujawniane. Takie są zasady enkapsulacji i ukrywania danych, które są fundamentalne w OO. Jednak metoda gettera z definicji nie ujawnia szczegółów implementacji. W przypadku obiektu danych Klienta właściwości Nazwa , Adres itp. Nie są szczegółami implementacji, ale raczej całym celem obiektu i powinny być częścią interfejsu publicznego.

Przeczytaj kontynuację artykułu, do którego linkujesz, aby zobaczyć, jak sugeruje on ustawianie właściwości takich jak „imię” i „pensja” na obiekcie „Pracownik” bez użycia złych seterów. Okazuje się, że używa wzorca z „eksporterem”, który jest zapełniony metodami o nazwie dodaj nazwę, dodaj wynagrodzenie, które z kolei ustawia pola o tej samej nazwie ... W końcu ostatecznie używa dokładnie wzoru ustawiającego, tylko z inna konwencja nazewnictwa.

To tak, jakbyś myślał, że unikniesz pułapek singletonów, zmieniając ich nazwy tylko na inne, zachowując tę ​​samą implementację.

JacquesB
źródło
Tak się składa, że ​​tak zwani eksperci zdają się mówić, że tak nie jest.
IntelliData
8
Z drugiej strony, niektórzy ludzie powiedzieli „Nigdy nie używaj OOP”: szkodliwe. Cat-v.org/software/OO_programming
JacquesB
7
FTR, myślę, że więcej „ekspertów” nadal uczy, jak bezsensownie tworzyć pobierających / ustawiających wszystko , niż nigdy w ogóle ich nie tworzyć. IMO, ta ostatnia jest mniej mylącą radą.
lewo około
4
@leftaroundabout: OK, ale sugeruję środkową płaszczyznę między „zawsze” a „nigdy”, która jest „użyj, gdy jest to właściwe”.
JacquesB
3
Myślę, że problem polega na tym, że wielu programistów przekształca każdy obiekt (lub zbyt wiele obiektów) w DTO. Czasami są konieczne, ale unikaj ich w jak największym stopniu, ponieważ oddzielają dane od zachowania. (Zakładając, że jesteś pobożnym OOPerem)
user949300
11

Aby przekształcić Customerklasę z obiektu danych, możemy zadać sobie następujące pytania dotyczące pól danych:

Jak chcemy korzystać z {data field}? Gdzie jest używane {pole danych}? Czy można i należy przenieść użycie {pola danych} do klasy?

Na przykład:

  • Jaki jest cel Customer.Name?

    Możliwe odpowiedzi, wyświetl nazwę na stronie logowania, użyj nazwy w mailach do klienta.

    Co prowadzi do metod:

    • Customer.FillInTemplate (…)
    • Customer.IsApplicableForMailing (…)
  • Jaki jest cel Customer.DOB?

    Sprawdzanie wieku klienta. Zniżki na urodziny klienta. Mailingi

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

Biorąc pod uwagę komentarze, przykładowy obiekt Customer- zarówno jako obiekt danych, jak i „prawdziwy” obiekt z własnymi obowiązkami - jest zbyt szeroki; tzn. ma zbyt wiele właściwości / obowiązków. Co prowadzi do wielu składników w zależności od Customer(poprzez odczytanie jego właściwości) lub do Customerwielu składników. Być może istnieją różne poglądy klienta, być może każdy powinien mieć własną odrębną klasę 1 :

  • Klient w kontekście Accounttransakcji pieniężnych i jest prawdopodobnie używany tylko do:

    • pomagać ludziom rozpoznać, że ich przekaz pieniężny trafia do właściwej osoby; i
    • grupa Accounts.

    Ten klient nie potrzebuje pola, na przykład DOB, FavouriteColour, Tel, a może nawet nie Address.

  • Klient w kontekście użytkownika logującego się na stronie bankowej.

    Odpowiednie pola to:

    • FavouriteColour, które mogą przybrać formę spersonalizowanych tematów;
    • LanguagePreferences, i
    • GreetingName

    Zamiast właściwości z modułami pobierającymi i ustawiającymi można je przechwycić za pomocą jednej metody:

    • PersonaliseWebPage (strona szablonu);
  • Klient w kontekście marketingu i spersonalizowanej korespondencji.

    Tutaj nie polegamy na właściwościach obiektu danych, lecz zaczynamy od obowiązków obiektu; na przykład:

    • IsCustomerInterestedInAction (); i
    • GetPersonalDiscounts ().

    Fakt, że ten obiekt klienta ma FavouriteColourwłaściwość i / lub Addresswłaściwość, staje się nieistotny: być może implementacja korzysta z tych właściwości; ale może również korzystać z niektórych technik uczenia maszynowego i wykorzystywać wcześniejsze interakcje z klientem, aby odkryć, które produkty mogą być zainteresowane.


1. Oczywiście klasy Customeri Accountbyły przykładami, a dla prostego przykładu lub zadania domowego rozdzielenie tego klienta może być przesadą, ale na przykładzie podziału mam nadzieję wykazać, że metoda przekształcenia obiektu danych w obiekt z obowiązki będą działać.

Kasper van den Berg
źródło
4
Głosuj, bo tak naprawdę odpowiadasz na pytanie :) Jednak oczywiste jest również, że proponowane rozwiązania są znacznie gorsze niż posiadanie gettes / setterów - np. FillInTemplate wyraźnie łamie zasadę rozdzielania obaw. Co tylko pokazuje, że przesłanka pytania jest błędna.
JacquesB,
@Kasper van den Berg: A jeśli masz wiele atrybutów u klienta, jak to zwykle bywa, jak byś je początkowo ustawił?
IntelliData
2
@IntelliData Twoje wartości prawdopodobnie pochodzą z bazy danych, XML itp. Klient lub CustomerBuilder czyta je, a następnie ustawia za pomocą (tj. W Javie) dostępu prywatnego / pakietu / klasy wewnętrznej lub (ick) refleksji. Nie jest to idealne, ale zwykle można uniknąć publicznych seterów. (Więcej szczegółów znajdziesz w mojej odpowiedzi)
user949300
6
Nie sądzę, że klasa klientów powinna wiedzieć o którejkolwiek z tych rzeczy.
CodesInChaos
Co powiesz na coś takiego Customer.FavoriteColor?
Gabe,
8

TL; DR

  • Modelowanie zachowania jest dobre.

  • Modelowanie dobrych (!) Abstrakcji jest lepsze.

  • Czasami wymagane są obiekty danych.


Zachowanie i abstrakcja

Istnieje kilka powodów, dla których należy unikać getterów i seterów. Jednym z nich jest, jak zauważyłeś, unikanie modelowania danych. To jest właściwie drobny powód. Głównym powodem jest zapewnienie abstrakcji.

W twoim przykładzie z kontem bankowym, które jest jasne: setBalance()Metoda byłaby naprawdę zła, ponieważ ustawienie salda nie jest tym, do czego powinno się używać konta. Zachowanie konta powinno jak najbardziej odbiegać od jego bieżącego salda. Może brać pod uwagę saldo przy podejmowaniu decyzji o niepowodzeniu wypłaty, może dać dostęp do bieżącego salda, ale modyfikacja interakcji z kontem bankowym nie powinna wymagać od użytkownika obliczenia nowego salda. To samo powinno zrobić konto.

Nawet parę deposit()i withdraw()metod nie jest idealna do modelowania konta bankowego. Lepszym sposobem byłoby zapewnienie tylko jednej transfer()metody, która bierze pod uwagę inne konto i kwotę jako argumenty. Pozwoliłoby to klasie kont na trywialne upewnienie się, że przypadkowo nie utworzysz / nie zniszczysz pieniędzy w systemie, zapewniłoby to bardzo użyteczną abstrakcję i zapewniłoby użytkownikom więcej wglądu, ponieważ wymusiłoby użycie specjalnych kont dla zarobione / zainwestowane / utracone pieniądze (patrz podwójne księgowanie ). Oczywiście nie każde użycie konta wymaga takiego poziomu abstrakcji, ale zdecydowanie warto zastanowić się, ile abstrakcji mogą zapewnić twoje klasy.

Pamiętaj, że zapewnianie abstrakcji i ukrywanie wewnętrznych danych nie zawsze jest tym samym. Prawie każda aplikacja zawiera klasy, które są właściwie tylko danymi. Krotki, słowniki i tablice są częstymi przykładami. Nie chcesz ukrywać współrzędnej x punktu przed użytkownikiem. Jest bardzo mało abstrakcji, którą możesz / powinieneś robić z racją.


Klasa klienta

Klient jest z pewnością podmiotem w twoim systemie, który powinien spróbować dostarczyć użytecznych abstrakcji. Na przykład prawdopodobnie powinien być powiązany z koszykiem, a połączenie koszyka z klientem powinno pozwolić na dokonanie zakupu, co może zainicjować takie działania, jak wysłanie mu żądanych produktów, obciążenie go pieniędzmi (biorąc pod uwagę wybraną przez niego płatność metoda) itp.

Najważniejsze jest to, że wszystkie dane, o których wspomniałeś, są nie tylko powiązane z klientem, ale wszystkie te dane można modyfikować. Klient może się przenieść. Mogą zmienić wystawcę karty kredytowej. Mogą zmienić swój adres e-mail i numer telefonu. Cholera, mogą nawet zmienić nazwisko i / lub płeć! Tak więc w pełni funkcjonalna klasa klienta rzeczywiście musi zapewniać pełny dostęp modyfikujący do wszystkich tych elementów danych.

Mimo to ustawiający mogą / powinni świadczyć nietrywialne usługi: mogą zapewnić prawidłowy format adresów e-mail, weryfikację adresów pocztowych itp. Podobnie „osoby pobierające” mogą świadczyć usługi na wysokim poziomie, takie jak udostępnianie adresów e-mail w Name <[email protected]>formacie używając pól nazw i zdeponowanego adresu e-mail, lub podaj poprawnie sformatowany adres pocztowy itp. Oczywiście, co z tej funkcji wysokiego poziomu ma sens, zależy w dużej mierze od twojego przypadku użycia. Może to być całkowita przesada lub może wezwać inną klasę, aby zrobiła to dobrze. Wybór poziomu abstrakcji nie jest łatwy.

cmaster
źródło
brzmi dobrze, chociaż nie zgadzam się z seksem ...;)
IntelliData
6

Próbując rozwinąć odpowiedź Kaspera, najłatwiej jest zwalczać i eliminować seterów. W dość niejasnym, wymachującym ręką (i miejmy nadzieję dowcipnym) sporze:

Kiedy Customer.Name kiedykolwiek się zmieni?

Rzadko. Może wzięli ślub. Lub poszedł na ochronę świadków. Ale w takim przypadku chciałbyś również sprawdzić i ewentualnie zmienić ich miejsce zamieszkania, najbliższych krewnych i inne informacje.

Kiedy DOB kiedykolwiek się zmieni?

Tylko przy początkowym utworzeniu lub przy wkręcaniu danych. Lub jeśli są dominikańskim graczem w baseball. :-)

Te pola nie powinny być dostępne dla rutynowych, normalnych seterów. Może masz Customer.initialEntry()metodę lub Customer.screwedUpHaveToChange()metodę wymagającą specjalnych uprawnień. Ale nie mam publicznej Customer.setDOB()metody.

Zwykle klient jest odczytywany z bazy danych, interfejsu API REST, niektórych plików XML, cokolwiek. Mieć metodę Customer.readFromDB()lub, jeśli jesteś bardziej rygorystyczny w kwestii SRP / separacji problemów, będziesz miał oddzielny konstruktor, np. CustomerPersisterObiekt z read()metodą. Wewnętrznie jakoś ustawiają pola (wolę używać dostępu do pakietu lub wewnętrznej klasy YMMV). Ale znowu unikaj publicznych seterów.

(Dodatek, ponieważ pytanie nieco się zmieniło ...)

Powiedzmy, że twoja aplikacja intensywnie korzysta z relacyjnych baz danych. Byłoby głupotą mieć metody Customer.saveToMYSQL()lub Customer.readFromMYSQL()metody. To tworzy niepożądane połączenie z konkretnym, niestandardowym i prawdopodobnie zmieniającym podmiot. Na przykład po zmianie schematu lub zmianie na Postgress lub Oracle.

Jednak IMO, to całkowicie dopuszczalne, aby para Klienta do abstrakcyjnego standardzie , ResultSet. Oddzielny obiekt pomocnika (nazywam go CustomerDBHelper, który prawdopodobnie jest podklasą AbstractMySQLHelper) wie o wszystkich złożonych połączeniach z bazą danych, zna trudne szczegóły dotyczące optymalizacji, zna tabele, zapytania, sprzężenia itp. (Lub używa ORM jak Hibernacja), aby wygenerować zestaw wyników. Twój przedmiot do rozmowy ResultSet, który jest abstrakcyjny standardem , raczej się nie zmieni. Po zmianie podstawowej bazy danych lub schemacie Klient się nie zmienia , ale CustomerDBHelper tak. Jeśli masz szczęście, zmienia się tylko AbstractMySQLHelper, który automatycznie wprowadza zmiany dla klienta, sprzedawcy, wysyłki itp.

W ten sposób możesz (być może) uniknąć lub zmniejszyć potrzebę pobierania i ustawiania.

I głównym punktem artykułu Holub, porównaj i porównaj powyższe z tym, jak by to było, gdybyś użył getterów i settererów do wszystkiego i zmienił bazę danych.

Podobnie, powiedzmy, że używasz dużo XML. IMO, możesz połączyć klienta z abstrakcyjnym standardem, takim jak Python xml.etree.ElementTree lub Java org.w3c.dom.Element . Klient otrzymuje i ustawia się z tego. Ponownie możesz (być może) zmniejszyć potrzebę pobierania i ustawiania.

użytkownik949300
źródło
czy powiedziałbyś, że wskazane jest użycie Wzorca konstruktora?
IntelliData
Konstruktor jest przydatny, aby uczynić konstrukcję obiektu łatwiejszym i bardziej niezawodnym, a jeśli chcesz, pozwolić, aby obiekt był niezmienny. Jednak nadal (częściowo) ujawnia fakt, że obiekt leżący pod spodem ma pole DOB, więc nie jest to koniec wszystkiego.
user949300
1

Kwestia posiadania programów pobierających i ustawiających może wynikać z faktu, że klasa może być używana w logice biznesowej w jeden sposób, ale możesz także mieć klasy pomocnicze do serializacji / deserializacji danych z bazy danych lub pliku lub innego trwałego magazynu.

Z uwagi na fakt, że istnieje wiele sposobów przechowywania / pobierania danych i chcesz oddzielić obiekty danych od sposobu ich przechowywania, hermetyzacja może być „zagrożona” przez upublicznienie tych członków lub udostępnienie ich za pośrednictwem programów pobierających i seterów, co jest prawie tak złe, jak ich upublicznienie.

Są na to różne sposoby. Jednym ze sposobów jest udostępnienie danych „przyjacielowi”. Chociaż przyjaźń nie jest dziedziczona, można temu zaradzić przez dowolny serializator żądający informacji od przyjaciela, tj. Podstawowy serializator „przekazuje” informacje.

Twoja klasa może mieć ogólną metodę „fromMetadata” lub „toMetadata”. Z metadanych konstruuje obiekt, więc równie dobrze może być konstruktorem. Jeśli jest to język dynamicznie typowany, metadane są dość standardowe dla takiego języka i prawdopodobnie są podstawowym sposobem konstruowania takich obiektów.

Jeśli twoim językiem jest język C ++, jednym z rozwiązań jest posiadanie publicznej „struktury” danych, a następnie, aby klasa miała instancję tej „struktury” jako członka, a właściwie wszystkie dane, które zamierzasz przechowywać / odzyskać do zapisania w nim. Następnie możesz łatwo napisać „opakowania”, aby odczytać / zapisać dane w wielu formatach.

Jeśli twoim językiem jest C # lub Java, które nie mają „struktur”, możesz zrobić podobnie, ale teraz jest to klasa dodatkowa. Nie ma prawdziwej koncepcji „własności” danych ani ciągłości, więc jeśli podasz instancję klasy zawierającej twoje dane i będzie ona publicznie dostępna, wszystko, co zostanie utrzymane, może ją zmodyfikować. Możesz go „sklonować”, chociaż może to być kosztowne. Alternatywnie możesz sprawić, aby ta klasa miała prywatne dane, ale korzystały z akcesoriów. Daje to użytkownikom twojej klasy rondo sposób na uzyskanie dostępu do danych, ale nie jest to bezpośredni interfejs z twoją klasą i jest to naprawdę szczegół w przechowywaniu danych klasy, co również jest przypadkiem użycia.

Dojną krową
źródło
0

OOP polega na enkapsulacji i ukrywaniu zachowań wewnątrz obiektów. Obiekty to czarne skrzynki. To jest sposób na zaprojektowanie rzeczy. Zaletą jest to, że w wielu przypadkach nie trzeba znać stanu wewnętrznego innego komponentu i lepiej jest tego nie znać. Możesz wymusić ten pomysł głównie za pomocą interfejsów lub wewnątrz obiektu z widocznością i dbając tylko o dozwolone czasowniki / działania są dostępne dla dzwoniącego.

Działa to dobrze w przypadku jakiegoś problemu. Na przykład w interfejsach użytkownika do modelowania poszczególnych składników interfejsu użytkownika. Kiedy interaktujesz z polem tekstowym, jesteś zainteresowany tylko ustawieniem tekstu, uzyskaniem go lub odsłuchaniem zdarzenia zmiany tekstu. Zazwyczaj nie interesuje Cię, gdzie znajduje się kursor, czcionka użyta do narysowania tekstu lub sposób użycia klawiatury. Kapsułkowanie zapewnia tutaj wiele.

Przeciwnie, gdy dzwonisz do usługi sieciowej, podajesz wyraźne dane wejściowe. Zwykle jest tam gramatyka (jak w JSON lub XML) i wszystkie opcje wywołania usługi nie mają powodu do ukrywania. Chodzi o to, że możesz wywołać usługę tak, jak chcesz, a format danych jest publiczny i opublikowany.

W tym przypadku lub wielu innych (takich jak dostęp do bazy danych) naprawdę pracujesz z udostępnionymi danymi. Jako taki nie ma powodu, aby go ukrywać, wręcz przeciwnie, chcesz go udostępnić. Mogą istnieć obawy o dostęp do odczytu / zapisu lub spójność sprawdzania danych, ale w tym rdzeniu jest to podstawowa koncepcja, jeśli jest ona publiczna.

Dla takiego wymogu projektowego, w którym chcesz uniknąć enkapsulacji i upublicznić rzeczy, i oczywiście unikać obiektów. Tak naprawdę potrzebujesz krotek, struktur C lub ich odpowiedników, a nie obiektów.

Ale dzieje się tak również w językach takich jak Java, jedyne, co możesz modelować, to obiekty lub tablice obiektów. Obiekty te mogą pomieścić kilka typów tubylców (int, float ...), ale to wszystko. Ale obiekty mogą również zachowywać się jak zwykła struktura z tylko polami publicznymi i tym wszystkim.

Więc jeśli modelujesz dane, możesz to zrobić za pomocą publicznych pól wewnątrz obiektów, ponieważ nie potrzebujesz więcej. Nie używasz enkapsulacji, ponieważ jej nie potrzebujesz. Odbywa się to w wielu językach. Historycznie w javie standardowa róża, w której za pomocą gettera / settera można przynajmniej mieć kontrolę odczytu / zapisu (nie dodając na przykład settera), a narzędzia i frameworki przy użyciu interfejsu API instrospection szukałyby metod getter / setter i używały go do autouzupełnianie treści lub wyświetlanie tez jako modyfikowalnych pól w automatycznie generowanym interfejsie użytkownika.

Jest też argument, że można dodać trochę logiki / sprawdzania w metodzie settera.

W rzeczywistości prawie nie ma uzasadnienia dla getter / setter, ponieważ są one najczęściej używane do modelowania czystych danych. Frameworki i programiści używający twoich obiektów oczekują, że getter / setter i tak zrobią tylko ustawianie / pobieranie pól. W rzeczywistości nie robisz nic więcej z getter / setter niż to, co można zrobić z polami publicznymi.

Ale to stare nawyki i stare nawyki są trudne do usunięcia ... Możesz nawet być zagrożony przez twoich kolegów lub nauczyciela, jeśli nie umieścisz getterów / seterów wszędzie na ślepo, jeśli nie mają tła, aby lepiej zrozumieć, czym są i czym są nie.

Prawdopodobnie będziesz musiał zmienić język, aby uzyskać dostęp do wszystkich kodów pobierających / ustawiających tezy. (Jak C # lub lisp). Dla mnie getters / setter to kolejny błąd o wartości miliarda dolarów ...

Nicolas Bousquet
źródło
6
Właściwości C # tak naprawdę nie implementują enkapsulacji tak samo jak gettery i settery ...
IntelliData
1
Zaletą [gs] etters jest to, że możesz robić rzeczy, których zwykle nie robisz (sprawdzać wartości, powiadamiać obserwatorów), symulować nieistniejące pola itp., A przede wszystkim to, że możesz to później zmienić . Z Lombok The boilerplate zniknął: @Getter @Setter class MutablePoint3D {private int x, y, z;}.
maaartinus
1
@maaartinus Na pewno [gs] etter może zrobić wszystko, jak każda inna metoda. Oznacza to, że każdy kod dzwoniącego powinien wiedzieć, że każda ustawiona przez niego wartość może zgłosić wyjątek, zostać zmieniona lub potencjalnie wysłać powiadomienie o zmianie ... lub cokolwiek innego. Że mniej więcej [gs] etter nie zapewnia dostępu do pola, ale wykonuje dowolny kod.
Nicolas Bousquet,
@IntelliData C # właściwości pozwalają nie pisać 1 bezużytecznego pojedynczego znaku na płycie kotła i nie przejmować się tymi wszystkimi getterami / ustawiaczami ... To już jest lepsze osiągnięcie niż projekt lombok. Również dla mnie POJO z tylko getterami / setterami nie jest tutaj, aby zapewnić enkapsulację, ale raczej opublikować format danych, który można swobodnie czytać lub pisać w ramach wymiany z usługą. Hermetyzacja jest zatem przeciwieństwem wymogu projektowego.
Nicolas Bousquet
Nie sądzę, żeby właściwości były naprawdę dobre. Jasne, zapisujesz prefiks i nawiasy, tj. 5 znaków na połączenie, ale 1. wyglądają jak pola, co jest mylące. 2. są dodatkową rzeczą, dla której potrzebujesz wsparcia w refleksji. Nie jest to wielka sprawa, ale też nie ma dużej przewagi (w porównaniu do Javy + Lombok; czysta Java ma wyraźną stratę).
maaartinus
0

Na przykład, biorąc pod uwagę klasę klienta, która utrzymuje dużo, jeśli informacje o kliencie, np. Imię, nazwisko, numer telefonu, adres itp., W jaki sposób można uniknąć getter / setterów, aby uzyskać i ustawić wszystkie te atrybuty? Jaką metodą typu „zachowanie” można zapisać, aby wypełnić wszystkie te dane?

Myślę, że to pytanie jest kłujące, ponieważ martwisz się metodami zapełniania danych, ale nie widzę żadnych oznak tego, jakie zachowanie Customerklasa obiektów ma zawierać.

Nie należy mylić Customerjako klasy obiektów z „Klientem” jako użytkownikiem / aktorem, który wykonuje różne zadania za pomocą oprogramowania.

Kiedy mówisz, że dana klasa klienta zachowuje wiele, jeśli informacje o kliencie, to jeśli chodzi o zachowanie, wygląda na to, że klasa klienta niewiele odróżnia ją od kamienia. A Rockmoże mieć kolor, możesz nadać mu nazwę, możesz mieć pole do przechowywania jego bieżącego adresu, ale nie oczekujemy żadnego inteligentnego zachowania od skały.

Z powiązanego artykułu o getterach / seterach będących złymi:

Proces projektowania OO koncentruje się na przypadkach użycia: użytkownik wykonuje samodzielne zadania, które przynoszą użyteczne rezultaty. (Logowanie nie jest przypadkiem użycia, ponieważ nie ma przydatnego wyniku w dziedzinie problemów. Wyciąganie wypłaty jest przypadkiem użycia.) Następnie system OO realizuje działania niezbędne do odtworzenia różnych scenariuszy, które składają się na przypadek użycia.

Bez zdefiniowanego zachowania odniesienie do skały jako do Customernie zmienia faktu, że jest to po prostu obiekt z pewnymi właściwościami, które chcesz śledzić, i nie ma znaczenia, jakie sztuczki chcesz zagrać, aby uciec od łapaczy i setery. Skała nie dba o to, czy ma prawidłową nazwę, a od skały nie można się dowiedzieć, czy adres jest prawidłowy, czy nie.

Twój system zamówień może kojarzyć się Rockz zamówieniem zakupu i dopóki Rockma on zdefiniowany adres, wówczas pewna część systemu może zapewnić dostawę przedmiotu do skały.

We wszystkich tych przypadkach Rockjest to po prostu Obiekt danych i pozostanie nim, dopóki nie zdefiniujemy konkretnych zachowań z przydatnymi wynikami zamiast hipotetycznych.


Spróbuj tego:

Kiedy unikniesz przeciążenia słowa „Klient” 2 potencjalnie różnymi znaczeniami, powinno to ułatwić konceptualizację.

Czy Rockobiekt składa zamówienie, czy też jest to coś, co robi człowiek, klikając elementy interfejsu użytkownika w celu uruchomienia akcji w systemie?

nvuono
źródło
Klient jest aktorem, który robi wiele rzeczy, ale zdarza się, że ma wiele powiązanych z nim informacji; czy to uzasadnia utworzenie 2 oddzielnych klas, 1 jako aktora i 1 jako obiektu danych?
IntelliData
@IntelliData przepuszczanie bogatych obiektów przez warstwy jest trudne. Jeśli wysyłasz obiekt z kontrolera do widoku, widok musi zrozumieć ogólną umowę obiektu (na przykład standard JavaBeans). Jeśli wysyłasz obiekt przez drut, JaxB lub podobny musi być w stanie przywrócić go do głupiego obiektu (ponieważ nie dałeś im pełnego bogatego obiektu). Bogate obiekty doskonale nadają się do manipulowania danymi - nie nadają się do przenoszenia stanu. I odwrotnie, głupie obiekty nie nadają się do manipulowania danymi i mogą przesyłać stan.
0

Dodam tutaj moje 2 centy, wspominając o podejściu do obiektów mówiących w języku SQL .

Podejście to opiera się na pojęciu obiektu zamkniętego. Ma wszystkie zasoby potrzebne do wdrożenia swojego zachowania. Nie trzeba mówić, jak wykonać swoją pracę - wystarczy deklaracja. Obiekt zdecydowanie nie musi przechowywać wszystkich swoich danych jako właściwości klasy. To naprawdę nie ma - i nie powinno - mieć znaczenia, skąd pochodzą.

Mówiąc o agregacie , niezmienność również nie stanowi problemu. Powiedzmy, że istnieje sekwencja stanów, które może zawierać agregacja: Zbiorczy korzeń jako saga za każdym razem można wdrożyć każdy stan jako samodzielny obiekt. Prawdopodobnie możesz pójść jeszcze dalej: porozmawiaj ze swoim ekspertem w dziedzinie. Są szanse, że on lub ona nie postrzega tego agregatu jako jakiegoś zjednoczonego bytu. Prawdopodobnie każdy stan ma swoje znaczenie, zasługując na swój własny przedmiot.

Na koniec chciałbym zauważyć, że proces wyszukiwania obiektów jest bardzo podobny w przypadku rozkładu systemu na podsystemy . Oba są oparte na zachowaniu, a nie na niczym innym.

Zapadło
źródło