Funkcyjny język ułatwiający jednolity sposób kodowania - czy to możliwe?

0

Tak sobię myślę odnośnie języka Go i tego, że duża część kodu z std/thirdParty libów oraz gotowych aplikacji wygląda podobnie niezależnie od tego przez kogo ten kod był pisany. Zgaduję, że te czynniki mocno wpływają na to:

  1. duża biblioteka standardowa, która jest pewnym przykładem pokazującym jak kod powinien wyglądać
  2. prosty język, gdzie nie ma dużo konkurujących ze sobą ficzerów. Każdy dodatkowy ficzer to możliwy rozjazd, bo ktoś sobie uzna, że A jest lepsze od B
  3. paradygmat imperatywny. Do napisania dowolnego programu wystarczy if, goto, funkcje biblioteczne, operacje arytmetyczne i pamięć. W języku funkcyjnym potrzeba dużo więcej bazy, żeby napisać coś praktycznego
  4. małe pole do popisu, jeśli chodzi o abstrakcje. Są interfejsy a nie dziedziczenie (które IMO dużo łatwiej nazwać w jednolity sposób niż w przypadku hierarchii obiektów)
  5. statyczne typowanie: jak go nie ma to ludzie walą dynamiczne sprawdzanie (bo jest ładniej) przez co część kodu (zwłaszcza biblioteki) wygląda inaczej niż reszta

Pytanie: czy takie coś jest możliwe? Jeśli chodzi o punkt 1, 4, 5 to raczej na pewno tak. Dużo bardziej martwię się o 2, 3. Jak język funkcyjny jest najbliższy temu opisowi?

2

Mam kilka typów języków funkcyjnych. Pierwszy to Elixir i jego całe bogactwo Erlanga. Drugi to F# ale ma małą bibliotekę. Trzeci to Scala i Idris i całe bogactwo JVM. Do Go jest bardzo podobny Odin, składnia praktycznie identyczna, oraz V ma sporo funkcyjnych opcji i jest podobny do Go.

2

mysle iz jest to mozliwe tylko ktos musialby zaplanowac taki jezyk. Bo w tej chwili sytuacja z Haskellem wyglada jak sytuacja z C++ czyli zbiera kazdy mozliwy feature a to co chcesz oznaczaloby iz w tym nowym jezyku nie bedzie jakis feature ktore sa w Haskellu tak samo jak Go nie ma wszystkich feature z C++

4

zaprojektowanie i implementacja statycznie typowanego języka funkcyjnego samo w sobie jest wyższą szkołą jazdy. zrobienie tego dodatkowo tak, żeby zrobić dobrze wielu malkontentom i jednocześnie zachować umiar i równowagę w języku, to podniesienie poprzeczki jeszcze bardziej. na razie rozwój statycznie typowanych języków funkcyjnych to nadal (w dużej mierze) akademickie badania. takie języki można z powodzeniem stosować w praktyce (np. scalę :) ), ale jest jeszcze za wcześnie na stabilność (czy tam stagnację architekturalną) ekosystemu.

za to kaczo typowane języki funkcyjne to zupełnie inna historia. te maja dużo dłuższą historię (np. cała masa lispów) i chyba nie ma tam już jakichś wielkich zwrotów akcji jeśli chodzi o styl programowania.

2

Powiedziałbym że Clojure jest spoko językiem do tego, gdyby nie to że to jest przyspawane do JVM :/

Z tym statycznym typowaniem to też nie jest tak kolorowo, bo static typing ma jeden mankament — mianowicie, są przypadki gdzie nie da się osiągnąć dependency inversion ze statycznym typowaniem :o Albo inaczej, są przypadki, gdzie żeby osiągnąć DI, trzeba złamać statyczne typowanie. Dynamiczne typowanie to jedyne podejście które w 100% umożliwia DI.

Tylko że praktycznie żaden dynamicznie typowany język nie udostępnia takiego na prawdę porządnego typowania :/ Idealny język powinien mieć typowanie tak rzetelne jak aktualnie statycznie typowane języki - tylko że runtime'ie.

screenshot-20240109124625.png

Tylko że takich języków nie ma 🫤

3

btw zastanawiam sie ktore feature z Haskella mialyby sie znalezc w takim jezyku. Np czy byloby GADT czy tylko ADT?

BTW w jezyki ktore mozna by uznac za zaplanowane to purescript, idris i BQN. Czy spelniaja one twoja definicje prostego jezyka funkcyjnego?

Albo inaczej, są przypadki, gdzie żeby osiągnąć DI, trzeba złamać statyczne typowanie

😮 mozesz rozwinac, @Riddle ?

1
KamilAdam napisał(a):

Albo inaczej, są przypadki, gdzie żeby osiągnąć DI, trzeba złamać statyczne typowanie

😮 mozesz rozwinac, @Riddle ?

No np jak masz dwa moduły, w którym pierwszy ma abstrakcyjna fabrykę, a drugi z niej korzysta. Wtedy ten drugi jakoś musi "powiedzieć" pierwszej jakiego typu obiekt ma dostać. I albo przekażesz stringa (łamiąc statyczne typowanie), albo dodasz enuma łamiąc dependency inversion (bo takiego enuma nie da się "odwrócić" w DI).

Mówiąc inaczej, jak masz poprawnie zrobione Dependency Inversion, to zawsze się znajdzie przypadek, którego się nie da typować w compile-time (jedynie w runtime).

3

dlaczego enum lamie DI? I czemu nie moge go zastapic interfejsem?

5
Riddle napisał(a):
KamilAdam napisał(a):

Albo inaczej, są przypadki, gdzie żeby osiągnąć DI, trzeba złamać statyczne typowanie

😮 mozesz rozwinac, @Riddle ?

No np jak masz dwa moduły, w którym pierwszy ma abstrakcyjna fabrykę, a drugi z niej korzysta. Wtedy ten drugi jakoś musi "powiedzieć" pierwszej jakiego typu obiekt ma dostać. I albo przekażesz stringa (łamiąc statyczne typowanie), albo dodasz enuma łamiąc dependency inversion (bo takiego enuma nie da się "odwrócić" w DI).

Mówiąc inaczej, jak masz poprawnie zrobione Dependency Inversion, to zawsze się znajdzie przypadek, którego się nie da typować w compile-time (jedynie w runtime).

To raczej bzdura. Może lepiej podaj przykład konkretny w kodzie. Oczywiście możemy bawić się w Ridlyzmy czyli kłócić co to jest "poprawnie zrobione Dependency Inversion", ale generalnie nie widzę problemu, o którym piszesz.

1
Riddle napisał(a):

