Oddzielny interfejs dla metod mutacji

11

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 Foozdefiniowany 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, MutableFooktó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, MutableFooktó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, StatSetktó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:

  1. Czy istnieje nazwa dzielenia interfejsów przez akcesory i mutatory na osobne interfejsy?
  2. 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ć DefaultFoolub DefaultMutableFooale jest ukryty, ponieważ createImmutableFoozwraca Foo)?
  3. 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 EffectiveStatSetnie ma większego sensu, ponieważ w żaden sposób nie rozszerzamy funkcjonalności. Moglibyśmy zmienić implementację i stworzyć połączenie EffectiveStatSetdwó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);
    }
}
Zymus
źródło
1
Myślę, że problem polega na tym, że twój obiekt można modyfikować, nawet jeśli uzyskujesz do niego dostęp, mój „niezmienny” interfejs. Lepiej mieć zmienny obiekt za pomocą Player.TrainStats ()
Ewan
@gnat Czy masz jakieś odniesienia do tego, gdzie mogę dowiedzieć się więcej na ten temat? Na podstawie edytowanego pytania nie jestem pewien, w jaki sposób i gdzie mógłbym to zastosować.
Zymus
@gnat: twoje linki (oraz linki drugiego i trzeciego stopnia) są bardzo przydatne, ale chwytliwe frazy mądrości niewiele pomogą. Przyciąga tylko nieporozumienia i pogardę.
rwong,

Odpowiedzi:

6

Wydaje mi się, że masz rozwiązanie szukające problemu.

Czy istnieje nazwa dzielenia interfejsów przez akcesory i mutatory na osobne interfejsy?

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ę?

Rozdziela je w ten sposób na „właściwe” podejście, jeśli istnieje prawdopodobieństwo, że zostaną one zastosowane w równym stopniu lub jeśli istnieje inny, bardziej akceptowany wzór

„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 Fooklasy z atrybutami X, Y, Z (wektor trójwymiarowy) prawdopodobnie wolałbym wariant niezmienny.

Czy są jakieś dające się natychmiast przewidzieć wady korzystania z tego wzorca, z wyjątkiem komplikowania hierarchii interfejsu?

Zbyt skomplikowane konstrukcje sprawiają, że oprogramowanie jest trudniejsze do testowania, trudniejsze w utrzymaniu, trudniejsze do ewolucji.

Doktor Brown
źródło
1
Myślę, że masz na myśli „KISS - Keep It Simple, Stupid”
Epicblood
2

Czy istnieje nazwa dzielenia interfejsów przez akcesory i mutatory na osobne interfejsy?

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

  • w Javie nie ma interfejsu ReadOnlyList

Wersja ReadOnly używa „WritableInterface” i zgłasza wyjątek, jeśli używana jest metoda zapisu

k3b
źródło
Zmodyfikowałem OP, aby wyjaśnić podstawowy przypadek użycia.
Zymus
2
„w Javie nie ma interfejsu ReadOnlyList” - szczerze mówiąc, powinno być. Jeśli masz fragment kodu, który akceptuje List <T> jako parametr, nie możesz łatwo stwierdzić, czy będzie działać z listą niemodyfikowalną. Kod zwracający listy, nie ma możliwości sprawdzenia, czy można je bezpiecznie modyfikować. Jedyną opcją jest albo poleganie na potencjalnie niekompletnej lub niedokładnej dokumentacji, albo wykonanie kopii obronnej na wszelki wypadek. Typ kolekcji tylko do odczytu uprościłby to.
Jules
@Jules: rzeczywiście, Java wydaje się być powyższa: aby upewnić się, że dokumentacja jest kompletna i dokładna, a także na wszelki wypadek wykonać kopię obronną. Z pewnością skaluje się do bardzo dużych projektów biznesowych.
rwong,
1

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 Adaje klasie Bdostę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 klasa A) 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).

  1. Zdefiniuj DTO (obiekt przesyłania danych), znany również jako krotka wartości.
    • Ten DTO będzie zawierać trzy pola: hitpoints, attack, defense.
    • Tylko pola: publiczne, każdy może pisać, bez ochrony.
    • Jednak DTO musi być obiektem jednorazowym: jeśli klasa Amusi przekazać DTO do klasy B, tworzy jego kopię i zamiast tego przekazuje kopię. BMoże więc korzystać z DTO w dowolny sposób (pisząc do niego), bez wpływu na DTO, które się Autrzymuje.
  2. grantExperienceFunkcja dzieli się na dwie części:
    • calculateNewStats
    • increaseStats
  3. calculateNewStats pobierze dane wejściowe z dwóch DTO, jeden reprezentujący statystyki gracza, a drugi reprezentujący statystyki wroga, i wykona obliczenia.
    • Aby wprowadzić dane, dzwoniący powinien wybrać spośród podstawowych, wyszkolonych lub skutecznych statystyk, zgodnie z Twoimi potrzebami.
    • Rezultatem będzie nowe DTO gdzie każde pole ( hitpoints, attack, defense) przechowuje kwota do zwiększany do tego zdolności.
    • Nowa DTO „kwota do przyrostu” nie dotyczy górnej granicy (nałożonego maksymalnego limitu) dla tych wartości.
  4. 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ć.
    • Jeśli obowiązują maksymalne wartości dla tych statystyk, są one tutaj egzekwowane.

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 calculateNewStatsokaż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 i calculateNewStatsmuszą zostać zaktualizowane, aby wykorzystać jak najwięcej ich nowych właściwości), wówczas calculateNewStatsnależ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.

rwong
źródło
1

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, setYi setZmetody - po prostu zwrócić nową strukturę zamiast modyfikowania obiekt, który nazwie je.

// Mutates mutableFoo
mutableFoo.setX(...)
// Creates a new updated immutableFoo, existing immutableFoo is unchanged
newFoo = immutableFoo.setX(...)

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 niezmienne, a gracz jest zmienny. Zamiast:

// Stats are mutable, mutates self.stats
self.stats.setX(...)

Miałbyś:

// Stats are immutable, mutates self, setX() returns new updated stats
self.stats = self.stats.setX(...)

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.

Jacek
źródło
1

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 FooViewerczy ReadOnlyFooi interfejs zapisem Tylko FooEditorczy WriteableFoolub w Javie Myślę, że widziałem FooMutatorkiedyś. 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ć EffectiveStatskomponować i niezmienna Statsi coś StatModifiersco dalej komponować jakiś zestaw StatModifiers, które reprezentują co może modyfikować statystyk (efekty tymczasowe, artykuły, Rozmieszczenie w jakiejś dziedzinie poprawy, zmęczenie), ale twój EffectiveStatsnie musiałby zrozumieć bo StatModifiersBĘDZIE zarządzać, jakie były te rzeczy i jaki wpływ i jaki będą mieć na jakie statystyki. TheStatModifierbył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 lepiej StatModifierbyłoby po prostu ujawnić metodę wytwarzania nowej niezmienności Statsopartej na innej, Statsktóra byłaby inna, ponieważ została odpowiednio zmieniona. Możesz wtedy zrobić coś takiego currentStats = statModifier2.modify(statModifier1.modify(baseStats))i wszystko Statsmoż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 od baseStats.

Jason Kell
źródło