Czy abstrakcyjne konstruktory mogłyby mieć sens?

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.

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

1

W Vavr 0.x masz generyczne gotowce, np: https://static.javadoc.io/io.vavr/vavr/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.

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.

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".

0
Wibowit napisał(a):

W Vavr 0.x masz generyczne gotowce, np: https://static.javadoc.io/io.vavr/vavr/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 ->.

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.

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).

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

0
jarekr000000 napisał(a):

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 -> 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).

Ale wiesz że ta implementacja miałaby być wybierana w runtime'ie, nie? O to się cały problem rozchodzi.

0

Nie rozumiem tylko celu tego. Interface definiuje zachowanie, a nie sposób w jaki to ma być "wykonane"

0
danek napisał(a):

Nie rozumiem tylko celu tego. Interface definiuje zachowanie, a nie sposób w jaki to ma być "wykonane"

Matko boska SPECJALNIE PO TO NAPISAŁEM WSTĘP na początku postu:

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.
  • 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.
2

No ale własnie pola i konstruktor to nie są elementy interface tylko "interface" konkretnej klasy. Czemu chcesz narzucać jakiejś implementacji co dokładnie ma przyjmować w konstruktorze? Może wynika to z tego, że nie do końca rozumiem co chcesz zrobić? Jaka ma być wartość dodana tej możliwości?

0
danek napisał(a):

No ale własnie pola i konstruktor to nie są elementy interface tylko "interface" konkretnej klasy. Czemu chcesz narzucać jakiejś implementacji co dokładnie ma przyjmować w konstruktorze? Może wynika to z tego, że nie do końca rozumiem co chcesz zrobić? Jaka ma być wartość dodana tej możliwości?

Na prawdę chcesz się kłócić o nazewnictwo...?

SPECJALNIE PO TO NAPISAŁEM WSTĘP W PIERWSZYM POŚCIE.

Dobrze, specjalnie dla Ciebie zmienię słownictwo.

  • Interfejs - Twoje użycie słowa interface - we wstępie jako "interfejs"
  • Maciek - Twoje użycie słowa "interface" - we wstępie jako" interfejs abstrakcyjny"

Mówię to już trzeci raz. Wartością dodaną miałaby być Dedykowana składania do wyboru implementacji Maćka przy instancjonowaniu.

Dokładnie tak samo jak -> jest już dedykowaną skladnią do instancjonowania interfejsów abstrakcyjnych. Nie wierzę że muszę to programistom mówić trzeci raz.

1
TomRiddle napisał(a):

Dokładnie tak samo jak -> jest już dedykowaną skladnią do instancjonowania interfejsów abstrakcyjnych. Nie wierzę że muszę to programistom mówić trzeci raz.

Zachowujesz się jak meNADŻER. Przyjmij, że jeżeli musisz ileś razy powtarzać, to może po prostu nie piszesz jasno. Jest taka możliwość, potencjalnie....
Nie wiem czy nie lepiej by było jakbyś jeszcze raz opisał bardziej rzeczywisty przypadek - z kontekstem. Mam podejrzenie XY.

Bo jeśli

Wartością dodaną miałaby być Dedykowana składania do wyboru implementacji Maćka przy instancjonowaniu.

To ewidentnie takie coś juz istnieje i nazywa sie new - daje możliwośc wyboru implementacji przy instancjonowaniu.
Tytlko nadal nmyśle, że nie o to chodzi...

A wracając do pierwszego postu również Templaty w C++ jak najbardziej twój przypadek obsługują.

3

@TomRiddle
Cały pomysł z abstrakcyjnym konstruktorem generalnie przeczy zasadzie polimorfizmu, bo klasa (czy interfejs) nadrzędna musi wiedzieć jakie zależności są potrzebne klasom dziedziczącym. W ogólnym rozrachunku takie coś nie powinno mieć miejsca.

Dedykowana zwięzła składnia do skracania kodu hierarchi klas z takimi samymi konstruktorami prowadziłaby do tego, że ludzie na siłę by takie rzeczy tworzyli.

Zamiast Calendar mógłbym mieć np UserRepository i podklasy InMemoryUserRepository oraz DbUserRepository. DbUserRepository musi przyjmować pulę połączeń do bazki, a dla InMemoryUserRepository taka pula jest niepotrzebna. Gdybym chciał wstawić taki abstrakcyjny konstruktor jak ty chcesz to wstawiłbym do niego pulę, a w InMemoryUserRepository bym go zignorował.

interface UserRepository {
  constructor UserRepository(DbConnectionPool dbConnPool);
}
class DbUserRepository implements UserRepository {
  DbUserRepository(DbConnectionPool dbConnPool) {
    // tutaj zapisuję dbConnPool
  }
}
class InMemoryUserRepository implements UserRepository {
  InMemoryUserRepository(DbConnectionPool ignored) {
    // tutaj ignoruję referencję do puli połączeń bazodanowych
  }
}

Taki abstrakcyjny konstruktor w praktyce to byłby raczej antywzorzec, moim zdaniem.

0
Wibowit napisał(a):

@TomRiddle
Cały pomysł z abstrakcyjnym konstruktorem generalnie przeczy zasadzie polimorfizmu, bo klasa (czy interfejs) nadrzędna musi wiedzieć jakie zależności są potrzebne klasom dziedziczącym. W ogólnym rozrachunku takie coś nie powinno mieć miejsca.

A jak to się ma to tego że korzystając z fabryki, do instancjonowania konkretnych implementacji korzysta się z jednej metody calendarFactory.create(user, events);. Tutaj też nie ma informacji o zależnościach (zapewne wszystkie zależności ma Factory).

Czemu przypadek z dedykowaną składnią też nie mógłby brać zależności "z góry"?

1
  1. Fabryki to zupełnie inny kod niż tworzone instancje. Fabryki zwykle tworzy się w projekcie, w którym znasz wszystkie tworzone podklasy.
  2. Fabryki też mogą mieć swoje zależności, czyli teoretycznie możesz mieć:
class DbUserRepositoryFactor implements UserRepositoryFactory {
  DbUserRepositoryFactory(DbConnectionPoolFactory aaa) { ... }
  UserRepository makeUserRepository() { ... }
}

Fabryki czy ogólnie DI możesz zrobić na milion sposobów, zamiast wspawać to w jeden sztywny sposób do klasy z logiką biznesową.

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