Pracowałem nad refaktoryzacją kodu i myślę, że mogłem zrobić pierwszy krok w stronę króliczej nory. Piszę przykład w Javie, ale przypuszczam, że może być agnostyczny.
Mam interfejs Foo
zdefiniowany jako
public interface Foo {
int getX();
int getY();
int getZ();
}
I wdrożenie jako
public final class DefaultFoo implements Foo {
public DefaultFoo(int x, int y, int z) {
this.x = x;
this.y = y;
this.z = z;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
public int getZ() {
return z;
}
private final int x;
private final int y;
private final int z;
}
Mam również interfejs, MutableFoo
który zapewnia dopasowanie mutatorów
/**
* This class extends Foo, because a 'write-only' instance should not
* be possible and a bit counter-intuitive.
*/
public interface MutableFoo extends Foo {
void setX(int newX);
void setY(int newY);
void setZ(int newZ);
}
Istnieje kilka implementacji, MutableFoo
które mogą istnieć (jeszcze ich nie wdrożyłem). Jeden z nich jest
public final class DefaultMutableFoo implements MutableFoo {
/**
* A DefaultMutableFoo is not conceptually constructed
* without all values being set.
*/
public DefaultMutableFoo(int x, int y, int z) {
this.x = x;
this.y = y;
this.z = z;
}
public int getX() {
return x;
}
public void setX(int newX) {
this.x = newX;
}
public int getY() {
return y;
}
public void setY(int newY) {
this.y = newY;
}
public int getZ() {
return z;
}
public void setZ(int newZ) {
this.z = newZ;
}
private int x;
private int y;
private int z;
}
Powodem, dla którego je podzieliłem, jest to, że użycie każdego z nich jest równie prawdopodobne. Oznacza to, że równie prawdopodobne jest, że ktoś korzystający z tych klas będzie chciał instancji niezmiennej, ponieważ tak właśnie będzie chciał mieć zmienną instancję.
Podstawowym przykładem użycia, który mam, jest nazywany interfejs, StatSet
który reprezentuje pewne szczegóły walki dla gry (punkty życia, atak, obrona). Jednak „skuteczne” statystyki lub rzeczywiste statystyki są wynikiem statystyk podstawowych, których nigdy nie można zmienić, oraz wyszkolonych statystyk, które można zwiększyć. Te dwa są powiązane przez
/**
* The EffectiveStats can never be modified independently of either the baseStats
* or trained stats. As such, this StatSet must never provide mutators.
*/
public StatSet calculateEffectiveStats() {
int effectiveHitpoints =
baseStats.getHitpoints() + (trainedStats.getHitpoints() / 4);
int effectiveAttack =
baseStats.getAttack() + (trainedStats.getAttack() / 4);
int effectiveDefense =
baseStats.getDefense() + (trainedStats.getDefense() / 4);
return StatSetFactory.createImmutableStatSet(effectiveHitpoints, effectiveAttack, effectiveDefense);
}
Trenowane stany są zwiększane po każdej bitwie
public void grantExperience() {
int hitpointsReward = 0;
int attackReward = 0;
int defenseReward = 0;
final StatSet enemyStats = enemy.getEffectiveStats();
final StatSet currentStats = player.getEffectiveStats();
if (enemyStats.getHitpoints() >= currentStats.getHitpoints()) {
hitpointsReward++;
}
if (enemyStats.getAttack() >= currentStats.getAttack()) {
attackReward++;
}
if (enemyStats.getDefense() >= currentStats.getDefense()) {
defenseReward++;
}
final MutableStatSet trainedStats = player.getTrainedStats();
trainedStats.increaseHitpoints(hitpointsReward);
trainedStats.increaseAttack(attackReward);
trainedStats.increaseDefense(defenseReward);
}
ale nie są zwiększane tuż po bitwie. Używanie niektórych przedmiotów, stosowanie określonej taktyki, sprytne korzystanie z pola bitwy mogą zapewnić różne doświadczenia.
Teraz moje pytania:
- Czy istnieje nazwa dzielenia interfejsów przez akcesory i mutatory na osobne interfejsy?
- Czy podzielenie ich w ten sposób jest „właściwym” podejściem, jeśli są równie prawdopodobne, że zostaną użyte, czy też istnieje inny, bardziej akceptowany wzorzec, którego powinienem użyć (np.
Foo foo = FooFactory.createImmutableFoo();
Który mógłby zwrócićDefaultFoo
lubDefaultMutableFoo
ale jest ukryty, ponieważcreateImmutableFoo
zwracaFoo
)? - Czy są jakieś dające się natychmiast przewidzieć wady korzystania z tego wzorca, z wyjątkiem komplikowania hierarchii interfejsu?
Powodem, dla którego zacząłem projektować go w ten sposób, jest to, że uważam, że wszyscy realizatorzy interfejsu powinni stosować się do najprostszego możliwego interfejsu i nie dostarczać nic więcej. Dodając setery do interfejsu, teraz skuteczne statystyki można modyfikować niezależnie od jego części.
Stworzenie nowej klasy dla EffectiveStatSet
nie ma większego sensu, ponieważ w żaden sposób nie rozszerzamy funkcjonalności. Moglibyśmy zmienić implementację i stworzyć połączenie EffectiveStatSet
dwóch różnych StatSets
, ale uważam, że nie jest to właściwe rozwiązanie;
public class EffectiveStatSet implements StatSet {
public EffectiveStatSet(StatSet baseStats, StatSet trainedStats) {
// ...
}
public int getHitpoints() {
return baseStats.getHitpoints() + (trainedStats.getHitpoints() / 4);
}
}
Odpowiedzi:
Wydaje mi się, że masz rozwiązanie szukające problemu.
To może być trochę prowokujące, ale tak naprawdę nazwałbym to „nadprojektowaniem rzeczy” lub „nadmiernym komplikowaniem rzeczy”. Oferując zmienny i niezmienny wariant tej samej klasy, oferujesz dwa funkcjonalnie równoważne rozwiązania dla tego samego problemu, które różnią się jedynie niefunkcjonalnymi aspektami, takimi jak zachowanie wydajności, API i bezpieczeństwo przed skutkami ubocznymi. Wydaje mi się, że dzieje się tak dlatego, że boisz się podjąć decyzję, którą wybrać, lub dlatego, że próbujesz zaimplementować funkcję „const” w C ++ w C #. Myślę, że w 99% wszystkich przypadków nie zrobi to wielkiej różnicy, jeśli użytkownik wybierze wariant zmienny lub niezmienny, może rozwiązać swoje problemy z jednym lub drugim. Zatem „prawdopodobieństwo zastosowania klasy”
Wyjątkiem jest projektowanie nowego języka programowania lub uniwersalnego frameworka, który będzie używany przez około 10 tysięcy programistów lub więcej. Wtedy może rzeczywiście lepiej skalować, jeśli oferujesz niezmienne i zmienne warianty typów danych ogólnego przeznaczenia. Ale jest to sytuacja, w której będziesz mieć tysiące różnych scenariuszy użytkowania - co prawdopodobnie nie jest problemem, z którym się mierzysz, tak myślę?
„Bardziej akceptowany wzór” nazywa się KISS - bądź prosty i głupi. Podejmij decyzję dotyczącą lub przeciw zmienności dla konkretnej klasy / interfejsu w bibliotece. Na przykład, jeśli twój „StatSet” ma kilkanaście atrybutów lub więcej i są one najczęściej zmieniane indywidualnie, wolałbym wariant zmienny i po prostu nie modyfikowałbym podstawowych statystyk tam, gdzie nie powinny być modyfikowane. Dla czegoś w rodzaju
Foo
klasy z atrybutami X, Y, Z (wektor trójwymiarowy) prawdopodobnie wolałbym wariant niezmienny.Zbyt skomplikowane konstrukcje sprawiają, że oprogramowanie jest trudniejsze do testowania, trudniejsze w utrzymaniu, trudniejsze do ewolucji.
źródło
Może być na to nazwa, jeśli ta separacja jest przydatna i zapewnia korzyść, której nie widzę . Jeśli separaty nie przynoszą korzyści, pozostałe dwa pytania nie mają sensu.
Czy możesz nam powiedzieć o jakimkolwiek biznesowym przypadku, w którym dwa oddzielne interfejsy zapewniają nam korzyści, czy to pytanie jest problemem akademickim (YAGNI)?
Mogę myśleć o sklepie ze zmiennym wózkiem (możesz umieścić więcej artykułów), który może stać się zamówieniem, w którym klient może zmienić artykuły. Status zamówienia jest wciąż zmienny.
Implementacje, które widziałem, nie rozdzielają interfejsów
Wersja ReadOnly używa „WritableInterface” i zgłasza wyjątek, jeśli używana jest metoda zapisu
źródło
Oddzielenie modyfikowalnego interfejsu gromadzenia danych i interfejsu gromadzenia danych tylko do odczytu jest przykładem zasady segregacji interfejsów. Nie sądzę, aby nadano każdemu zastosowaniu zasady jakieś specjalne nazwy.
Zwróć uwagę na kilka słów tutaj: „umowa tylko do odczytu” i „kolekcja”.
Kontrakt tylko do odczytu oznacza, że klasa
A
daje klasieB
dostęp tylko do odczytu, ale nie oznacza, że podstawowy obiekt kolekcji jest niezmienny. Niezmienny oznacza, że nie zmieni się w przyszłości, bez względu na przedstawiciela. Umowa tylko do odczytu mówi tylko, że odbiorca nie może jej zmienić; ktoś inny (w szczególności klasaA
) może to zmienić.Aby obiekt stał się niezmienny, musi być naprawdę niezmienny - musi odmawiać prób zmiany danych bez względu na to, czy agent o nie prosi.
Wzorzec najprawdopodobniej obserwuje się na obiektach reprezentujących zbiór danych - listy, sekwencje, pliki (strumienie) itp.
Słowo „niezmienny” staje się modne, ale koncepcja nie jest nowa. Istnieje wiele sposobów wykorzystania niezmienności, a także wiele sposobów osiągnięcia lepszego projektu przy użyciu czegoś innego (tj. Jego konkurentów).
Oto moje podejście (nie oparte na niezmienności).
hitpoints
,attack
,defense
.A
musi przekazać DTO do klasyB
, tworzy jego kopię i zamiast tego przekazuje kopię.B
Może więc korzystać z DTO w dowolny sposób (pisząc do niego), bez wpływu na DTO, które sięA
utrzymuje.grantExperience
Funkcja dzieli się na dwie części:calculateNewStats
increaseStats
calculateNewStats
pobierze dane wejściowe z dwóch DTO, jeden reprezentujący statystyki gracza, a drugi reprezentujący statystyki wroga, i wykona obliczenia.hitpoints
,attack
,defense
) przechowuje kwota do zwiększany do tego zdolności.increaseStats
jest metodą stosowaną przez gracza (nie w DTO), która wykorzystuje DTO „kwota do zwiększenia” i stosuje ten przyrost w DTO, która jest własnością gracza i reprezentuje DTO gracza, którego można trenować.W przypadku
calculateNewStats
, gdy okaże się, że nie zależy od żadnej innej informacji o graczu lub wrogu (poza wartościami w dwóch wejściowych DTO), ta metoda może potencjalnie być zlokalizowana w dowolnym miejscu projektu.Jeśli
calculateNewStats
okaże się, że ma pełną zależność od obiektów gracza i wroga (tzn. Przyszłe obiekty gracza i wroga mogą mieć nowe właściwości icalculateNewStats
muszą zostać zaktualizowane, aby wykorzystać jak najwięcej ich nowych właściwości), wówczascalculateNewStats
należy zaakceptować te dwa przedmioty, nie tylko DTO. Jednak wynik jego obliczeń nadal będzie przyrostem DTO lub dowolnymi prostymi obiektami do przesyłania danych, które zawierają informacje używane do wykonania przyrostu / aktualizacji.źródło
Jest tu duży problem: fakt, że struktura danych jest niezmienna, nie oznacza, że nie potrzebujemy jej zmodyfikowanych wersji . Prawdziwy niezmienna wersja struktury danych ma zapewnić
setX
,setY
isetZ
metody - po prostu zwrócić nową strukturę zamiast modyfikowania obiekt, który nazwie je.Więc w jaki sposób dajesz jednej części systemu możliwość jej zmiany przy jednoczesnym ograniczeniu innych części? Zmienny obiekt zawierający odwołanie do instancji niezmiennej struktury. Zasadniczo, zamiast tego, że twoja klasa gracza jest w stanie mutować swój obiekt statystyk, jednocześnie dając każdej innej klasie niezmienny widok swoich statystyk, jego statystyki są niezmienne, a gracz jest zmienny. Zamiast:
Miałbyś:
Zobacz różnicę? Obiekt statystyk jest całkowicie niezmienny, ale obecne statystyki gracza są zmiennym odniesieniem do obiektu niezmiennego. W ogóle nie trzeba tworzyć dwóch interfejsów - po prostu spraw, aby struktura danych była całkowicie niezmienna i zarządzaj zmiennym odniesieniem do niej w miejscu, w którym używasz jej do przechowywania stanu.
Oznacza to, że inne obiekty w twoim systemie nie mogą polegać na odwołaniu do obiektu statystyk - obiekt statystyk, który mają, nie będzie tym samym obiektem statystyk, który gracz ma po aktualizacji statystyk.
W każdym razie ma to większy sens, ponieważ koncepcyjnie tak naprawdę nie zmieniają się statystyki , to bieżące statystyki gracza . Nie obiekt statystyk. Odniesienie do statystyk object obiekt gracz ma. Tak więc inne części twojego systemu, w zależności od tego odniesienia, powinny jawnie odwoływać się do
player.currentStats()
niektórych lub takich, zamiast uzyskiwać dostęp do obiektu statystyk gracza, przechowywać go gdzieś i polegać na jego aktualizacji poprzez mutacje.źródło
Wow ... to naprawdę zabiera mnie z powrotem. Kilka razy próbowałem tego samego pomysłu. Byłem zbyt uparty, żeby się poddać, ponieważ myślałem, że będzie coś dobrego do nauczenia się. Widziałem też, jak inni próbują tego w kodzie. Większość implementacji Widziałem nazywane Read-Only interfejsy jak twój
FooViewer
czyReadOnlyFoo
i interfejs zapisem TylkoFooEditor
czyWriteableFoo
lub w Javie Myślę, że widziałemFooMutator
kiedyś. Nie jestem pewien, czy istnieje oficjalne lub nawet wspólne słownictwo do robienia rzeczy w ten sposób.To nigdy nie pomogło mi w miejscach, w których próbowałem. Zupełnie bym tego unikał. Zrobiłbym to, co sugerują inni, i cofnę się o krok i zastanów się, czy naprawdę potrzebujesz tego pojęcia w swoim większym pomyśle. Nie jestem pewien, czy istnieje odpowiedni sposób, aby to zrobić, ponieważ nigdy nie zachowałem żadnego kodu, który stworzyłem, próbując tego. Za każdym razem wycofywałem się po znacznym wysiłku i upraszczałem rzeczy. Rozumiem przez to coś, co inni mówili o YAGNI, KISS i DRY.
Jeden możliwy minus: powtórzenie. Będziesz musiał stworzyć te interfejsy dla wielu klas potencjalnie, a nawet dla jednej klasy musisz nazwać każdą metodę i opisać każdą sygnaturę co najmniej dwa razy. Ze wszystkich rzeczy, które palą mnie w kodowaniu, zmiana wielu plików w ten sposób sprawia mi najwięcej problemów. W końcu zapominam o wprowadzeniu zmian w jednym lub drugim miejscu. Kiedy już będziesz mieć klasę Foo i interfejsy FooViewer i FooEditor, jeśli zdecydujesz, że lepiej byłoby nazwać ją Bar, zamiast tego trzykrotnie refaktor-> zmień nazwę, chyba że masz naprawdę niesamowicie inteligentne IDE. Stwierdziłem nawet, że tak jest w przypadku, gdy miałem znaczną ilość kodu pochodzącego z generatora kodu.
Osobiście nie lubię tworzyć interfejsów dla rzeczy, które mają tylko jedną implementację. Oznacza to, że kiedy podróżuję po kodzie, nie mogę po prostu przejść bezpośrednio do jedynej implementacji, muszę przejść do interfejsu, a następnie do implementacji interfejsu lub przynajmniej nacisnąć kilka dodatkowych klawiszy, aby się tam dostać. Te rzeczy się sumują.
A potem jest komplikacja, o której wspomniałeś. Wątpię, czy kiedykolwiek jeszcze pójdę w ten konkretny sposób z moim kodem. Nawet dla tego miejsca, które najlepiej pasuje do mojego ogólnego planu dla kodu (zbudowałem ORM), wyciągnąłem to i zastąpiłem go czymś łatwiejszym do kodowania.
Naprawdę bardziej zastanowiłbym się nad wspomnianą kompozycją. Jestem ciekawy, dlaczego uważasz, że byłby to zły pomysł. Liczyłam mieć
EffectiveStats
komponować i niezmiennaStats
i cośStatModifiers
co dalej komponować jakiś zestawStatModifier
s, które reprezentują co może modyfikować statystyk (efekty tymczasowe, artykuły, Rozmieszczenie w jakiejś dziedzinie poprawy, zmęczenie), ale twójEffectiveStats
nie musiałby zrozumieć boStatModifiers
BĘDZIE zarządzać, jakie były te rzeczy i jaki wpływ i jaki będą mieć na jakie statystyki. TheStatModifier
byłby interfejsem dla różnych rzeczy i mógłby wiedzieć takie rzeczy, jak „czy jestem w strefie”, „kiedy medycyna się zużyje” itp., ale nie musiałby nawet informować innych przedmiotów o takich rzeczach. Trzeba tylko powiedzieć, która statystyka i jak zmodyfikowana była teraz. Jeszcze lepiejStatModifier
byłoby po prostu ujawnić metodę wytwarzania nowej niezmiennościStats
opartej na innej,Stats
która byłaby inna, ponieważ została odpowiednio zmieniona. Możesz wtedy zrobić coś takiegocurrentStats = statModifier2.modify(statModifier1.modify(baseStats))
i wszystkoStats
może być niezmienne. Nie kodowałbym tego nawet bezpośrednio, prawdopodobnie przepuściłbym wszystkie modyfikatory i zastosowałbym każdy z wyników poprzednich modyfikatorów, zaczynając odbaseStats
.źródło