Mappery, wrappery, interceptory, adaptery

0

Czy ktoś mógłby mi wyjaśnić do czego używamy mapperów, wrapperów, interceptorów i adapterów?

Jeśli jakieś podobne twory pominąłem to proszę o rozszerzenie.

5

Tak w skrócie:
mapper - mapuje jeden obiekt na inny, np masz kilka api które zwracają rózną strukturę danych ale podobną treść to robisz mapper który generuje obiekt którym potem się poruszasz w projekcie mając gdzieś w jakiej formie dane przychodzą z tego api
wrapper - to takie funkcje które tylko wywołują inną funkcje ale np w tej wywołanej funkcji ustawiają konkretny parametr typu funkcja post(...) wywoła funckje request('post', ...)
interceptor - trudno mi to opisać prosto hmm to taka klasa/funckja która rozszerza inna klase ale z boku bez wiedzy kodu który używa tej klasy rozszerzanej. Często się to spotyka np w klasach do tworzenia requestów, można w takiej klasie zarejestrować klase która wykona jakieś dodatkowe operacje przed requestem i po requeście.
adapter - działa dokładnie tak jak każdy inny adapter często stosowane by ukryć zewnętrzną zależność na wypadek jej późniejszej wymiany, czyli tylko ten adapter korzysta z biblioteki zewnętrznej a cała aplikacja używa tego adaptera, tym samym podmiana biblioteki spowoduje potrzebę modyfikacji tylko adaptera.

0

Czy mapper to musi być oddzielna klasa? Czy może lepiej po prostu napisać metodę typu "from(..)" lub "to(..)" w jednej z klas, której dotyczy mapowanie?

0

Nikt ci nie zabroni zrobić tego tak jak chcesz ale generalnie powinna to być osobna klasa jeśli nie chcesz łamać solida

0

No właśnie pytam pod kątem dobrych praktyk, bo widziałem oba podejścia. Tak mi się zdaje, że wydzielanie jednej metody z kilkoma linijkami kodu do innej klasy, która jedyne co robi, to "przepisuje" dane z jednego obiektu do drugiego jest trochę dziwne. Jak takie wystawianie wszystkich danych obu klas i operowanie nimi w innej klasie ma się np. do enkapsulacji i ogólnie zasad programowania obiektowego? Czy taki mapper nie pełni po prostu funkcji konstruktora obiektu (lub static factory method)? Czy może chodzi o to, że mappera używamy, gdy konstrukcja obiektu jest bardziej złożona i np. dynamiczna (mapper jako obiekt może przechowywać stan). Może dobrym przykładem jest ObjectMapper z Jacksona, który jest konfigurowalny.

0

tylko że klasa mappera może mieć statyczne metody, ona nie trzyma żadnych danych ona je dostaje w parametrze i wypluwa obiekt z danymi który potem jest obsługiwany przez reszte systemu

0
bustard2 napisał(a):

Czy mapper to musi być oddzielna klasa? Czy może lepiej po prostu napisać metodę typu "from(..)" lub "to(..)" w jednej z klas, której dotyczy mapowanie?

Jeśli źródła klasy "wiedzą" do czego są mapowane, tym samym tracą uniwersalność - i przestają być "czyste" czyli łamią SOLID jak pisze ekhhh
Wyobraź sobie hipotetyczną klasę, która "u dołu" ma związki z bazą danych, "od góry" z JSONem na webie, i jednocześnie z jakiś formatem XML typu "plik dla księgowości". Zawrzesz w jednej klasie te trzy kompatybilności (mapery) ?
Programista konserwujący np interfejs XML będzie powodował (pozorne dla innych) podbicie wersji klasy w repozytorium ?

0

@ZrobieDobrze: Faktycznie, jeśli się tak na to spojrzy, to taka klasa może robić za dużo rzeczy i to w dodatku takich, które nie są potrzebne do dostarczenia swojej funkcjonalności. Rozumiem, że to się sprowadza głównie do poprawienia czytelności.

Programista konserwujący np interfejs XML będzie powodował (pozorne dla innych) podbicie wersji klasy w repozytorium ?

Rozumiem, że to się odnosi specyficznych sytuacji, w której programujemy bibliotekę? Wydaję mi się, że w większości przypadkach, takie klasy żyją wewnątrz jednego pakietu i nigdy z niego nie wyjdzie.