No np jak masz dwa moduły, w którym pierwszy ma abstrakcyjna fabrykę, a drugi z niej korzysta. Wtedy ten drugi jakoś musi "powiedzieć" pierwszej jakiego typu obiekt ma dostać. I albo przekażesz stringa (łamiąc statyczne typowanie), albo dodasz enuma łamiąc dependency inversion (bo takiego enuma nie da się "odwrócić" w DI).

Mówiąc inaczej, jak masz poprawnie zrobione Dependency Inversion, to zawsze się znajdzie przypadek, którego się nie da typować w compile-time (jedynie w runtime).

To jest tzw. circular reasoning. Twierdzisz, że:

  1. Enumy łamią DI
  2. Ponieważ nie da się stosować poprawnie DI w obiektach, który przyjmują enumy
  3. Ponieważ enumy łamią DI

Takie perpetum mobile logiczne.

0
jarekr000000 napisał(a):

Oczywiście możemy bawić się w Ridlyzmy

:)
na jakiej to licencji udostępniasz? Copyleft ?

1

Wstęp

Rozumiem, że dla niektórych to może być szok! Przecież dynamiczne typowanie jest flaky i mniej bezpieczne, a statyczne typowanie jest safe, sound i bezpieczne. Owszem — ale czasem statyczne typowanie zmusza nas żeby gdzieś dodać coupling, nawet pomiędzy rzeczami pomiędzy którymi tego nie chcemy. I dlatego czasem statyczne typowanie zmusza nas do złamania DI.

Rozwinięcie

jarekr000000 napisał(a):

Oczywiście możemy bawić się w Ridlyzmy czyli kłócić co to jest "poprawnie zrobione Dependency Inversion"

Poprawnie zrobione dependency inversion to takie, w którym zależność w kodzie źródłowym jest w jedną stronę (tylko jedną), ale kontrola przepływu jest w drugą (tylko w drugą).

  • Bez DI — zależność w kodzie i kontrola przepływu idzie w jedną stronę (obie w jedną, albo obie w drugą). Czyli kierunek kontroli przepływu jest taki sam jak kierunek zależności.
  • "Dependency Inversion" dlatego nazywa się "inversion", bo pozwala obrócić te dwa kierunki w przeciwne strony. Czyli kierunek kontroli przepływu jest odwrotny niż kierunek zależności - stąd "odwrócenie zależności".

Innymi słowy, jeśli pomiędzy dwoma modułami jest DI, to jeden z tych modułów da się skompilować bez drugiego (czyt. sprawdzić typy w jednym, bez drugiego).

KamilAdam napisał(a):

dlaczego enum lamie DI? I czemu nie moge go zastapic interfejsem?

To ja może powiem jeszcze raz.

Masz abstrakcyjną fabrykę w głównej części programu. Dołaczasz plugin który implementuje tą fabrykę i zwraca jeden rodzaj obiektów. Super. Wszystko działa, static typing działa, DI działa. Zwykły polimorfizm. Teraz — co w sytuacji, w którym ta fabryka ma stworzyć kilka różnych rodzajów obiektów?

Masz kilka sposobów żeby ją sparametryzować:

  • Jeśli piszesz w dynamicznie typowanym języku:
    • po prostu dodajesz metodę i tyle. Wszystko gra. Interpreter wymusi istnienie tej metody w runtime.
  • Jeśli piszesz w statycznie typowanym języku, to:
    • Albo z aplikacji do pluginu przekażesz prymityw: integer albo string — tylko wtedy nie ma statycznego typowania, bo kompilator nie jest w stanie tego sprawdzić podczas kompilacji
    • Albo z aplikacji do pluginu przekażesz enum (czy tam nawet implementację interfejs). Tylko że taki enum albo interfejs musiałby zostać zadeklarowany w głównej części programu, a nie w pluginie — tylko wtedy nie ma DI. Bo wtedy chcąc dodać nową wartość w pluginie, musiałbyś zmodyfikować moduł aplikacji.
    • Nie możesz zawołać niezadeklarowanej metody bo kompilator, który sprawdza statycznie typ, Ci na to nie pozwoli.

Oczywiście — jeśli nie musisz parametryzować tej fabryki, to wtedy problemu nie ma — i zarówno static typing jak i DI są możliwe równocześnie.

jarekr000000 napisał(a):

To raczej bzdura. Może lepiej podaj przykład konkretny w kodzie, ale generalnie nie widzę problemu.

Dobrze, napiszę w Javie, może być?

Mamy dwa osobne moduły, server.jar oraz client.jar. Chcemy żeby client.jar jar był pluginem od server.jar (czyli server.jar nie wie o istnieniu client.jar). Zwykłe Dependency Inversion.

server.jar

package server;

interface FruitShop {
  Fruit buy(String fruitName);
}

interface Fruit {
  void doStuff();
}

class Application {
  FruitShop shop;
  public Application(FruitShop shop) {
    this.shop = shop;
  }

  public void run() {
    Fruit obj1 = factory.buy("apple");  // tutaj nie ma sprawdzania typów,
    Fruit obj2 = factory.buy("orange"); // np można wywołac .create("banana");
                                        // i żaden kompilator tego nie sprawdzi      
    obj1.doStuff();
  }
}

client.jar

package client;

import server.FruitShop;
import server.Fruit;

class ClientShop implements FruitShop {
  Fruit buy(String something) {
    if (something == "apple") {
      return new Apple();
    }
    return new Orange();

    // jeśli ktoś przekaże złą wartość, np "banana",
    // to system typów tego nie sprawdzi.

    // oczywiście, można rzucić wyjątek typu `new InvalidType(something)`, ale to
    // jest praktycznie dynamiczne typowanie, bo nie będzie sprawdzone w compile time,
    // błąd poleci w runtime.
  }
}

class Apple implements Fruit {}

class Orange implements Fruit {}

Jakie kryteria musi spełniać ta aplikacja, żeby można było powiedzieć że spełnia DI

  1. W server.jar ma nie być żadnego import client, bo wtedy client nie byłby pluginem, a więc łamie DI.
  2. W client.jar może być dowolna ilość import server.
  3. W client.jar chcę móc dodawać nowe implementacje Fruit do woli.
  4. W client.jar chce móc sprawić, że ClientShop.buy() może zwracać te nowe owoce.
  5. Kod server.jar chcę móc skompilować bez udziału client.jar.

I teraz to czego się nie da zrobić - to nie da się w compile time sprawdzić poprawności kodu w server.jar pod względem typów, dlatego że w compile-time server nawet nie wie, jakie typy są w client.jar. Da się to zrobić tylko i wyłącznie w runtime - dlatego że w runtime ten drugi moduł jest, w compiletime go nie ma.

