Czy to jest poprawne zastosowanie Dependency Injection?

Hej,

W książkach jako argument metody pojawia się zawsze Obiekt.new z parametrami
Natomiast u mnie metoda Trial::check_activity nie zna wartości id, name, date itd. więc Mailer::new mogę utworzyć dopiero w metodzie Trial::send_mail

Moje pytanie: czy to jest poprawny sposób czy sknociłem?

class Trial
  def check_activity
    if activity > 100
      send_mail(Mailer)
    end
  end
 
  def send_mail(mail)
    # cośtam cośtam, tworzę argumenty dla mail::new
    mail.new(id, name, date, total_activity)
  end
end
 
class Mailer
  def initialize(id, name, date, total_activity)
    # blabla
  end
end

Nikt nie wie? :frowning:

Nie jestem eksperę ale wydaje mi się że nie. Bo tu nie masz dependency injection. Po prostu tworzysz nowy obiekt typu Mailer. Nie “wstrzykujesz” do obiektu typu Trial już istniejącego obiektu którego instancja (wraz z istniejącymi właściwościami tej konkretnej instancji) może być np. współdzielona pomiędzy innymi obiektami tylko tworzysz zupełnie nową instancję.

Dependency Injection mogło by wyglądać np. tak:

class Config
  def initialize(a, b, c)
  @a = a
  @b = b
  @c = c

  def do_something
    ....
  end 

  def do_something_else
    ....
  end 
end

class A
  def initialize
    ....
  end

  def set_config config
    # setting an INSTANCE of config with
    @config = config
  end

  def do_something
    # retuns '1' + 'a', where 'a' is kept in config and is shared beetween between other classes
    x = 1 + @config.a
    a++
    return x
  end
end

class B 
  def initialize
    ....
  end

  def set_config config
    # setting an INSTANCE of config with
    @config = config
  end

  def do_something_else
    # retuns '1' + 'a', where 'a' is kept in config and is shared beetween between other classes
    x = 1 + @config.a
    a++
    return x
  end
end

class C
  def initialize
    @config = new Config 1 2 3
    object_a = new A
    object_b = new B
    # injecting config to objects, so they can share all the properties of config
    object_a.set_config @config
    object_b.set_config @config
    # line below will return 2
    object_a.do_something
    # line below will return 3, because we have incremented a that's in shared config in line above
    object_b.do_something_else
  end
end

Tak jak pisałem nie jestem ekspertem, ale nie znalazłem zastosowania dla Dependency Injecton w prostych aplikacjach webowych w których. Bo są one praktycznie bezstanowe, to znaczy stan aplikacji pomiędzy poszczególnymi wywołaniami danej strony jest trzymany w bazie danych, albo w ciasteczku, a nie bezpośrednio w pamięci komputera. Aplikacja startuje, obsługuje żądanie i kończy swoją pracę, następne żądanie jest już obsługiwane przez na nowo wystartowaną aplikację (w skrócie)

Dependency Injection przydaje się w aplikacjach desktopowych. Jeżeli masz np. okno w którym coś wyświetlasz, to nie będziesz za każdym razem na nowo tworzył okna przez “new Okno” bo albo byś miał ich tysiące, albo byś musiał zamykać okno i dopiero nowe tworzyć, tylko będziesz przekazywał instancję okna do poszczególnych obiektów które mają akurat coś w tym oknie narysować. To tak trochę chaotycznie i skrótowo, ale może Ci coś pomoże :wink:

Ps. Co do ostatniego akapitu a propos desktopu, wyobraź sobie że masz grę 2d. W głównej pętli możesz albo pobierać od wszystkich postaci na ekranie ich pozycję, potem pobierać informację jaki mają obrazek, sprawdzić czy się mogą ruszyć tam gdzie chcą (czy też nie bo tam jest np. ściana) potem te informację zebrać do kupy i wyświetlić na “Ekranie”. Wtedy będziesz miał wiele liniijek kodu w swojej głównej klasie, albo możesz wywołać metodę każdej z postaci która wszystko posprawdza, ruszy się i narysuje swój obrazek na 'Ekranie" (czyli jednej instancji tego ekranu czy okna).