0
ehhhhh napisał(a):

Nikt ci nie zabroni zrobić tego tak jak chcesz ale generalnie powinna to być osobna klasa jeśli nie chcesz łamać solida

To zależy od języka. Jeżeli piszemy o Java, to faktycznie ta metoda powinna być w oddzielnej klasie. Oczywiście podstawową cechą mappera jest utrzymanie niewiedzy struktury mapowanej o docelowej i odwrotnie, jednak jeżeli jakiś język wspiera funkcje nie będące częścią żadnej klasy, to są one lepszym pomysłem niż tworzenie całej klasy. Jeżeli wspierane są extension functions, to do takich transformacji nadadzą się idealnie i z jednej strony będzie można sobie napisać personEntity.toPersonDTO(), z drugiej strony wciąż PersonEntity nie będzie w żaden sposób powiązane (nie będzie miało wiedzy) o PersonDTO, ani o tym jak taką transformację wykonać.

Jednak muszę dodać parę rzeczy.

Mapper - zmiana struktury danych bez istotnej zmiany treści. Masz system, który musi przyjąć dane w formacie A, ale wysłać je dalej w formacie B, to co przyjmie format A, a wypluje format B to właśnie mapper.

Wrapper - wzorzec projektowy. Klasa Wrappera deklaruje implementację tego samego interface'u, co klasa owijana. Przyjmuje obiekt klasy "owijanej" w konstruktorze i przekazuje wszystkie wywołania 1:1 do klasy owijanej.

Czyli w Java:

public class MyListWrapper<T> implements List<T>{
private final List<T> underlyingObject;

public MyListWrapper<T>(List<T> list){
  this.underlyingObject = list;
}

public int getSize(){
  return this.underlyingObject.getSize()
}
//reszta metod interface'u List
}

Albo w języku bardziej dla ludzi:

class MyListWrapper<T>(val underlyingObject:List<T>): List<T> by underlyingObject

Interceptor - nie znam, wg. mnie to to samo co wrapper, ale może jestem niedouczony.

Adapter jest ciekawy i użyteczny, a rzadko stosowany. Pozwala na zmianie (dostosowaniu) interface'u do aktualnych potrzeb, bez mappera. Np. masz zarządzany przez jakiś komponent zbiór danych List<Person> i chcesz go wyświetlać za pomocą biblioteki, która wymaga jakiegoś tam ListSuplier. Możesz to rozwiązać za pomocą mappera (zmieniając strukturę danych), ale możesz też stworzyć sobie klasę, która będzie implementować ListSuplier, ale generować odpowiedzi na podstawie danych, które zostały jej przekazane w konstruktorze.

1
piotrpo napisał(a):

Wrapper - wzorzec projektowy. Klasa Wrappera deklaruje implementację tego samego interface'u, co klasa owijana. Przyjmuje obiekt klasy "owijanej" w konstruktorze i przekazuje wszystkie wywołania 1:1 do klasy owijanej.

Myślę, że nie.
Szczegół o jakim piszesz (stosik obiektów spełniających ten sam interfejs) występuje np w Chain of Responsibility, ale to zupełnie inna opowieść
A samego Wrappera bym nie zaliczył do tych samych "religijnych" wzorców OOP (zależy jakie kto znaczenie przykłada pod słowo "wzorzec", a to długi temat). Wrapper może np obiektowo udostępniać coś proceduralnego (Gtk -> Gtk++ itd), albo proceduralnie ale ładniej udostepniać coś innego proceduralnego.
Oczywiście , nadanie nazwy nastąpiło (a nazwanie pomaga - choć czasem już jest fundamentem religii) podobnie jak z wzorcami OOP, ale to raczej prosta technika koderska niż "święte" wzorce.

Interceptor - nie znam, wg. mnie to to samo co wrapper, ale może jestem niedouczony.

Nie znam twardej definicji interceptora, ale lokował bym go w okolicy listenera. Raczej jak najdalej od wrappera. Ten wątek, pod wzgledem wynajdywania coraz to nowych rzeczowników z "r" na końcu, bez fundamentów, nie ma sensu.

Adapter jest ciekawy i użyteczny, a rzadko stosowany.