Dlaczego:

  • Dla przykładu, gdybyś chciał zrobić że FruitShop.buy() przymuje enum, to ten enum musiałby być zadeklarowany w server.jar. Jeśli wtedy client.jar chciałby dodać nowy typ owocu, to wtedy musiałby zmienić ten enum, łamiąc DI.
  • Jeśli chciałbyś zamiast parametrów mieć różne metody w FruitShop, to jeśli plugin chciałby dodać nowy obiekt, to musiałby dodać nową funkcję do FruitShop, tym samym łamiąc DI.

Dlaczego w dynamicznym typowaniu to działa?

  • Bo jeśli mielibyśmy dynamiczne typowanie, to FruitShop nie jest być zadeklarowany w server.jar, i można do niego dowolnie dodawać funkcje i enumy.

Czemu statyczne typowanie czasem wyklucza DI?

Dlatego że dowolna forma statycznego typowania (takiego sprawdzanego w compiletime) musi mieć te typy w server.jar, tym samym "wiążąc je" z zależnością w kodzie źródłowym.

Jedyny sposób żeby osiągnąć DI zawsze, to jest nie mieć typów w server.jar, a skoro tak, to nie można przeprowadzić statycznego sprawdzania typów (jedynie dynamiczne).

Zakończenie

Kluczem do Dependency Inversion jest polimorfizm - który istnieje zarówno w statycznie i dynamicznie typowanych językach. Różnica polega na tym, że w statycznie typowanych językach deklaracje polimorficznych calli muszą być znane podczas kompilacji (np musi być zadeklarowany interfejs), a w dynamicznie typowanych nie - i przez to czasem dependency inversion w statycznie typowanych językach czasem nie jest możliwe. Bo jeśli zajdzie potrzeba modyfikacji tego typu - to w dynamicznym typowaniu, zmiana modułu aplikacji nie jest konieczna, a w statycznie typowanych jest.

Jak więc osiągnąć sprawdzanie typów? Tylko dynamicznie, czyli w runtime'ie.

PS: Jak by to wyglądało w dynamicznie typowanym języku?

server.py

class Application:
  def __init__(shop):
    this.shop = shop

  def run() {
    obj1 = factory.buyApple()
    obj2 = factory.buyOrange() # interpreter pilnuje istnienia metody
                               # nie da się wywołac self.buyBanana()
                                

    obj1.doStuff();

client.py

class ClientShop:
  def buyApple(self):
    return Apple()

  def buyOrange(self):
    return Orange()

class Apple:
  pass
class Orange:
  pass

Teraz mogę do woli dodawać nowe funkcje i typy w client.py, a server.py się nie zmieni - osiągnięte Dependency Inversion

Oczywiście - większość dynamicznie typowanych języków nie jest tak rzetelna

4

@Riddle czyli chcesz uzywac api niezdefiniowanego jeszcze w momencie uzycia i dziwisz sie iz to jest niemozliwe w statycznie typowanych jezykach? Ciekawe

0
KamilAdam napisał(a):

@Riddle czyli chcesz uzywac api niezdefiniowanego jeszcze w momencie uzycia i dziwisz sie iz to jest niemozliwe w statycznie typowanych jezykach? Ciekawe

Czemu niezdefiniowanego? Po prostu chcę w pluginie dodawać nowe funkcjonalności, tak żeby moduł aplikacji nie wymagał zmiany. Da się to zrobić w pewnych prostych case'ach - korzystając z polimorfizmu, wystarczy dodać swoją implementację interfejsu i po sprawie. DI gotowe.

Ale w innych nie, kiedy musimy sparametryzować dostarczane implementacje - wtedy musimy przekazać string albo enum (jedno łamie static typing, drugie łamie DI).

KamilAdam napisał(a):

i dziwisz sie iz to jest niemozliwe w statycznie typowanych jezykach? Ciekawe

Nie dziwię się. Ja to przedstawiłem jako fakt. Czasem nie da się jednocześnie mieć statycznego typowania i dependency inversion. Tyle.

1

@Riddle: twoje przykłady to nie jest żadne Dependency Inversion. Ta zasada oznacza tyle, że moduły z abstrakcjami nie powinny zawierać implementacji, tylko moduł z implementacją powinien zależeć od modułu z abstrakcją. U ciebie są dwa moduły z implementacją, ergo to nie jest DI.

0
wartek01 napisał(a):

@Riddle: twoje przykłady to nie jest żadne Dependency Inversion. Ta zasada oznacza tyle, że moduły z abstrakcjami nie powinny zawierać implementacji, tylko moduł z implementacją powinien zależeć od modułu z abstrakcją. U ciebie są dwa moduły z implementacją, ergo to nie jest DI.

twoje przykłady to nie jest żadne Dependency Inversion. - to jest dependency inversion, dlatego że w client.jar mogę dodawać funkcjonalności, w taki sposób ze server.jar nie wymaga zmiany. Można też skompilować server.jar bez użycia client.jar. Spełnione jest też kryterium że kontrola przepływu jest w drugą stronę niż zależności (client dostarcza polimorficznie Fruit.doStuff(), ale server nie wie nic o client.jar). Czego więcej Ci brakuje?

Ta zasada oznacza tyle, że moduły z abstrakcjami nie powinny zawierać implementacji, tylko moduł z implementacją powinien zależeć od modułu z abstrakcją. yy... no nie do końca. W DI moduły które zawierają abstrakcje mogą mieć też implementację, tak długo jak ta implementacja jest częścią modułu aplikacji. To czego nie powinna zawierać, to implementacja związana z pluginem - ta powinna być wyniesiona, tak jak w moim przykładzie.Chyba że się mylę, to podeślij źródło.

Ale nawet jak chcesz się upierać że chodzi o "abstrakcje i implementacje", to w banalny sposób można wynieść te abstrakcje, i wszystkie moje argumenty z poprzedniego posta nadal trzymają. Twój już nie, bo teraz w abstractions.jar nie ma żadnej implementacji.

abstractions.jar

package abstractions;

interface FruitShop {
  Fruit buy(String fruitName);
}

interface Fruit {
  void doStuff();
}

server.jar

package server;

import abstractions.FruitShop;
import abstractions.Fruit;

class Application {
  FruitShop shop;
  public Application(FruitShop shop) {
    this.shop = shop;
  }

  public void run() {
    Fruit obj1 = factory.buy("apple");  // tutaj nie ma sprawdzania typów,
    Fruit obj2 = factory.buy("orange"); // np można wywołac .create("banana");
                                        // i żaden kompilator tego nie sprawdzi      
    obj1.doStuff();
  }
}

client.jar

package client;

import abstractions.FruitShop;
import abstractions.Fruit;

class ClientShop implements FruitShop {
  Fruit buy(String something) {
    if (something == "apple") {
      return new Apple();
    }
    return new Orange();

    // jeśli ktoś przekaże złą wartość, np "banana",
    // to system typów tego nie sprawdzi.

    // oczywiście, można rzucić wyjątek typu `new InvalidType(something)`, ale to
    // jest praktycznie dynamiczne typowanie, bo nie będzie sprawdzone w compile time,
    // błąd poleci w runtime.
  }
}

class Apple implements Fruit {}

class Orange implements Fruit {}
5

podejmę rękawicę :) prawdopodobnie problem nie jest do końca precyzyjnie opisany, ale odniosę się do tego co napisał @Riddle

