Jak utrzymywać argument na niskim poziomie i nadal rozdzielać zależności osób trzecich?

13

Korzystam z biblioteki strony trzeciej. Przekazują mi POJO, które dla naszych celów i celów jest prawdopodobnie realizowane w następujący sposób:

public class OurData {
  private String foo;
  private String bar;
  private String baz;
  private String quux;
  // A lot more than this

  // IMPORTANT: NOTE THAT THIS IS A PACKAGE PRIVATE CONSTRUCTOR
  OurData(/* I don't know what they do */) {
    // some stuff
  }

  public String getFoo() {
    return foo;
  }

  // etc.
}

Z wielu powodów, w tym między innymi enkapsulacji API i ułatwiania testów jednostkowych, chcę opakować ich dane. Ale nie chcę, aby moje podstawowe klasy były zależne od ich danych (ponownie, z powodów testowych)! Więc teraz mam coś takiego:

public class DataTypeOne implements DataInterface {
  private String foo;
  private int bar;
  private double baz;

  public DataTypeOne(String foo, int bar, double baz) {
    this.foo = foo;
    this.bar = bar;
    this.baz = baz;
  }
}

public class DataTypeTwo implements DataInterface {
  private String foo;
  private int bar;
  private double baz;

  public DataTypeOne(String foo, int bar, double baz, String quux) {
    this.foo = foo;
    this.bar = bar;
    this.baz = baz;
    this.quux = quux;
  }
}

A potem to:

public class ThirdPartyAdapter {
  public static makeMyData(OurData data) {
    if(data.getQuux() == null) {
      return new DataTypeOne(
        data.getFoo(),
        Integer.parseInt(data.getBar()),
        Double.parseDouble(data.getBaz()),
      );
    } else {
      return new DataTypeTwo(
        data.getFoo(),
        Integer.parseInt(data.getBar()),
        Double.parseDouble(data.getBaz()),
        data.getQuux();
      );
  }
}

Ta klasa adaptera jest połączona z kilkoma innymi klasami, które MUSZĄ wiedzieć o interfejsie API innej firmy, ograniczając jej wszechobecność w pozostałej części mojego systemu. Jednak ... to rozwiązanie jest BRUTTO! W Clean Code, strona 40:

Więcej niż trzy argumenty (poliadowe) wymagają bardzo specjalnego uzasadnienia - i i tak nie powinny być używane.

Rzeczy, które rozważałem:

  • Tworzenie obiektu fabrycznego zamiast statycznej metody pomocniczej
    • Nie rozwiązuje problemu posiadania bajillionowych argumentów
  • Tworzenie podklasy DataTypeOne i DataTypeTwo, która ma zależny konstruktor
    • Nadal ma chroniony poliadycznie konstruktor
  • Twórz całkowicie osobne implementacje zgodne z tym samym interfejsem
  • Wiele powyższych pomysłów jednocześnie

Jak sobie z tym poradzić?


Uwaga: nie jest to sytuacja warstwy antykorupcyjnej . Nie ma nic złego w ich API. Problemy są następujące:

  • Nie chcę mieć MOICH struktur danych import com.third.party.library.SomeDataStructure;
  • Nie mogę zbudować ich struktur danych w moich testowych przypadkach
  • Moje obecne rozwiązanie skutkuje bardzo bardzo dużą liczbą argumentów. Chcę, aby liczba argumentów była niska, BEZ przekazywania ich struktur danych.
  • To pytanie brzmi „ czym jest warstwa antykorupcyjna?”. Moje pytanie brzmi: „ Jak mogę użyć wzoru, dowolnego wzoru, aby rozwiązać ten scenariusz?”

Nie pytam też o kod (inaczej to pytanie byłoby na SO), po prostu proszę o wystarczającą odpowiedź, aby umożliwić mi skuteczne napisanie kodu (czego to pytanie nie zapewnia).

durron597
źródło
Jeśli istnieje kilka takich POJO innych firm, warto spróbować napisać niestandardowy kod testowy, który używa mapy z pewnymi konwencjami (np. Nazwij klucze int_bar) jako danych wejściowych testu. Lub użyj JSON lub XML z niestandardowym kodem pośredniczącym. W efekcie coś w rodzaju DSL do testowania com.thirdparty.
user949300,
Pełny cytat z Clean Code:The ideal number of arguments for a function is zero (niladic). Next comes one (monadic), followed closely by two (dyadic). Three arguments (triadic) should be avoided where possible. More than three (polyadic) requires very special justification — and then shouldn’t be used anyway.
Lilienthal
11
Ślepe przestrzeganie wzorca lub wytycznych programowych jest jego własnym anty-wzorcem .
Lilienthal
2
„enkapsulowanie ich API i ułatwianie testowania jednostek”. Brzmi jak ten może być dla mnie nadmiernym testowaniem i / lub spowodowanym przez test uszkodzeniem projektu (lub może wskazywać, że możesz zaprojektować to inaczej na początku). Zadaj sobie to pytanie: czy to naprawdę ułatwia zrozumienie, zmianę i ponowne użycie kodu? Odłożyłbym pieniądze na „nie”. Jak realistycznie prawdopodobne jest, że kiedykolwiek wymienisz tę bibliotekę? Prawdopodobnie niezbyt. Jeśli to zrobisz, czy to naprawdę ułatwia upuszczenie zupełnie innego? Znowu postawiłbym na „nie”.
jpmc26
1
@JamesAnderson Właśnie odtworzyłem pełny cytat, ponieważ uznałem, że jest interesujący, ale z fragmentu nie było dla mnie jasne, czy odnosi się on do funkcji w ogóle, czy konkretnie do konstruktorów. Nie chciałem poprzeć roszczenia i, jak powiedział jpmc26, mój następny komentarz powinien dać ci pewne wskazówki, że tego nie robię. Nie jestem pewien, dlaczego czujesz potrzebę atakowania naukowców, ale użycie polisyllab nie czyni kogoś akademickim elitą osadzonym na jego wieży z kości słoniowej ponad chmurami.
Lilienthal,

Odpowiedzi:

10

Strategia, której użyłem, gdy istnieje kilka parametrów inicjalizacji, polega na stworzeniu typu zawierającego tylko parametry inicjalizacji

public class DataTypeTwoParameters {
    public String foo;  // use getters/setters instead if it's appropriate
    public int bar;
    public double baz;
    public String quuz;
}

Następnie konstruktor DataTypeTwo pobiera obiekt DataTypeTwoParameters, a DataTypeTwo jest konstruowany poprzez:

DataTypeTwoParameters p = new DataTypeTwoParameters();
p.foo = "Hello";
p.bar = 4;
p.baz = 3;
p.quuz = "World";

DataTypeTwo dtt = new DataTypeTwo(p);

Daje to wiele możliwości wyjaśnienia, jakie są wszystkie parametry wchodzące w skład DataTypeTwo i co one oznaczają. Można również podać rozsądne wartości domyślne w konstruktorze DataTypeTwoParameters, aby można było ustawić tylko wartości, które należy ustawić, w dowolnej kolejności, którą lubi konsument interfejsu API.

Erik
źródło
Ciekawe podejście Gdzie umieścisz odpowiedni Integer.parseInt? W seterze, czy poza klasą parametrów?
durron597
5
Poza klasą parametrów. Klasa parametrów powinna być „głupim” obiektem i nie powinna próbować robić nic poza wyrażeniem wymaganych danych wejściowych i ich typów. Parsowanie powinno odbywać się w innych miejscach, takich jak: p.bar = Integer.parseInt("4").
Erik
7
brzmi to jak wzorzec obiektu parametru
komenda
9
... lub anty-wzór.
Telastyn
1
... lub możesz po prostu zmienić nazwę DataTypeTwoParametersna DataTypeTwo.
user253751
14

Naprawdę masz tutaj dwie osobne obawy: owijanie API i utrzymywanie niskiej liczby argumentów.

Podczas pakowania interfejsu API chodzi o zaprojektowanie interfejsu od zera, nie znając niczego poza wymaganiami. Mówisz, że nie ma nic złego w ich interfejsie API, a następnie na tym samym oddechu wypisz kilka rzeczy błędnych w ich interfejsie API: testowalność, konstruowalność, zbyt wiele parametrów w jednym obiekcie itp. Napisz API, które chciałbyś mieć. Jeśli wymaga to wielu obiektów zamiast jednego, zrób to. Jeśli wymaga to zawinięcia o jeden poziom wyżej , zrób to z obiektami, które tworzą POJO.

Po uzyskaniu pożądanego interfejsu API liczba parametrów może już nie stanowić problemu. Jeśli tak, należy wziąć pod uwagę wiele typowych wzorców:

  • Obiekt parametru, jak w odpowiedzi Erika .
  • Wzór budowniczym , gdzie można utworzyć oddzielny obiekt Builder, a następnie wywołać szereg ustawiaczy, aby ustawić parametry indywidualnie, a następnie stworzyć swój końcowy obiekt.
  • Wzór prototyp , gdzie można sklonować podklasy żądanego obiektu z pola już ustawione wewnętrznie.
  • Fabryka, którą już znasz.
  • Niektóre kombinacje powyższych.

Zauważ, że te wzorce tworzenia często kończą się wywoływaniem konstruktora poliadowego, co powinieneś uznać za dobre, gdy jest zamknięty. Problem z konstruktorami poliadowymi polega na tym, że nie wywołuje się ich raz, lecz wtedy trzeba je wywoływać za każdym razem, gdy trzeba zbudować obiekt.

Zauważ, że zwykle o wiele łatwiej i łatwiej jest utrzymać interfejs API, przechowując referencję do OurDataobiektu i przekazując wywołania metod, a nie próbując ponownie wdrożyć jego elementy wewnętrzne. Na przykład:

public class DataTypeTwo implements DataInterface {
  private OurData data;

  public DataTypeOne(OurData data) {
    this.data = data;
  }

   public String getFoo() {
    return data.getFoo();
  }

  public int getBar() {
    return Integer.parseInt(data.getBar());
  }
  ...
}
Karl Bielefeldt
źródło
Pierwsza połowa tej odpowiedzi: świetna, bardzo pomocna, +1. Druga połowa tej odpowiedzi: „przejdź do bazowego interfejsu API, przechowując odwołanie do OurDataobiektu” - tego właśnie staram się unikać, przynajmniej w klasie podstawowej, aby upewnić się, że nie ma zależności.
durron597
1
Dlatego robisz to tylko w jednym ze swoich wdrożeń DataInterface. Tworzysz kolejną implementację dla swoich próbnych obiektów.
Karl Bielefeldt
@ durron597: tak, ale już wiesz, jak rozwiązać ten problem, jeśli naprawdę Ci to przeszkadza.
Doc Brown
1

Myślę, że zbyt surowo interpretujesz zalecenie wuja Boba. W przypadku normalnych klas, z logiką i metodami oraz konstruktorami itp., Konstruktor poliadowy rzeczywiście przypomina zapach kodu. Ale w przypadku czegoś, co jest ściśle kontenerem danych, który odsłania pola i jest generowane przez obiekt, który jest już zasadniczo obiektem fabryki, nie sądzę, że jest tak źle.

Państwo może wykorzystać wzór Object parametrów, jak sugeruje w komentarzu, można owinąć te parametry konstruktora dla ciebie, co lokalny typ danych jest wrapper jest już zasadniczo obiektem parametru. Wszystko, co zrobisz w swoim obiekcie Parameter, to spakowanie parametrów (Jak to utworzysz? Konstruktorem poliadowym?), A następnie rozpakowanie ich sekundę później w obiekt, który jest prawie identyczny.

Jeśli nie chcesz wystawiać seterów dla swoich pól i nazywać ich, myślę, że trzymanie się konstruktora poliadowego w dobrze zdefiniowanej i zamkniętej fabryce jest w porządku.

Avner Shahar-Kashtan
źródło
Problem polega na tym, że liczba pól w mojej strukturze danych zmieniała się wiele razy i prawdopodobnie zmieni się ponownie. Co oznacza, że ​​muszę refaktoryzować konstruktor we wszystkich moich testach. Wzorzec parametrów z rozsądnymi wartościami domyślnymi brzmi jak lepsza droga; posiadanie modyfikowalnej wersji, która zostaje zapisana w niezmiennej formie, może ułatwić mi życie na wiele sposobów.
durron597