Trudno wytłumaczyć Adapter komuś, kto nie stanął wobec potrzeby (np integracji zupełnie innych linii oprogramowania). Wykładanie / zmuszanie do użycia wzorców "bo musisz" a nie "bo występuje potrzeba" prowadzi do patologii, staje się w złym sensie religią czy dyktaturą. Swięte wojny, czy jest dokładnie 21, czy inna liczba itd...

Widzę że potrzeba od czasu do czasu przypomnieć sama istotę wzorca. Fachowcy (różnych branż, o czym zdanie niżej) stają wobec potrzeb, w sposób intuicyjny / albo nauczany młodych adeptów po pewnym czasie okazuje się, że tą samą potrzebę implementujemy w podobny/jednakowy sposób.
Teoretyczne ujęcie wzorca (artykuł / nazwa /ksiazka) pojawia się z radosnym "o kurcze, widzę ze wszyscy ten problem rozwiązujemy podobnie", i dla łatwości komunikacji następuje nadanie nazwy.

Potrzeba: złączyć prostopadle deski ich końcami
Metoda: seria ukośnych nacięć, potem praca dłutem. Montaż lekko pobijając przez przekładkę
Nazwa: jaskółczy ogon
Zalety, wady: (pominę)

Na gruncie architektury (tej budowlanej) wzorce analizował p. o nazwisku Alexander, również metodą "o kurcze tzreba stwierdzić, że potrzebę X od dawna rozwiązujemy przez Y"

1
ZrobieDobrze napisał(a):

Myślę, że nie.
Szczegół o jakim piszesz (stosik obiektów spełniających ten sam interfejs) występuje np w Chain of Responsibility, ale to zupełnie inna opowieść
A samego Wrappera bym nie zaliczył do tych samych "religijnych" wzorców OOP (zależy jakie kto znaczenie przykłada pod słowo "wzorzec", a to długi temat). Wrapper może np obiektowo udostępniać coś proceduralnego (Gtk -> Gtk++ itd), albo proceduralnie ale ładniej udostepniać coś innego proceduralnego.
Oczywiście , nadanie nazwy nastąpiło (a nazwanie pomaga - choć czasem już jest fundamentem religii) podobnie jak z wzorcami OOP, ale to raczej prosta technika koderska niż "święte" wzorce.

No właśnie z nazwami jest trochę bałaganu, ale chodzi mi dokładnie o wzorzec proxy https://en.wikipedia.org/wiki/Proxy_pattern

Nie znam twardej definicji interceptora, ale lokował bym go w okolicy listenera. Raczej jak najdalej od wrappera. Ten wątek, pod wzgledem wynajdywania coraz to nowych rzeczowników z "r" na końcu, bez fundamentów, nie ma sensu.

Nie jestem pewien czy istnieje "twarda definicja", co jak widać jest problemem. Wikipedia podaje, że przykładem jest Filter z JEE, czyli metoda m(T):T, gdzie zewnętrzny "orkiestrator" włącza ten interceptor w ciąg wywoływania właściwej metody.

Adapter jest ciekawy i użyteczny, a rzadko stosowany.

Trudno wytłumaczyć Adapter komuś, kto nie stanął wobec potrzeby (np integracji zupełnie innych linii oprogramowania). Wykładanie / zmuszanie do użycia wzorców "bo musisz" a nie "bo występuje potrzeba" prowadzi do patologii, staje się w złym sensie religią czy dyktaturą. Swięte wojny, czy jest dokładnie 21, czy inna liczba itd...

To może na przykładzie: https://www.geeksforgeeks.org/arrayadapter-in-android-with-example/ Czyli przejściówka pomiędzy w tym przypadku tablicą, a interfacem BaseAdapter, który służy do zasilania listy UI. Czyli w sumie dość intuicyjna nazwa, bo zamieniamy jeden interface na inny "w locie", podobnie jak adapter z kontynentalnego gniazdka sieciowego na brytyjskie.

0
piotrpo napisał(a):

Wrapper - wzorzec projektowy. Klasa Wrappera deklaruje implementację tego samego interface'u, co klasa owijana. Przyjmuje obiekt klasy "owijanej" w konstruktorze i przekazuje wszystkie wywołania 1:1 do klasy owijanej.