serwer nie wie nic o typach z klientów, więc metoda run w poniższym kodzie nie może być otypowana. tego typu metody mają sens tylko w kliencie. poza tym jest chyba literówka, tzn. pomylone shop z factory:

server.jar

package server;

interface FruitShop {
  Fruit buy(String fruitName);
}

interface Fruit {
  void doStuff();
}

class Application {
  FruitShop shop;
  public Application(FruitShop shop) {
    this.shop = shop;
  }

  public void run() {
    Fruit obj1 = factory.buy("apple");  // tutaj nie ma sprawdzania typów,
    Fruit obj2 = factory.buy("orange"); // np można wywołac .create("banana");
                                        // i żaden kompilator tego nie sprawdzi      
    obj1.doStuff();
  }
}

cała klasa Application wraz metodą run (albo ogólnie kod tego typu) musi być przeniesiona do klienta. resztę można by przemodelować tak:

package server;

interface FruitShop<T extends Fruit> {
  T buy(Class<T> fruitType);
}

interface Fruit {
  void doStuff();
}

klient wtedy może użyć sealed classes https://openjdk.org/jeps/409 do implementacji:

client.jar

package client;

import server.FruitShop;
import server.Fruit;

class ClientShop implements FruitShop<MyFruit> {
  MyFruit buy(Class<MyFruit> fruitType) {
    if (fuitType == Apple.class) {
      return new Apple();
    }
    return new Orange();
  }
}

sealed interface MyFruit extends Fruit {}

class Apple implements MyFruit {}

class Orange implements MyFruit {}

do tego można klepnąć test oparty o metodę https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/Class.html#getPermittedSubclasses() który sprawdzi, że metoda ClientShop.buy działa poprawnie dla każdego typu (tzn. test dla każdej podklasy zwróconej z getPermittedSubclasses() sprawdza czy metoda buy zwraca instancję podklasy).

jeszcze wracając do tego kodu (poprawiłem factory na shop, bo tak chyba powinno tu być):

class Application:
  def __init__(self, shop):
    self.shop = shop

  def run(self):
    obj1 = self.shop.buyApple()
    obj2 = self.shop.buyOrange() # interpreter pilnuje istnienia metody
                                 # nie da się wywołac self.buyBanana()
                                

    obj1.doStuff();

to jeśli to jest w serwerze to np. usunięcie metody z klienta wymaga przerobienia serwera, tak czy siak. co tu niby to kacze typowanie daje? tak czy siak musisz zmienić powyższą metodę jeśli zmieniasz klienta, bo powyższa metoda zależy od szczegółów implementacyjnych klienta (i dlatego nie pasuje do serwera). czegoś nie widzę?

0
Wibowit napisał(a):

serwer nie wie nic o typach z klientów, więc metoda run w poniższym kodzie nie może być otypowana.

Tak.

Inaczej mówiąc metoda run() nie może być otypowana w statycznie typowanym języku (ale może w dynamicznie typowanym).

Wibowit napisał(a):

tego typu metody mają sens tylko w kliencie.
cała klasa Application wraz metodą run (albo ogólnie kod tego typu) musi być przeniesiona do klienta

Pozwól że powiem to co Ty, tylko trochę inaczej:

"cała klasa Application wraz metodą run (albo ogólnie kod tego typu) musiAŁABY być przeniesiona do klienta. **żeby dało się osiągnąć DI wraz ze statycznym typowaniem. **"

Bo jeśli tak, to z tym się zgadzam - faktycznie, żeby osiągnąć DI i statyczne typowanie ta metoda musiałaby zostać przeniesiona. Co znaczy, Q.E.D że jeśli ta metoda jest w server.jar to nie da się zastosować jednocześnie DI i statycznego typowania.

Jasne, że jeśli możemy przenieść część kodu aplikacji do pluginu to to "pyknie". To miałem na myśli, kiedy pisałem wcześniej że są przypadki kiedy da się osiągnąć jednocześnie static typing i DI.

Poza tym, nie kupuję tego argumentu. Wiadomo że zawsze możesz przenieść kod z a do b, i wtedy nie ma problemu z DI. Możemy w ogóle połączyć te dwa moduły w jeden, i wtedy mieć perfekcyjne statyczne typowanie.

.resztę można by przemodelować tak:

Wibowit napisał(a):
package server;

interface FruitShop<T extends Fruit> {
  T buy(Class<T> fruitType);
}

interface Fruit {
  void doStuff();
}

klient wtedy może użyć sealed classes https://openjdk.org/jeps/409 do implementacji:

client.jar

package client;

import server.FruitShop;
import server.Fruit;

class ClientShop implements FruitShop<MyFruit> {
  MyFruit buy(Class<MyFruit> fruitType) {
    if (fuitType == Apple.class) {
      return new Apple();
    }
    return new Orange();
  }
}

sealed interface MyFruit extends Fruit {}

class Apple implements MyFruit {}

class Orange implements MyFruit {}

(drobna uwaga: Użyłeś klasy obiektu zamiast stringa. Pod względem DI właściwie tożsame z enumem, ale niech będzie).

To teraz pokaż jak wygląda metoda run() w server.jar? Bo w moim przykładzie run() ma być w server.jar. Jak przeniesiesz run() z servera do klienta, to tak na prawdę ignorujesz mój przykład, na rzecz innego przykładu w którym da się zastosować static typing i DI.

Powtarzam: są przypadki gdzie da się użyć DI i statycznego typowania razem. Wiem to. Ale to co napisałem to to - że są też przypadki gdzie się nie da - gdzie czasem statyczne typowanie zmusi nas do złamania DI, a chęć użycia DI zmusi nas do złamania typów - i podałem przykład takiego programu, gdzie run() jest w serverze (bo np jest współdzielona pomiędzy innymi pluginami które mają mieć tą samą logikę, bo mamy np client1.jar, client2.jar, client3.jar - i nie chcemy przenosić run() do każdego z nich).

Wibowit napisał(a):