PPS: Tu chyba najlepiej jest opisane co to w ogóle jest Dependency Injection :slight_smile: http://stackoverflow.com/a/140655/3373872 Sam używałem od dawna Dependency Injection, ale dopiero całkiem nie dawno się dowiedziałem że to co robię tak się właśnie nazywa :slight_smile:

Moim skromymn zdaniem, ani jedno ani drugie rozwiązanie nie ma za wiele wspólnego z DI. (isnieje nie zerowa szansa, że czegoś nie zrozumiałem w waszych rozwiązaniach)

Co to jest DI:

Tu jest przykład możliwego wykorzystania w ruby:

Pytanie czy potrzebujesz tego patternu w ruby ?

Sorry, ale polska wikipedia strasznie kuleje. Naprawdę techniczne artykuły są o klasę lepsze po angielsku http://en.wikipedia.org/wiki/Dependency_injection A tu rewelacyjny artykuł o którym pisałem wcześniej w “ps”: http://www.jamesshore.com/Blog/Dependency-Injection-Demystified.html

To jest tak banalnie proste, a tyle fachowego bełkotu jest w sieci, że aż głowa boli. Spróbuję to bardzo prosto napisać.
Jeżeli masz jakąś klasę A której działanie zależy od innych klas, to możesz albo sobie stworzyć w klasie A instancję tych klas od których zależy, albo przekazać już istniejącą instancję przez konstruktor albo jakąś metodę - settera*. I to jest naprawdę wszystko. Dlaczego warto stosować DI? Bo dzięki temu możemy się pozbyć zmiennych globalnych, czy porąbanych singletonów. Dlaczego lepiej stosować DI zamiast za każdym razem tworzyć nowe instancje? Bo to zmniejsza ilość potrzebnego do napisania kodu a do tego szalenie ułatwia testowanie, bo wtedy do testowanej klasy/metody przekazujesz tak spreparowany inny obiekt żeby testował ale nie robił żadnych innych niepotrzebnych przy danym teście rzeczy***.

Tak jak pisałem w większości przypadków DI w RoR przy prostych aplikacjach nic nam za bardzo nie daje, bo przy tak małej ilości kodu i trybie pracy (uruchom aplikację/wątek, obsłuż jedno żądanie, zamknij wątek/aplikację) nie warto sobie DI zaprzątać głowy, z drugiej strony używanie DI nic nas nie kosztuje i po pewnym czasie jest to naturalna droga tworzenia obiektów.

@maringan Proszę nie bierz za bardzo do Siebie, bo to raczej o ogół ludzi raczej mi chodzi. Próba wmawiania że DI to coś więcej jest tak samo bez sensu jak twierdzenie że język X nie jest językiem w pełni obiektowym bo nie ma np. interfejsów tak jak Java czy C#, albo że język Y nie jest językiem w pełni obiektowym bo nie obsługuje dziedziczenia po wielu klasach tak jak np. Python czy C++. To są tak samo głupie dyskusje jak głupie jest uczenie się wzorców projektowych na such na podstawie przykładów.


*można jeszcze przez interfejs itd. ale to są szczegóły implementacji które dla idei nie maja znaczenia.
** zakładając że publiczne metody tak samo się nazywają, przyjmują te same argumenty i zwracają to samo
***wyświetlał coś na ekranie, wysyłał coś do bazy, pobierał skądś dane itd. itp.

Dzięki bardzo za pomoc i przydatne linki.
DI chcę się dobrze nauczyć żeby tworzyć łatwo testowalny kod
Zmodyfikowałem nieco swój kod zgodnie z tym co mi poradziliście. Wrzucam takie coś czym sobie testuję pomysły.

W tym przypadku metoda ::initialize w klasie Mailer znika. Teraz argumenty do Mailer::send muszę przekazać przy wywoływaniu tej konkretnej metody, nie ustawię ich przy tworzeniu obiektu.