To raczej antywzorzec niż wzorzec, bo z definicji nie wnosi żadnej wartości, a więc jest zbędny.
Jednakże czasem się przydaje, jeśli chcemy opakować zewnętrzne API w naszą klasę (np. z naszym interfejsem, żeby się dało mockować).
Niestety, mimo bezużyteczności, jest jednocześnie bardzo popularny - programiści uwielbiają produkować tony kodu pisząc zbędne wrappery.

Interceptor - nie znam, wg. mnie to to samo co wrapper, ale może jestem niedouczony.

Raczej jak dekorator, bo przechwytuje wejście i wyjście metody.
Tyle, że jest niekompilowany (podpinany w runtime), więc lepiej nie używać, jeśli naprawdę nie trzeba.

1

@somekind: Nie powiedziałbym, że proxy jest antywzorcem. To sposób na uniknięcie dziedziczenia a jednocześnie osiągnięcie podobnych jak w przypadku dziedziczenia efektów. Prosty przykład to kiedy masz klasę final implementującą jakiś interface i chcesz ją rozszerzyć. Nie możesz tego robić dziedziczeniem, bo klasa nie jest gotowa do tego, żeby po niej bezpiecznie dziedziczyć, ale możesz osiągnąć ten cel właśnie używając wrappera.
Coś takiego opisał Bloch w Effective Java w odniesieniu do listy, przykład z pamięci i w Kotlinie, bo mi się nie chce tyle klepać:

interface List<T>{
    add(item:T)
    addAll(items: List<T>
}
open class ArrayList<T>: List<T>{
    addAll(items: List<t>){
      items.forEach{
        this.add(it) //wywołanie innej metody publicznej
      }
    }
}

I klasa w której wchodzimy na minę

ObservableList<T>:ArrayList<T>{
  override add(item:T){
      notifyListeners(item)
      super.add(item)
  }

  override addAll(items:List<t>){
      super.addAll(items)
      items.forEach{
        notifyListeners(it)
      }
  }
}

Niby wykonaliśmy to dziedziczenie zgodnie z regułami sztuki, a jednak, po wywołaniu addAll() każdy listener zostanie wywołany 2 razy, bo nie zdajemy sobie sprawy z szczegółów implementacyjnych ArrayList.
Tymczasem, gdybyśmy zastosowali "niepotrzebny" wrapper:

open class ListWrapper<T>(val underlyingObject: List<T>):List<T> by underlyingObject{
}

i wtedy dziedziczyli naszą implementację po nim, zamiast po ArrayList, to:

  • Walą nas szczegóły implementacyjne ArrayList
  • Mamy tę funkcjonalność dostępną dla dowolnej implementacji List
0
piotrpo napisał(a):

@somekind: Nie powiedziałbym, że proxy jest antywzorcem.

Ja też tego nie powiedziałem.
Antywzorcem jest wrapper - takie coś, co tylko opakowuje faktyczny kod pod spodem, i niczego dodatkowego nie daje.
Przykładem jest opakowywanie ORMa w generyczne repozytorium i UoW albo jakiejś biblioteki logującej w swoją własną klasę loggera.

0

Destroyer, terminator, assesor, kwerenda "*r" przez słownik. Alternatywna kwerenda "Arnold Schwarzenegger"
Jak kapralowi w PRL-owskim wojsku, rzuca się jakieś słówko, a on potrafi się n/t wypowiedzieć. Kolega rzucił takie słówka, a kapralami LWP jesteśmy my.

Chyba pora kończyć wątek zapoczątkowany przez @Commander300
Albowiem dlatego że ponieważ
a) nie da się skatalogować wzorców (tzn dało by się na mocy jakiegoś "faszystowskiego" zamiaru , ale to by było śmiertelne dla rozsądku kodowania / twórczości, i to już widac, juniorzy zmuszeni do stosowania wzorców sztuk trzy itd)
b) pożytek dla pytającego jest zerowy lub raczej sądzę ujemny. Bez normalnego rozwoju w OOP, w teorii i praktyce, ogarnianiu coraz większych projektów, w tym integracyjnych, powtórzę "pozór wiedzy jest gorszy od jej braku"

1 użytkowników online, w tym zalogowanych: 0, gości: 1