do tego można klepnąć test oparty o metodę https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/Class.html#getPermittedSubclasses() który sprawdzi, że metoda ClientShop.buy działa poprawnie dla każdego typu (tzn. test dla każdej podklasy zwróconej z getPermittedSubclasses() sprawdza czy metoda buy zwraca instancję podklasy).

Jak najbardziej warto sprawdzać typy testami, również jak programujesz w dynamicznie typowanym języku. Testy typów są bardzo ważne.

Wibowit napisał(a):

to jeśli to jest w serwerze to np. usunięcie metody z klienta wymaga przerobienia serwera, tak czy siak. co tu niby to kacze typowanie daje? tak czy siak musisz zmienić powyższą metodę jeśli zmieniasz klienta. czegoś nie widzę?

No oczywiście że jeśli usuniesz metodę, to to nie jest kompatybilne wstecz.

Zakończenie

Ja też chciałbym żeby statyczne typowanie było lekiem na całe zło, i dało się wszystko sprawdzić w compile-time'ie. I da się - ale wtedy zawsze jest gdzieś miejsce gdzie DI jest złamane :/ To nie jest tak że mam jakąś złą wolę, że na siłę chce popsuć static typing wszystkim. Po prostu nie da się zawsze tych dwóch rzeczy mieć. Statyczne typowanie ma zalety, ale ma też koszty - i takim kosztem jest to że czasem nie da się uż

4
Riddle napisał(a):
Wibowit napisał(a):

cała klasa Application wraz metodą run (albo ogólnie kod tego typu) musi być przeniesiona do klienta. resztę można by przemodelować tak:

Jasne, że jeśli możemy przenieść część kodu aplikacji do pluginu to to "pyknie". To miałem na myśli, kiedy pisałem wcześniej że są przypadki kiedy da się osiągnąć jednocześnie static typing i DI.

Poza tym, nie kupuję tego argumentu. Wiadomo że zawsze możesz przenieść kod z a do b, i wtedy nie ma problemu z DI. Możemy w ogóle połączyć te dwa moduły w jeden, i wtedy mieć perfekcyjne statyczne typowanie.

Czyli Ty nie twierdzisz, że są przypadki, że nie da się osiągnąć DI i static typinigu jednocześnie. Ty twierdzisz, że
Można stworzyć sztuczne ograniczenia gdzie powstaną przypadki gdzie nie da się osiągnąć Di i static typing.

Idąc dalej, można stworzyć sztuczne ograniczenia gdzie nie da się w języku dynamicznym stworzyć fabryki. Albo Di. itd.

0
anonimowy napisał(a):

Czyli Ty nie twierdzisz, że są przypadki, że nie da się osiągnąć DI i static typinigu jednocześnie.

Przeciwnie. Twierdzę, że są przypadki gdzie nie da się osiągnąć DI i static typing jednocześnie.

anonimowy napisał(a):

Ty twierdzisz, że
Można stworzyć sztuczne ograniczenia gdzie powstaną przypadki gdzie nie da się osiągnąć Di i static typing.

Tak, można.

Dodatkowo, takie ograniczenia same wychodzą podczas życia projektów.

Są też takie przypadki, że jak mamy dwa moduły, to mimo najszczerszych chęci nie da się tak poprzenosić metod z jednego modułu do drugiego żeby osiągnąć DI i static typing jednocześnie, i wtedy jedynym wyjściem byłoby połaczyć te dwa moduły w jeden. No albo dodać dynamiczne typowanie.

5
Riddle napisał(a):
Wibowit napisał(a):

to jeśli to jest w serwerze to np. usunięcie metody z klienta wymaga przerobienia serwera, tak czy siak. co tu niby to kacze typowanie daje? tak czy siak musisz zmienić powyższą metodę jeśli zmieniasz klienta. czegoś nie widzę?

No oczywiście że jeśli usuniesz metodę, to to nie jest kompatybilne wstecz.

w oryginalnej postaci problemu, jeśli dodam metodę w klientach to bez przerabiania serwera też nie zostaną automagicznie użyte. obojętnie czy dodajesz czy usuwasz metody z klientów, żeby serwer mógł na to zareagować to musisz go przerobić, a więc jest sprzężony ze szczegółami implementacyjnymi klientów.

Riddle napisał(a):

To teraz pokaż jak wygląda metoda run() w server.jar? Bo w moim przykładzie run() ma być w server.jar. Jak przeniesiesz run() z servera do klienta, to tak na prawdę ignorujesz mój przykład, na rzecz innego przykładu w którym da się zastosować static typing i DI.

Powtarzam: są przypadki gdzie da się użyć DI i statycznego typowania razem. Wiem to. Ale to co napisałem to to - że są też przypadki gdzie się nie da - gdzie czasem statyczne typowanie zmusi nas do złamania DI, a chęć użycia DI zmusi nas do złamania typów - i podałem przykład takiego programu, gdzie run() jest w serverze (bo np jest współdzielona pomiędzy innymi pluginami które mają mieć tą samą logikę, bo mamy np client1.jar, client2.jar, client3.jar - i nie chcemy przenosić run() do każdego z nich).

pokaż jakieś konkretne przykłady, gdzie widać sens takiego podejścia.

Riddle napisał(a):

Powtarzam: są przypadki gdzie da się użyć DI i statycznego typowania razem. Wiem to. Ale to co napisałem to to - że są też przypadki gdzie się nie da - gdzie czasem statyczne typowanie zmusi nas do złamania DI, a chęć użycia DI zmusi nas do złamania typów

ja bym raczej stwierdził: w 99.99% przypadków DI i statyczne typowanie ładnie współgrają, a w 0.01% sztucznych przypadków coś tam nie gra :) jak dla mnie to bardzo łatwy do zaakceptowania kompromis. nie widzę powodu, by odrzucać statyczne typowanie, bo w 0.01% sztucznych przypadków coś nie spełnia warunków.

0
Wibowit napisał(a):
Riddle napisał(a):

ja bym raczej stwierdził: w 99.99% przypadków DI i statyczne typowanie ładnie współgrają, a w 0.01% sztucznych przypadków coś tam nie gra :) jak dla mnie to bardzo łatwy do zaakceptowania kompromis. nie widzę powodu, by odrzucać statyczne typowanie, bo w 0.01% sztucznych przypadków coś nie spełnia warunków.

Ale ja nie mówiłem, żeby odrzucać statyczne typowanie :|

Ja powiedziałem że nie da się mieć zawsze statycznego typowania oraz dependency inversion. To znaczy, że jeśli piszesz w statycznie typowanym języku (i wszędzie masz sprawdzane typy), i masz nietrywialny program podzielony na więcej niż jeden moduł, to idę o zakład że gdzieś w jakimś miejscu jest złamane dependency inversion. I usunięcie tego miejsca, łamie DI w innym. I jedynym sposobem na całkowite zniwelowanie tego, jest dynamiczne typowanie (np if na stringach). A jak osiągnąłeś DI, to wtedy idę o zakładać że gdzieś jest jakiś typ nie jest sprawdzany.