Czy teraz jest już poprawnie?

class Trial
  
  def initialize(mailer)
    @mailer = mailer
  end
  
  def check_activity
    activity = 200
    
    if activity > 100
      send_mail
    end
  end
 
  def send_mail
    # blabla, tworzę argumenty dla Mailer::send
    @mailer.send("id", "name", "date", "total_activity")
  end
end
 
class Mailer
  def send(id, name, date, total_activity)
    puts id, name, date, total_activity
  end
  
    
end


trial = Trial.new(Mailer.new)
trial.check_activity

@sztosz
Hej,
jestem daleki od wmawiana tego, że DI jest fajny, adresuje pewne problemy, które w ruby są egzotyką, problem jaki miałem z wcześniejszymi rozwiązaniami był taki, że nie było wstrzykiwania tym czym jest właściwie DI, była próba stworzenia kontenera, który adresuje np. problem singletonów, ale tych javowowych nie rubiowych. Co do polskiej wiki tak jest mało jest biedniejsza od angielskiej tylko czego brakuje w polskiej wiki biorąc pod uwagę potrzeby tego problemu ? IMHO spełnia swoje zadanie. Nie biorę tego do siebie, jeśli nauczę się czegoś nowego będę tylko wdzięczny :slight_smile:

DI w ruby:

before DI

class Tadam
   def initialize
     @x = X.new
   end
end

after DI

class Tadam
   def initialize(x = X.new)
      @x = x
   end
end

W pewnym sensie DI w ruby to po prostu :attr_accessor / :attr_writer - ustawianie ‘pol’ obiektu przez obiekt, ktory go wykonuje / uzywa.

Przy czym pojawia sie pytanie ‘kto’ definiuje ‘co’ ma byc wstrzykniete ‘gdzie’. W podejsciu Javowo/Springowym odpowiada za to albo konfiguracja w plikach XML, albo annotacje w klasach. Wtedy kod moze zalozyc, ze dane ‘property’ bedzie ustawione z automatu zgodnie z konfiguracja.

Inne pytanie to takie, co powinien zrobic obiekt, jesli jego pole/pola NIE zostaly ustawione. Mozna uzyc konstruktora do utworzenia wartosci domyslnych, a mozna po prostu rzucic wyjatek w trakcie dzialania (obiekt nie do konca zainicjalizowany)

Kodu, tylko tego i aż tego. Może to to kwestia mnie, ale nie umiem czytać polskiej dokumentacji :wink: a jak mam z czymś problem to zaczynam od szukania i czytania kodu. Nie ważne że ja coś piszę w Pythonie albo C a przykład znajdę np. w Javie. Zawsze czytelny kawałek kodu lepiej opisze program niż “słowny” opis po angielsku.

\troll mode on
http://david.heinemeierhansson.com/2012/dependency-injection-is-not-a-virtue.html

sorry, nie mogłem się powstrzymać :smiley:

@Avat, przeczytaj ten wcześniej podlinkowany post Solnica i nie przejmuj się niczym innym, na razie powinien on Ci wystarczyć, a i tak nauczysz się tego wszystkiego na błędach. ;>

W tym przypadku metoda ::initialize w klasie Mailer znika. Teraz argumenty do Mailer::send muszę przekazać przy wywoływaniu tej konkretnej metody, nie ustawię ich przy tworzeniu obiektu.

Czy teraz jest już poprawnie?

Jest zdecydowanie lepiej. Może mógłbyś utworzyć osobną klasę (prosty Struct), która będzie trzymała wszystkie parametry potrzebne do wywołania Mailer#send i przekazać ją do metody check_activity?

Obejrzyj sobie jeszcze talk “Nothing is Something” Sandi Metz. Cała prezentacja jest super, ale zwróć szczególną uwagę na ten fragment, w którym Sandi buduje klasę House, a później próbuje ją rozszerzyć.