Czy abstrakcyjne konstruktory mogłyby mieć sens?

Odpowiedz Nowy wątek
2019-05-09 19:24
0

Zanim zacznę pytanie zaznaczę może co ja rozumiem przez określenia "Interfejs" oraz "Interfejs abstrakcyjny".

  • Interfejs to po prostu sposób korzystania z klasy: metody, pola, konstruktor, throws w metodzie, to czy zwraca/nie zwraca nulle - wszystko to ma związek z korzystaniem z klasy - a więc jest to interfejs.
  • Interfejs abstrakcyjny to dla mnie ta część interfejsu struktur klas które są wspólne (np List.add(), List.remove() to części interfejsu abstrakcyjnego, bo niezależnie od implementacji list, dostęp do nich jest taki sam). Od razu widać też że konstruktor nie jest częścią interfejsu abstrakcyjnego, ponieważ różne implementacje mają różne konstruktory.

Wyobraźcie sobie kod - dosyć OOP:

class Application {
  User user;
  List<Events> events;

  Date day(int offset) {
    return new Calendar(user, events).nextDay(offset).getDate();
  }
}

I teraz chcemy mieć dwie implementacje Calendar: GregorianCalendar i JulianCalendar (oczywiście sytuacja abstrakcyjna). Co zrobi programista:

  • Fanatyk Clean Java Wujek Bob code - wyniesie new Calendar( do fabryki abstrakcyjnych (GregorianCalendarFactory, JulianCalendarFactory)

    class Application {
      CalendarFactory factory;
      User user;
      List<Events> events;
    
      Date day(int offset) {
        return factory.createCalendar(user, events).nextDay(offset).getDate();
      }
    }
  • Bootcampowiec powie że parametry w konstruktorze to zło, że zależnościami mogą być tylko inne serwisy i trzymanie jakiegokolwiek stanu (nawet immutable) jest be (kocha też kontenery DI) i wyrzuci User i List<Event> z parametrów konstruktora, i pewnie jeszcze dorzuci @Autowired żeby Spring mu to wstrzyknął.

    class Application {
      Calendar calendar;
      User user;
      List<Events> events;
    
      Date day(int offset) {
        return calendar.nextDay(user, events, offset).getDate();
      }
    }

I ja się zacząłem zastanawiać. Czemu żaden język jeszcze nie ma dedykowanej składni do takich przypadków? Np przy użyciu jakiegoś losowego znaku, niech będzie że np §. Można by zrobić tak, że konstruktor też mógłby być abstrakcyjny (pełniłby funkcję fabryki abstrakcyjnej, tylko że byłby częścią klasy). Mogłoby to wyglądać tak

interface Calendar {
  §Calendar(User user, List<Events>); // zaznaczenie że konstruktor też ma być abstrakcyjny
                                      // klasy implementujące go musiałyby mieć konstruktor z taką sygnaturą
  Date nextDay(int offset); // zwykła metoda
}

A użycie faktyczne, wyglądałoby tak

class Application {
  §Calendar calendar;  // to nie jest fabryka ani instancja Calendar - to tylko informacja którą implementację przy konstruowaniu wybrać
  User user;
  List<Events> events;

  Date day(int offset) {
     // instancjonowanie jednej z implementacji Calendar - tej implementacji która siedzi w `calendar`
    return new calendar§Calendar(user, events).getNext(offset).getDate();
  }
}

a chcąc zdecydować o implementacji można by tak

new Application(§JulianCalendar, user, events);   // zamiast `new JulianCalendarFactory()`
new Application(§GregorianCalendar, user, events);  // ewentualnie GregorianCalendar.class

Podsumowanie

Więc, to rozwiązanie ma kilka wad i zalet:
Wady:

  • Ma mniejsze możliwości niż zwykła fabryka (np nie da się zrobić cache'owania/współdzielenia instancji)
  • Ciężko zrobić różne implementacje które mają zupełnie różne parametry
  • Jasne że use-case'ów dla tego jest dużo mniej niż dla zwykłej fabryki.

Zalety:

  • Nie ma szumu z plikami fabryki - czasem mam problem z rozróżnieniem po co są fabryki, bo w projektach w których pracowałem około 50% fabryk jest po to żeby wybrać implementację, a pozostałe 50% po to żeby stworzyć obiekt który wymaga wiele set-up'u. Z § byłoby to czytelniejsze - od razu byłoby widać że chodzi o wybranie implementacji.
  • Kod dla niektórych jest czytelniejszy, bo zamiast obiekt.createInstance() mamy new impl§Calendar() - mam na myśli to że jest new, jest konstruktor i podane parametry. Innymi słowy nieco ciaśniejsze przywiązanie do siebie implementacji tego samego interfejsu.
  • Dzięki temu można mieć jednocześnie instancjonowanie w miejscu instancji oraz parametry w obiektach.

Jak uważacie pomysł za popier****y to mówcie.


char mander; bool basaur;
Zaawansowana biblioteka T-Regx do wyrażeń regularnych w PHP
edytowany 3x, ostatnio: TomRiddle, 2019-05-09 19:27

Pozostało 580 znaków

2019-05-09 19:44
4

To co zrobiłeś to jest właśnie factory. Jak podrążysz temat dalej to zobaczysz podobieństwo. W Javie 8 fabrykę możesz zaimplementować lambdą:

interface CalendarFactory {
  Calendar make(Arg1 arg1, Arg2 arg2);
}

class Application {
  Application(CalendarFactory calendarFactory) { ... }
}

new Application((arg1, arg2) -> new GregorianCalendar(arg1, arg2))
// chyba nawet dałoby się tak w tym przypadku
new Application(GregorianCalendar::new)

Coś a'la abstrakcyjny konstruktor można zaimplementować w Ruście. Rustowe traity to typeclassy, a typeclassy umożliwiają definicję zarówno abstrakcyjnych metod niestatycznych jak i statycznych. Konstruktor to metoda statyczna, więc typeclassa umożliwia zdefiniowanie abstrakcyjnego konstruktora. Przykład:

trait Calendar {
    pub fn new(arg1: Arg1, arg2: Arg2) -> Self; // abstrakcyjny konstruktor nie przyjmuje selfa
    pub fn epoch_time(&self) -> i32; // tutaj mamy metodę instancyjną przyjmującą selfa
}

Typeclassy to podejście funkcyjne. Fabryki to podejście obiektowe.

W Scali możliwe są oba podejścia. OOP jest dostępny wprost, typeclassy są emulowane za pomocą implicitów (Scalowe implicity to w zasadzie uogólnienie typeclass, bo typeclassy w Ruście czy Haskellu muszą być globalne i bezstanowe, a w Scali mogą być też lokalne i stanowe). Pisząc w Scali:

  • mam zwyczaj tworzenia metod fabrykujących przy każdej okazji - najwłaściwsza okazja to unikanie efektów ubocznych w konstruktorze (np rzucania wyjątków czy modyfikacji stanu spoza klasy)
  • zamiast fabryk zwykle używa się zwykłych funkcji, w Scali z automatu dostępne są od Function1 do Function22, więc spokojnie można każdy rozsądny konstruktor zapakować w taką funkcję. Nie zawsze jednak jest to czytelne, np Function3[String, String, String, MyConfig] - co to za Stringi?

"Programs must be written for people to read, and only incidentally for machines to execute." - Abelson & Sussman, SICP, preface to the first edition
"Ci, co najbardziej pragną planować życie społeczne, gdyby im na to pozwolić, staliby się w najwyższym stopniu niebezpieczni i nietolerancyjni wobec planów życiowych innych ludzi. Często, tchnącego dobrocią i oddanego jakiejś sprawie idealistę, dzieli od fanatyka tylko mały krok."
Demokracja jest fajna, dopóki wygrywa twoja ulubiona partia.
edytowany 3x, ostatnio: Wibowit, 2019-05-09 19:54
+1, dlaczego rzucanie wyjątków to efekt uboczny? bo wywołanie wtedy nie jest referential transparency? - nohtyp 2019-05-09 19:59
In computer science, an operation, function or expression is said to have a side effect if it modifies some state variable value(s) outside its local environment, that is to say has an observable effect besides returning a value (the main effect) to the invoker of the operation. - rzucanie wyjątku to obserwowalny efekt różny od zwracania wartości z metody. - Wibowit 2019-05-09 20:03
Tutaj https://stackoverflow.com/a/10720037 koleś napisał tak: Purity is only violated if you observe the exception, and make a decision based on it that changes the control flow. Actually throwing an exception value is referentially transparent -- it is semantically equivalent to non-termination or other so-called bottom values. Ja to rozumiem tak, że w Haskellu dzielenie przez 0 dla przykładu nie rzuca wyjątku, ale za to w zmiennej siedzi sobie tzw "bottom value" a nie konkretna liczba. Kolejne operacje na tej liczbie skutkują kolejnymi "bottom value". - Wibowit 2019-05-09 20:18
Tym samym zmienne w Haskellu są tak jakby wszystkie odpowiednikami Eithera (bądź Try) ze Scali czy Vavra. Precyzyjniej to nie tyko Eithera ale są także leniwe. A więc Haskellowy integer w Scali wyglądałby jako lazy val nazwa: Try[Int], a nie po prostu val nazwa: Int. - Wibowit 2019-05-09 20:20
W Scali, Javie, etc zamiast zwracania bottom value jest rzucanie wyjątku, przerwanie normalnego toku wykonywania programu i łapanie wyjątku w najbliższym bloku catch, który go obsługuje. To jest jednoznaczne z make a decision based on it that changes the control flow. - Wibowit 2019-05-09 20:24
wow, dzięki - nohtyp 2019-05-09 23:09

Pozostało 580 znaków

2019-05-09 20:06
0
Wibowit napisał(a):

To co zrobiłeś to jest właśnie factory. Jak podrążysz temat dalej to zobaczysz podobieństwo. W Javie 8 fabrykę możesz zaimplementować lambdą:

interface CalendarFactory {
  Calendar make(Arg1 arg1, Arg2 arg2);
}

class Application {
  Application(CalendarFactory calendarFactory) { ... }
}

new Application((arg1, arg2) -> new GregorianCalendar(arg1, arg2))
// chyba nawet dałoby się tak w tym przypadku
new Application(GregorianCalendar::new)

No tak, taki był plan.

Tylko ja jeszcze chciałem bez pliku i klasy CalendarFactory.


char mander; bool basaur;
Zaawansowana biblioteka T-Regx do wyrażeń regularnych w PHP

Pozostało 580 znaków

2019-05-09 20:33
1

W Vavr 0.x masz generyczne gotowce, np: https://static.javadoc.io/io.[...]0.10.0/io/vavr/Function5.html

Tylko ja jeszcze chciałem bez pliku i klasy CalendarFactory.

Możesz zapakować wiele publicznych interfejsów do jednej klasy:

public class Factories {
  public static interface CalendarFactory {
    Calendar make(Arg1 arg1, Arg2 arg2);
  }
  public static interface AnotherFactory {
    Something make(Arg1 arg1, Arg2 arg2);
  }
  public static void main(String[] args) {
    Factories.CalendarFactory calendarFactory = (arg1, arg2) -> { ... };
  }
}

Moim zdaniem nie byłoby to wcale kiepskie podejście. O ile moja wiedza w Javy się jeszcze nie przeterminiowała to słówko static przy interface sprawia, że możesz taki wewnętrzny interfejs zaimplementować lambdą z dowolnego miejsca.


"Programs must be written for people to read, and only incidentally for machines to execute." - Abelson & Sussman, SICP, preface to the first edition
"Ci, co najbardziej pragną planować życie społeczne, gdyby im na to pozwolić, staliby się w najwyższym stopniu niebezpieczni i nietolerancyjni wobec planów życiowych innych ludzi. Często, tchnącego dobrocią i oddanego jakiejś sprawie idealistę, dzieli od fanatyka tylko mały krok."
Demokracja jest fajna, dopóki wygrywa twoja ulubiona partia.
edytowany 3x, ostatnio: Wibowit, 2019-05-09 20:38

Pozostało 580 znaków

2019-05-09 20:50
0
TomRiddle napisał(a):

Zanim zacznę pytanie zaznaczę może co ja rozumiem przez określenia "Interfejs" oraz "Interfejs abstrakcyjny".

  • Interfejs to po prostu sposób korzystania z klasy: metody, pola, konstruktor, throws w metodzie, to czy zwraca/nie zwraca nulle - wszystko to ma związek z korzystaniem z klasy - a więc jest to interfejs.

Pola nie są częścią interfejsu tylko sposobem implementacji wykonania pewnego zadania, którym jest dostęp do danej. Nie wydaje mi się również, by konstrukcja była częścią interfejsu. Korzystanie z interfejsu oznacza, że obiekt wykonuje coś dla nas. Konstrukcja oznacza coś przeciwnego: to my wykonujemy coś dla obiektu, czyli tworzymy go. Interfejs to kontrakt, w ramach którego obiekty mówią: robimy takie i takie rzeczy. Ale czemu obiekty miałyby się ograniczać do tego w jaki sposób są tworzone? W Twoim przykładzie oba kalendarze mają parametry user i events, ale w przypadku innych interfejsów wcale nie muszą mieć tych samych parametrów by robić to samo.

Pozostało 580 znaków

2019-05-09 21:46
0
GutekSan napisał(a):
TomRiddle napisał(a):

Zanim zacznę pytanie zaznaczę może co ja rozumiem przez określenia "Interfejs" oraz "Interfejs abstrakcyjny".

  • Interfejs to po prostu sposób korzystania z klasy: metody, pola, konstruktor, throws w metodzie, to czy zwraca/nie zwraca nulle - wszystko to ma związek z korzystaniem z klasy - a więc jest to interfejs.

Pola nie są częścią interfejsu tylko sposobem implementacji wykonania pewnego zadania, którym jest dostęp do danej.

Pola publiczne/protected?

Nie wydaje mi się również, by konstrukcja była częścią interfejsu. Korzystanie z interfejsu oznacza, że obiekt wykonuje coś dla nas. Konstrukcja oznacza coś przeciwnego: to my wykonujemy coś dla obiektu, czyli tworzymy go. Interfejs to kontrakt, w ramach którego obiekty mówią: robimy takie i takie rzeczy. Ale czemu obiekty miałyby się ograniczać do tego w jaki sposób są tworzone? W Twoim przykładzie oba kalendarze mają parametry user i events, ale w przypadku innych interfejsów wcale nie muszą mieć tych samych parametrów by robić to samo.

Właśnie z myślą o takich osobach jak Ty zaznaczyłem co ja rozumiem poprzez określenia "Interfejs" i "Interfejs abstrakcyjny" na samej górze mojego postu. Twój opis "interfejsu" w mojej definicji wpasuje się w nazwę "Interfejs abstrakcyjny".


char mander; bool basaur;
Zaawansowana biblioteka T-Regx do wyrażeń regularnych w PHP

Pozostało 580 znaków

2019-05-09 21:49
0
Wibowit napisał(a):

W Vavr 0.x masz generyczne gotowce, np: https://static.javadoc.io/io.[...]0.10.0/io/vavr/Function5.html

Tylko ja jeszcze chciałem bez pliku i klasy CalendarFactory.

Możesz zapakować wiele publicznych interfejsów do jednej klasy:

public class Factories {
  public static interface CalendarFactory {
    Calendar make(Arg1 arg1, Arg2 arg2);
  }
  public static interface AnotherFactory {
    Something make(Arg1 arg1, Arg2 arg2);
  }
  public static void main(String[] args) {
    Factories.CalendarFactory calendarFactory = (arg1, arg2) -> { ... };
  }
}

No w zasadzie dokładnie o czymś takim myślałem (w zasadzie Twój przykład nie różni się niczym od mojego pierwszego), tylko że żeby właśnie do tego była dedykowana składnia, tak że cały ten interfejs Factories możnaby zastąpić jednym/dwoma znakami.

Kiedyś ludzie normalnie pisali new Runnable() { void run() { uważając to za całkowicie normalne, dopóki nie powstała do tego dedykowana składnia w postaci ->.


char mander; bool basaur;
Zaawansowana biblioteka T-Regx do wyrażeń regularnych w PHP
edytowany 1x, ostatnio: TomRiddle, 2019-05-09 21:50

Pozostało 580 znaków

2019-05-09 22:36
1

No w zasadzie dokładnie o czymś takim myślałem (w zasadzie Twój przykład nie różni się niczym od mojego pierwszego)

Różni się tym, że możesz napykać dużo public static interface w jednym pliku zamiast wielu plików.

tylko że żeby właśnie do tego była dedykowana składnia, tak że cały ten interfejs Factories możnaby zastąpić jednym/dwoma znakami.

Nie znam języka w którym można wyciągnąć typ z metody, w sensie coś takiego:

interface Interfejs {
  Typ1 metoda(TypA argA, TypB argB);
}

class Klasa {
  Interfejs.metoda metodaFabrykująca1;

  Klasa.new metodaFabrykująca2;
}

To z czym możesz jeszcze kombinować to np wykorzystać FunctionX z Vavr do skrócenia składni z:

public class Factories {
  public static interface CalendarFactory {
    Calendar make(Arg1 arg1, Arg2 arg2);
  }
  public static interface AnotherFactory {
    Something make(Arg1 arg1, Arg2 arg2);
  }
  public static void main(String[] args) {
    Factories.CalendarFactory calendarFactory = (arg1, arg2) -> { ... };
  }
}

do np

public class Factories {
  public static interface CalendarFactory extends Function2<Arg1, Arg2, Calendar>
  public static interface AnotherFactory extends Function2<Arg1, Arg2, Something>
  public static void main(String[] args) {
    Factories.CalendarFactory calendarFactory = (arg1, arg2) -> { ... };
  }
}

Wtedy CalendarFactory jest krótsze niż Function2<Arg1, Arg2, Calendar> i jeśli jest często używane to jest z tego zysk.


"Programs must be written for people to read, and only incidentally for machines to execute." - Abelson & Sussman, SICP, preface to the first edition
"Ci, co najbardziej pragną planować życie społeczne, gdyby im na to pozwolić, staliby się w najwyższym stopniu niebezpieczni i nietolerancyjni wobec planów życiowych innych ludzi. Często, tchnącego dobrocią i oddanego jakiejś sprawie idealistę, dzieli od fanatyka tylko mały krok."
Demokracja jest fajna, dopóki wygrywa twoja ulubiona partia.
edytowany 1x, ostatnio: Wibowit, 2019-05-09 22:38

Pozostało 580 znaków

2019-05-09 22:44
1

Zasadniczo mocno się mylisz, że żaden język nie ma. Bo jak najbardziej ma.

W trochę koślawy sposób dochodzisz do koncepcji typeclass.

Mam IMO lepszy przykład w postaci interfejsu JSONSerializable.
Każdy umi zrobić:

interface JSONSerializable {
  JSON toJSON();
}

i zaimplementować,
i jest polimorfizm,
i spoko.

Ale napiszmy teraz interfejs FromJSON.
Tak żeby była metoda, fromJSON(JSON jeON)-> T -= gdzie T to nasz typ.

Ta co szukamy to w istocie konstuktor - polimorficzny. I w takich językach jak Scala czy Haskell rzecz zupełnie normalna. Do uzyskania dzięki tzw. Typeclass.
(W Scali może lekko zabawnie, ale istnieje przyjęta konwencja jak to się robi).


Bardzo lubie Singletony, dlatego robię po kilka instancji każdego.
edytowany 1x, ostatnio: jarekr000000, 2019-05-10 07:44

Pozostało 580 znaków

2019-05-09 22:47
0

Tak na prawdę, to chciałbym dedykowaną składnię na coś takiego

§Calendar impl = §GregorianCalendar; // Albo §JulianCalendar

Calendar calendar = new Calendar§impl(user, events);

czyli w javie:

Class<Calendar> clazz = GregorianCalendar.class; // albo JulianCalendar.class

Constructor const = clazz.getConstructor(new Class[] {User.class, List.class});  // tylko to by się brało samo z interfejsu
Calendar calendar = (Calendar) const.newInstance(user, events);
                    // cast by się robił sam

char mander; bool basaur;
Zaawansowana biblioteka T-Regx do wyrażeń regularnych w PHP

Pozostało 580 znaków

2019-05-09 22:55
1

Zasadniczo mocno się mylisz, że żaden język nie ma. Bo jak najbardziej ma.

Pokaż jak. Pewnie się nie zrozumieliśmy.

Typeclass wymaga powtórzenia typów, np:

struct GregorianCalendar {
  a: i32,
  b: u32,
}

trait Calendar {
  // powtórzyłem typy a: i32 oraz b: u32. Za plus można uznać słówko Self które automatycznie oznacza konkretny typ.
  fn new(a: i32, b: u32) -> Self;
}

@TomRiddle:
Weź też pod uwagę, że klasa może mieć wiele konstruktorów. Co wtedy ma oznaczać taki §Calendar?


"Programs must be written for people to read, and only incidentally for machines to execute." - Abelson & Sussman, SICP, preface to the first edition
"Ci, co najbardziej pragną planować życie społeczne, gdyby im na to pozwolić, staliby się w najwyższym stopniu niebezpieczni i nietolerancyjni wobec planów życiowych innych ludzi. Często, tchnącego dobrocią i oddanego jakiejś sprawie idealistę, dzieli od fanatyka tylko mały krok."
Demokracja jest fajna, dopóki wygrywa twoja ulubiona partia.
edytowany 4x, ostatnio: Wibowit, 2019-05-09 22:59
Wyprzedziłem dyskusję i sobie dopisałem trochę. Bo jeśli ktoś widzi potrzebę polimorficznych kontruktorów, trochę mu się nie podobają fabryki, to jest IMO na drodze do typeclass. Fakt, że tu jeszcze pewnie pare stron dojdzie - na razie to jest problem XY. I polemizowałem, że tego nie ma żaden język. ALe mogę się mylić, bo może jednak OP chodzi o coś innego, na razie nadal nie jestem pewny o co. - jarekr000000 2019-05-10 07:51
Z tego co rozumiem OP chodzi o zmniejszenie narzutu składniowego. Nie podoba mu się dodawanie fabryki, czyli jednego interfejsu (z jedną metodą) na jedną hierarchię klas. Chciałby to skrócić chyba do zera. - Wibowit 2019-05-10 09:25

Pozostało 580 znaków

Odpowiedz
Liczba odpowiedzi na stronę

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

Użytkownik: Kamil Żabiński