@Wibowit Też miałem takie przekonanie jak Ty kiedyś. Też mi się wydawało że Dependency Inversion jest super, i zawsze, jak się tylko jest wystarczająco mądrym i zna wszystkie sztuczki języka, to można ogarnąć DI i statyczne typowanie - i Ci którzy nie umieją, to po prostu się nie starają, nie poświęcili na to wystarczająco dużo czasu albo są gorsi. Także doskonale rozumiem Twoją postawę.

Wibowit napisał(a):
Riddle napisał(a):
Wibowit napisał(a):

to jeśli to jest w serwerze to np. usunięcie metody z klienta wymaga przerobienia serwera, tak czy siak. co tu niby to kacze typowanie daje? tak czy siak musisz zmienić powyższą metodę jeśli zmieniasz klienta. czegoś nie widzę?

No oczywiście że jeśli usuniesz metodę, to to nie jest kompatybilne wstecz.

w oryginalnej postaci problemu, jeśli dodam metodę w klientach to bez przerabiania serwera też nie zostaną automagicznie użyte. obojętnie czy dodajesz czy usuwasz metody z klientów, żeby serwer mógł na to zareagować to musisz go przerobić, a więc jest sprzężony ze szczegółami implementacyjnymi klientów.

To akurat napisałem taki przykład, żeby był łatwy do zrozumienia. Ale oczywiście masz rację, to musiałoby być zrobione tak, że aplikacja modułu inicjalizuję plugin i "samo się używa".

Riddle napisał(a):

To teraz pokaż jak wygląda metoda run() w server.jar? Bo w moim przykładzie run() ma być w server.jar. Jak przeniesiesz run() z servera do klienta, to tak na prawdę ignorujesz mój przykład, na rzecz innego przykładu w którym da się zastosować static typing i DI.

Powtarzam: są przypadki gdzie da się użyć DI i statycznego typowania razem. Wiem to. Ale to co napisałem to to - że są też przypadki gdzie się nie da - gdzie czasem statyczne typowanie zmusi nas do złamania DI, a chęć użycia DI zmusi nas do złamania typów - i podałem przykład takiego programu, gdzie run() jest w serverze (bo np jest współdzielona pomiędzy innymi pluginami które mają mieć tą samą logikę, bo mamy np client1.jar, client2.jar, client3.jar - i nie chcemy przenosić run() do każdego z nich).

pokaż jakieś konkretne przykłady, gdzie widać sens takiego podejścia.

No np piszesz przeglądarkę bazy danych, i chcesz dołączyć postgres-driver.jar, mysql-driver.jar i sqlite-driver.jar. Cała logika przeglądarki UI ma być w module przeglądarki. Nie chcesz przenosić logiki UI do driver'ów. Ale chcesz żeby UI mogło wołać niektóre funkcje z bazy danych, za pomocą driver'a. I chcesz móc napisać sobie nowy driver, np mongo-driver.jar jak Ci się będzie chciało, ale chcesz też żeby przeglądarka UI nie wymaga żadnych zmian. Oraz, chcesz dodawać nowe funkcje do postgres-driver.jar, tak żeby przeglądarka UI nie musiała być rekompilowana. I to się da zrobić - ale gdzieś będziesz musiał złamać statyczne typowanie.

Wibowit napisał(a):

ja bym raczej stwierdził: w 99.99% przypadków DI i statyczne typowanie ładnie współgrają, a w 0.01% sztucznych przypadków coś tam nie gra :) jak dla mnie to bardzo łatwy do zaakceptowania kompromis. nie widzę powodu, by odrzucać statyczne typowanie, bo w 0.01% sztucznych przypadków coś nie spełnia warunków.

No, ja bym powiedział inaczej. W każdy nietrywialnym projekcie, 99% masy kodu spełnia statyczne typowanie i DI, ale w każdym takim programie gdzieś jest miejsce w którym albo DI albo statyczne typowanie jest złamane.

Cytuję mój pierwszy post:

Riddle napisał(a):

mianowicie, są przypadki gdzie nie da się osiągnąć dependency inversion ze statycznym typowaniem :o Albo inaczej, są przypadki, gdzie żeby osiągnąć DI, trzeba złamać statyczne typowanie. Dynamiczne typowanie to jedyne podejście które w 100% umożliwia DI.

2

ja to robię tak, że wstrzykiwanie zależności jest robione w module, który w zależnościach ma wszystkie inne moduły.

przykładowy podział na moduły:

  • common, który od niczego nie zależy
  • persistence, zależący od common
  • kafka, zależący od persistence (i zależności przechodnich)
  • rest api, zależący od persistence (i zależności przechodnich)
  • app (który spina resztę, tzn. wstrzykuje co trzeba i odpala aplikację), zależący od wszystkich innych modułów

nie ma żadnego wstrzykiwania opartego o refleksję czy o stringi i ifologię na stringach.

Riddle napisał(a):
Riddle napisał(a):

To teraz pokaż jak wygląda metoda run() w server.jar? Bo w moim przykładzie run() ma być w server.jar. Jak przeniesiesz run() z servera do klienta, to tak na prawdę ignorujesz mój przykład, na rzecz innego przykładu w którym da się zastosować static typing i DI.

Powtarzam: są przypadki gdzie da się użyć DI i statycznego typowania razem. Wiem to. Ale to co napisałem to to - że są też przypadki gdzie się nie da - gdzie czasem statyczne typowanie zmusi nas do złamania DI, a chęć użycia DI zmusi nas do złamania typów - i podałem przykład takiego programu, gdzie run() jest w serverze (bo np jest współdzielona pomiędzy innymi pluginami które mają mieć tą samą logikę, bo mamy np client1.jar, client2.jar, client3.jar - i nie chcemy przenosić run() do każdego z nich).

pokaż jakieś konkretne przykłady, gdzie widać sens takiego podejścia.

No np piszesz przeglądarkę bazy danych, i chcesz dołączyć postgres-driver.jar, mysql-driver.jar i sqlite-driver.jar. Cała logika przeglądarki UI ma być w module przeglądarki. Nie chcesz przenosić logiki UI do driver'ów. Ale chcesz żeby UI mogło wołać niektóre funkcje z bazy danych, za pomocą driver'a. I chcesz móc napisać sobie nowy driver, np mongo-driver.jar jak Ci się będzie chciało, ale chcesz też żeby przeglądarka UI nie wymaga żadnych zmian. Oraz, chcesz dodawać nowe funkcje do postgres-driver.jar, tak żeby przeglądarka UI nie musiała być rekompilowana. I to się da zrobić - gdzieś będziesz musiał złamać statyczne typowanie.

do przeprowadzania operacji na bazach danych jest ustandaryzowany interfejs: https://en.wikipedia.org/wiki/Java_Database_Connectivity .
zrobiłbym to tak, że miałbym takie moduły:

  • gui, który nie zależy od niczego
  • app, który tworzy kolekcję sterowników jdbc i wstrzykuje je do klasy z modułu gui i odpala apkę

et voila! :)

jeśli chodzi o bazy danych w javie to największym problemem nie jest wspólne api (bo przecież jest jdbc), a różnice w składni sql, bo co baza to inny i niekompatybilny wariant.

Oraz, chcesz dodawać nowe funkcje do postgres-driver.jar, tak żeby przeglądarka UI nie musiała być rekompilowana. I to się da zrobić - gdzieś będziesz musiał złamać statyczne typowanie.

dodać funkcję mogę zawsze, ale bez przerabiania przeglądarki ta nie będzie jej używać, bo niby skąd? przez refleksję odpala te metody? skonkretyzuj przykład. podaj jakiś kod.

2
Riddle napisał(a):

No np piszesz przeglądarkę bazy danych, i chcesz dołączyć postgres-driver.jar, mysql-driver.jar i sqlite-driver.jar. Cała logika przeglądarki UI ma być w module przeglądarki. Nie chcesz przenosić logiki UI do driver'ów. Ale chcesz żeby UI mogło wołać niektóre funkcje z bazy danych, za pomocą driver'a. I chcesz móc napisać sobie nowy driver, np mongo-driver.jar jak Ci się będzie chciało, ale chcesz też żeby przeglądarka UI nie wymaga żadnych zmian. Oraz, chcesz dodawać nowe funkcje do postgres-driver.jar, tak żeby przeglądarka UI nie musiała być rekompilowana. I to się da zrobić - ale gdzieś będziesz musiał złamać statyczne typowanie.

O! I to jest dobrze wymyślony sztuczny problem. Języki, które się do czegoś kompiluje będą tu miały pod górkę, a języki w których uruchamia się "źródła" dużo łatwiej. Aczkolwiek nie wiem czy to dużo wnosi do oryginalnej tezy, że statyczne typowanie jest problemem w dependency inversion.

Btw. uważam, że java już od dawna powinna mieć zależności od źródeł, a nie od jarów - to upraszcza trochę migrację na nowe wersje języka (obecnie mamy już jary z wieloma wersjami kodu kompilowanymi pod różne targety).

0
Wibowit napisał(a):

przykładowy podział na moduły:

  • common, który od niczego nie zależy
  • persistence, zależący od common
  • kafka, zależący od persistence (i zależności przechodnich)
  • rest api, zależący od persistence (i zależności przechodnich)
  • app (który spina resztę, tzn. wstrzykuje co trzeba i odpala aplikację), zależący od wszystkich innych modułów

nie ma żadnego wstrzykiwania opartego o stringi i ifologię na stringach.

Czyli zmiana w jakimś pluginie wymaga rekompilacji "app", jak rozumiem?

Bo jeśli tak to się zgadzamy.

Wprowadzając zmianę w pluginie, musisz zrekompilować jakieś miejsce w głównej aplikacji. I tym miejscem jest "app". To co zrobiłeś, to po prostu oznaczyłeś miejsce gdzie nie da się osiągnąć DI specjalną nazwą "app". To jest dokładnie to co chcę wykazać od początku w tym poście - że jeśli masz statyczne typowanie, to gdzieś będzie miejsce gdzie DI jest złamane. W tym wypadku tym miejscem jest app.

@Wibowit Nie mówię że to jest źle. To jest po prostu nieunikniona konsekwencja statycznego typowania i DI. Nie można po prostu ich mieć w 100% zawsze wszędzie obu.

Riddle napisał(a):
Riddle napisał(a):

To teraz pokaż jak wygląda metoda run() w server.jar? Bo w moim przykładzie run() ma być w server.jar. Jak przeniesiesz run() z servera do klienta, to tak na prawdę ignorujesz mój przykład, na rzecz innego przykładu w którym da się zastosować static typing i DI.

Powtarzam: są przypadki gdzie da się użyć DI i statycznego typowania razem. Wiem to. Ale to co napisałem to to - że są też przypadki gdzie się nie da - gdzie czasem statyczne typowanie zmusi nas do złamania DI, a chęć użycia DI zmusi nas do złamania typów - i podałem przykład takiego programu, gdzie run() jest w serverze (bo np jest współdzielona pomiędzy innymi pluginami które mają mieć tą samą logikę, bo mamy np client1.jar, client2.jar, client3.jar - i nie chcemy przenosić run() do każdego z nich).

pokaż jakieś konkretne przykłady, gdzie widać sens takiego podejścia.

No np piszesz przeglądarkę bazy danych, i chcesz dołączyć postgres-driver.jar, mysql-driver.jar i sqlite-driver.jar. Cała logika przeglądarki UI ma być w module przeglądarki. Nie chcesz przenosić logiki UI do driver'ów. Ale chcesz żeby UI mogło wołać niektóre funkcje z bazy danych, za pomocą driver'a. I chcesz móc napisać sobie nowy driver, np mongo-driver.jar jak Ci się będzie chciało, ale chcesz też żeby przeglądarka UI nie wymaga żadnych zmian. Oraz, chcesz dodawać nowe funkcje do postgres-driver.jar, tak żeby przeglądarka UI nie musiała być rekompilowana. I to się da zrobić - gdzieś będziesz musiał złamać statyczne typowanie.

do przeprowadzania operacji na bazach danych jest ustandaryzowany interfejs: https://en.wikipedia.org/wiki/Java_Database_Connectivity .
zrobiłbym to tak, że miałbym takie moduły:

  • gui, który nie zależy od niczego
  • app, który tworzy kolekcję sterowników jdbc i wstrzykuje je do klasy z modułu gui i odpala apkę

et voila! :)

To miałem na myśli mówiąc, że jak naprawisz coś w jednym miejscu, to popsujesz w innym.

Takie poleganie na interfejsie JDBC, to jest to samo ja zrobiłem w trzecim poście, jak wyniosłem abstractions.jar. Po prostu przenosisz swoje typy ze swojej aplikacji do "zewnętrznego bytu" jakim jest ten standard, i on ma dokładnie te same cechy (wady i zalety) co typy w mojej aplikacji - czyli dodanie czegoś do tego standardu, wiąże się ze zmianom klientów, a więc łamie DI.

Zauważ - wszystko czego nie da się zrobić z moim FruitShop w pierwszym przykładzie, nie da się też zrobić JDBC, i vice versa.


Możesz teraz argumentować że "standard to standard, nikt tam nie będzie dodawał metod". I to jest prawda. To jest po prostu jeden ze sposobów radzenia sobie z niemożliwością dodania DI do silnie typowanych języków. Masz silne typowanie - ergo nie masz DI - ergo robisz standard JDBC - ergo ludzie nie chcą dodawać do niego nowych metod - ergo Twoja aplikacja nie musi się rekompilować - solved.

Taki standard ma dokładnie te same problemy o których pisałem w moim przykładzie client.jar i server.jar - nie można dodać do niego nowych funkcji, bez zmiany jego użytkowników. To jest tożsamy przykład (z tą różnicą że nazwanie tego "standardem" sprawia ze ludzie są bardziej okej z tym).

Tylko widzisz, takie JDBC nie specjalnie coś zmienia. Bo ja w swoim module aplikacji też mogę sobie oznaczyć jakieś miejsce "proszę go nie zmieniać" (i go nazwać app, albo jdbc, albo contracts, albo jakkolwiek chcę), i tak długo jak się umówię z pluginami że ich nikt nie zmienia to jest spoko. Ale jak ktoś chce je zmienić, to albo osiągnę dependency inversion przez dynamiczne typowanie, albo będę sam musiał zmienić to miejsce które obiecałem że nie zmienię, jeśli plugin ma się zmienić; albo nie pozwolę pluginowi dodać zmiany.

Wibowit napisał(a):

dodać funkcję mogę zawsze, ale bez przerabiania przeglądarki ta nie będzie jej używać, bo niby skąd? przez refleksję odpala te metody?

Odpalenie metody przez refleksję byłoby tożsame z dynamicznym typowaniem, bo nie dałoby się sprawdzić typu w compile-time.

jarekr000000 napisał(a):

O! I to jest dobrze wymyślony sztuczny problem. Języki, które się do czegoś kompiluje będą tu miały pod górkę, a języki w których uruchamia się "źródła" dużo łatwiej. Aczkolwiek nie wiem czy to dużo wnosi do oryginalnej tezy, że statyczne typowanie jest problemem w dependency inversion.

Języki uruchamiane ze "źródła", czyli takie jak np Python, Ruby, PHP? Które często są dynamicznie typowane? No fakt, wtedy nie ma tego problemu. Co właściwie chciałem wykazać od początku.

1
Riddle napisał(a):

Języki uruchamiane ze "źródła", czyli takie jak np Python, Ruby, PHP? Które często są dynamicznie typowane? No fakt, wtedy nie ma tego problemu. Co właściwie chciałem wykazać od początku.

Nie, wykazałeś jedynie, że języków odpalanych ze źródła nie trzeba rekompilować (faktycznie, nie wpadlibyśmy na to).
Ale nic to nie wnosi do dependency inversion.

0
jarekr000000 napisał(a):
Riddle napisał(a):

Języki uruchamiane ze "źródła", czyli takie jak np Python, Ruby, PHP? Które często są dynamicznie typowane? No fakt, wtedy nie ma tego problemu. Co właściwie chciałem wykazać od początku.

Nie, wykazałeś jedynie, że języków odpalanych ze źródła nie trzeba rekompilować (faktycznie, nie wpadlibyśmy na to).

No, tylko że jeśli nie trzeba ich kompilować, to jakby z definicji nie mogą mieć statycznego typowania - bo i kiedy statyczne sprawdzenie typów miałoby się odbyć?

Sprawdzenie typów musiałoby się odbyć w momencie uruchomienia, czyli musiałoby być dynamicznym typowaniem.

jarekr000000 napisał(a):

Ale nic to nie wnosi do dependency inversion.

Tak, bo dynamicznie typowane języki są w 100% zgodne z Dependency Inversion, QED.

1
Riddle napisał(a):

No, tylko że jeśli nie trzeba ich kompilować, to jakby z definicji nie mogą mieć statycznego typowania - bo i kiedy statyczne sprawdzenie typów miałoby się odbyć?

Sprawdzenie typów musiałoby się odbyć w momencie uruchomienia, czyli musiałoby być dynamicznym typowaniem.

Statyczne typowanie odbywa się momencie kompilacji. Nie ma problemu z rekompilowaniem modułów w aplikacji - to nie powoduje, że "nie działa DI". W przeciwnym razie w językach odpalanych ze źródeł nie działa jeszcze bardziej, bo w tej sytuacji to w zasadzie kompilujesz całość przy każdym uruchomieniu.

0
jarekr000000 napisał(a):

Statyczne typowanie odbywa się momencie kompilacji. Nie ma problemu z rekompilowaniem modułów w aplikacji - to nie powoduje, że "nie działa DI". W przeciwnym razie w językach odpalanych ze źródeł nie działa jeszcze bardziej, bo w tej sytuacji to w zasadzie kompilujesz całość przy każdym uruchomieniu.

🤔

Dobrze, to pozwól że ujmę inaczej to co powiedziałem:

  • Jeśli kompilujesz program i sprawdzasz typy PRZED uruchomieniem, to gdzieś nie będzie się dało użyć DI (również znane jako statyczne typowanie)
  • Jeśli zaś "kompilujesz program przy każdym uruchomieniu" jak to nazwałeś (i wtedy też sprawdzasz typy), to da się użyć DI w 100% (również znane jako dynamiczne typowanie)

Nie mylisz czasem statycznego typowania z silnym typowaniem?

1
Riddle napisał(a):
KamilAdam napisał(a):

Albo inaczej, są przypadki, gdzie żeby osiągnąć DI, trzeba złamać statyczne typowanie

😮 mozesz rozwinac, @Riddle ?

No np jak masz dwa moduły, w którym pierwszy ma abstrakcyjna fabrykę, a drugi z niej korzysta. Wtedy ten drugi jakoś musi "powiedzieć" pierwszej jakiego typu obiekt ma dostać. I albo przekażesz stringa (łamiąc statyczne typowanie), albo dodasz enuma łamiąc dependency inversion (bo takiego enuma nie da się "odwrócić" w DI).

Mówiąc inaczej, jak masz poprawnie zrobione Dependency Inversion, to zawsze się znajdzie przypadek, którego się nie da typować w compile-time (jedynie w runtime).

Jeszcze są generyki przecież. Czyli masz fabrykę, która ma zadeklarowane, że przyjmuje generyczny argument oznaczający typ (nie dowolny, ale który by implementował jakiś konkretny interfejs). Czyli fabryka mogłaby stworzyć dowolny obiekt, ale jej customizacja by się odbywała w czasie pisania/kompilacji, za pośrednictwem generyków.

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