Po co używać wzorca publikowania / subskrypcji (w JS / jQuery)?

103

Tak więc kolega zapoznał mnie ze wzorcem publikowania / subskrypcji (w JS / jQuery), ale trudno mi zrozumieć, dlaczego można użyć tego wzorca „normalnego” JavaScript / jQuery.

Na przykład wcześniej miałem następujący kod ...

$container.on('click', '.remove_order', function(event) {
    event.preventDefault();
    var orders = $(this).parents('form:first').find('div.order');
    if (orders.length > 2) {
        orders.last().remove();
    }
});

I zamiast tego mogłem zobaczyć zalety zrobienia tego, na przykład ...

removeOrder = function(orders) {
    if (orders.length > 2) {
        orders.last().remove();
    }
}

$container.on('click', '.remove_order', function(event) {
    event.preventDefault();
    removeOrder($(this).parents('form:first').find('div.order'));
});

Ponieważ wprowadza możliwość ponownego użycia removeOrder funkcjonalności dla różnych wydarzeń itp.

Ale dlaczego miałbyś zdecydować się na zaimplementowanie wzorca publikowania / subskrypcji i przechodzenie do następujących długości, jeśli robi to samo? (FYI, użyłem jQuery tiny pub / sub )

removeOrder = function(e, orders) {
    if (orders.length > 2) {
        orders.last().remove();
    }
}

$.subscribe('iquery/action/remove-order', removeOrder);

$container.on('click', '.remove_order', function(event) {
    event.preventDefault();
    $.publish('iquery/action/remove-order', $(this).parents('form:first').find('div.order'));
});

Na pewno czytałem o wzorze, ale po prostu nie mogę sobie wyobrazić, dlaczego miałoby to kiedykolwiek być konieczne. Samouczki, które widziałem, wyjaśniają, jak to zrobić zaimplementować ten wzorzec, obejmują tylko tak podstawowe przykłady, jak moje własne.

Wyobrażam sobie, że użyteczność pub / sub ujawniłaby się w bardziej złożonej aplikacji, ale nie mogę sobie takiej wyobrazić. Obawiam się, że zupełnie nie rozumiem; ale chciałbym wiedzieć, o co chodzi, jeśli taki istnieje!

Czy mógłbyś zwięźle wyjaśnić, dlaczego iw jakich sytuacjach ten wzór jest korzystny? Czy warto używać wzorca pub / sub we fragmentach kodu, takich jak powyższe przykłady?

Maccath
źródło

Odpowiedzi:

222

Chodzi o luźne powiązanie i pojedynczą odpowiedzialność, która idzie w parze z wzorcami MV * (MVC / MVP / MVVM) w JavaScript, które są bardzo nowoczesne w ciągu ostatnich kilku lat.

Luźne powiązanie to zasada zorientowana obiektowo, w której każdy komponent systemu zna swoją odpowiedzialność i nie dba o inne komponenty (lub przynajmniej stara się nie przejmować nimi tak bardzo, jak to możliwe). Luźne połączenie to dobra rzecz, ponieważ można łatwo ponownie wykorzystać różne moduły. Nie jesteś połączony z interfejsami innych modułów. Używając publikuj / subskrybuj, łączysz się tylko z interfejsem publikuj / subskrybuj, co nie jest wielkim problemem - tylko dwie metody. Więc jeśli zdecydujesz się ponownie użyć modułu w innym projekcie, możesz go po prostu skopiować i wkleić, a prawdopodobnie zadziała lub przynajmniej nie będziesz potrzebować dużo wysiłku, aby działał.

Mówiąc o luźnym sprzężeniu należy wspomnieć o rozdzieleniu obaw. Jeśli tworzysz aplikację przy użyciu wzorca architektonicznego MV *, zawsze masz Model (y) i Widok (y). Model jest biznesową częścią aplikacji. Możesz go ponownie wykorzystać w różnych aplikacjach, więc nie jest dobrym pomysłem łączenie go z widokiem pojedynczej aplikacji, w której chcesz go wyświetlić, ponieważ zwykle w różnych aplikacjach masz różne widoki. Dlatego dobrym pomysłem jest użycie publikowania / subskrypcji do komunikacji Model-View. Gdy model się zmieni, publikuje zdarzenie, widok przechwytuje je i sam się aktualizuje. Nie masz żadnych kosztów związanych z publikacją / subskrypcją, pomaga to w oddzieleniu. W ten sam sposób możesz na przykład zachować logikę aplikacji w kontrolerze (MVVM, MVP to nie jest dokładnie kontroler) i utrzymywać widok tak prosty, jak to tylko możliwe. Kiedy Twój Widok się zmieni (lub na przykład użytkownik kliknie coś), po prostu opublikuje nowe zdarzenie, Kontroler przechwytuje je i decyduje, co zrobić. Jeśli znaszWzorzec MVC lub z MVVM w technologiach firmy Microsoft (WPF / Silverlight) można myśleć o publikacji / subskrypcji jak o wzorcu obserwatora . To podejście jest używane w frameworkach takich jak Backbone.js, Knockout.js (MVVM).

Oto przykład:

//Model
function Book(name, isbn) {
    this.name = name;
    this.isbn = isbn;
}

function BookCollection(books) {
    this.books = books;
}

BookCollection.prototype.addBook = function (book) {
    this.books.push(book);
    $.publish('book-added', book);
    return book;
}

BookCollection.prototype.removeBook = function (book) {
   var removed;
   if (typeof book === 'number') {
       removed = this.books.splice(book, 1);
   }
   for (var i = 0; i < this.books.length; i += 1) {
      if (this.books[i] === book) {
          removed = this.books.splice(i, 1);
      }
   }
   $.publish('book-removed', removed);
   return removed;
}

//View
var BookListView = (function () {

   function removeBook(book) {
      $('#' + book.isbn).remove();
   }

   function addBook(book) {
      $('#bookList').append('<div id="' + book.isbn + '">' + book.name + '</div>');
   }

   return {
      init: function () {
         $.subscribe('book-removed', removeBook);
         $.subscribe('book-aded', addBook);
      }
   }
}());

Inny przykład. Jeśli nie podoba ci się podejście MV *, możesz użyć czegoś nieco innego (istnieje skrzyżowanie między tym, które opiszę dalej, a ostatnim wymienionym). Po prostu podziel swoją aplikację na różne moduły. Na przykład spójrz na Twittera.

Moduły Twittera

Jeśli spojrzysz na interfejs, po prostu masz różne pola. Możesz myśleć o każdym pudełku jako o innym module. Na przykład możesz opublikować tweet. Ta akcja wymaga aktualizacji kilku modułów. Po pierwsze musi zaktualizować dane Twojego profilu (lewe górne pole), ale musi również zaktualizować Twoją oś czasu. Oczywiście możesz zachować odniesienia do obu modułów i aktualizować je osobno, używając ich publicznego interfejsu, ale łatwiej (i lepiej) jest po prostu opublikować wydarzenie. Ułatwi to modyfikację aplikacji ze względu na luźniejsze powiązania. Jeśli tworzysz nowy moduł, który zależy od nowych tweetów, możesz po prostu zapisać się na wydarzenie „publikuj-tweet” i obsłużyć. Takie podejście jest bardzo przydatne i może sprawić, że aplikacja będzie bardzo rozdzielona. Możesz bardzo łatwo ponownie wykorzystać swoje moduły.

Oto podstawowy przykład ostatniego podejścia (to nie jest oryginalny kod twittera, to tylko przykład mojego autorstwa):

var Twitter.Timeline = (function () {
   var tweets = [];
   function publishTweet(tweet) {
      tweets.push(tweet);
      //publishing the tweet
   };
   return {
      init: function () {
         $.subscribe('tweet-posted', function (data) {
             publishTweet(data);
         });
      }
   };
}());


var Twitter.TweetPoster = (function () {
   return {
       init: function () {
           $('#postTweet').bind('click', function () {
               var tweet = $('#tweetInput').val();
               $.publish('tweet-posted', tweet);
           });
       }
   };
}());

Dla tego podejścia jest doskonała przemowa Nicholasa Zakasa . Jeśli chodzi o podejście MV *, najlepsze znane mi artykuły i książki są publikowane przez Addy Osmani .

Wady: musisz uważać na nadmierne wykorzystanie opcji publikowania / subskrybowania. Jeśli masz setki wydarzeń, zarządzanie nimi wszystkimi może być bardzo skomplikowane. Możesz także mieć kolizje, jeśli nie używasz przestrzeni nazw (lub nie używasz jej we właściwy sposób). Zaawansowaną implementację Mediatora, która wygląda podobnie do publikacji / subskrypcji, można znaleźć tutaj https://github.com/ajacksified/Mediator.js . Ma przestrzeń nazw i funkcje takie jak „bulgotanie” zdarzeń, które oczywiście można przerwać. Inną wadą publikowania / subskrypcji jest trudne testowanie jednostkowe, może być trudne wyodrębnienie różnych funkcji w modułach i przetestowanie ich niezależnie.

Minko Gechev
źródło
3
Dziękuję, to ma sens. Znam wzorzec MVC, ponieważ używam go cały czas w PHP, ale nie myślałem o nim w kontekście programowania sterowanego zdarzeniami. :)
Maccath
2
Dzięki za ten opis. Naprawdę pomogło mi to ogarnąć koncepcję.
flybear
1
To doskonała odpowiedź. Nie mogłem się powstrzymać przed głosowaniem w tym głosowaniu :)
Naveed Butt
1
Świetne wyjaśnienie, wiele przykładów, dalsze sugestie dotyczące czytania. A ++.
Carson,
16

Głównym celem jest zmniejszenie sprzężenia między kodem. Jest to sposób myślenia w pewnym stopniu oparty na zdarzeniach, ale „zdarzenia” nie są związane z konkretnym obiektem.

Napiszę poniżej duży przykład w pseudokodzie, który wygląda trochę jak JavaScript.

Powiedzmy, że mamy klasę Radio i klasę przekaźnika:

class Relay {
    function RelaySignal(signal) {
        //do something we don't care about right now
    }
}

class Radio {
    function ReceiveSignal(signal) {
        //how do I send this signal to other relays?
    }
}

Zawsze, gdy radio odbiera sygnał, chcemy, aby pewna liczba przekaźników w jakiś sposób przekazywała wiadomość. Liczba i typy przekaźników mogą się różnić. Moglibyśmy to zrobić tak:

class Radio {
    var relayList = [];

    function AddRelay(relay) {
        relayList.add(relay);
    }

    function ReceiveSignal(signal) {
        for(relay in relayList) {
            relay.Relay(signal);
        }
    }

}

To działa dobrze. Ale teraz wyobraźmy sobie, że chcemy, aby inny komponent również brał udział w sygnałach odbieranych przez klasę Radio, a mianowicie Głośniki:

(przepraszam, jeśli analogie nie są na najwyższym poziomie ...)

class Speakers {
    function PlaySignal(signal) {
        //do something with the signal to create sounds
    }
}

Możemy powtórzyć wzór:

class Radio {
    var relayList = [];
    var speakerList = [];

    function AddRelay(relay) {
        relayList.add(relay);
    }

    function AddSpeaker(speaker) {
        speakerList.add(speaker)
    }

    function ReceiveSignal(signal) {

        for(relay in relayList) {
            relay.Relay(signal);
        }

        for(speaker in speakerList) {
            speaker.PlaySignal(signal);
        }

    }

}

Moglibyśmy to jeszcze ulepszyć, tworząc interfejs, taki jak „SignalListener”, dzięki czemu potrzebujemy tylko jednej listy w klasie Radio i zawsze możemy wywołać tę samą funkcję na jakimkolwiek obiekcie, który mamy, który chce słuchać sygnału. Ale to nadal tworzy sprzężenie między dowolnym interfejsem / klasą bazową / etc, na który zdecydujemy się, a klasą Radio. Zasadniczo za każdym razem, gdy zmieniasz jedną z klas Radio, Signal lub Relay, musisz pomyśleć o tym, jak może to wpłynąć na pozostałe dwie klasy.

Teraz spróbujmy czegoś innego. Utwórzmy czwartą klasę o nazwie RadioMast:

class RadioMast {

    var receivers = [];

    //this is the "subscribe"
    function RegisterReceivers(signaltype, receiverMethod) {
        //if no list for this type of signal exits, create it
        if(receivers[signaltype] == null) {
            receivers[signaltype] = [];
        }
        //add a subscriber to this signal type
        receivers[signaltype].add(receiverMethod);
    }

    //this is the "publish"
    function Broadcast(signaltype, signal) {
        //loop through all receivers for this type of signal
        //and call them with the signal
        for(receiverMethod in receivers[signaltype]) {
            receiverMethod(signal);
        }
    }
}

Teraz mamy wzorzec , którego jesteśmy świadomi i możemy go użyć dla dowolnej liczby i typów klas, o ile:

  • są świadomi RadioMast (klasa obsługująca wszystkie przekazywane wiadomości)
  • znają sygnaturę metody wysyłania / odbierania wiadomości

Więc zmieniamy klasę Radio na ostateczną, prostą formę:

class Radio {
    function ReceiveSignal(signal) {
        RadioMast.Broadcast("specialradiosignal", signal);
    }
}

I dodajemy głośniki i przekaźnik do listy odbiorników RadioMast dla tego typu sygnału:

RadioMast.RegisterReceivers("specialradiosignal", speakers.PlaySignal);
RadioMast.RegisterReceivers("specialradiosignal", relay.RelaySignal);

Teraz klasa Speakers and Relay nie ma żadnej wiedzy na temat czegokolwiek poza tym, że ma metodę, która może odebrać sygnał, a klasa Radio, będąc wydawcą, jest świadoma, że ​​RadioMast publikuje sygnały. To jest punkt używania systemu przekazywania wiadomości, takiego jak publikuj / subskrybuj.

Anders Arpi
źródło
Naprawdę wspaniale jest mieć konkretny przykład pokazujący, jak implementacja wzorca pub / sub może być lepsza niż używanie „normalnych” metod! Dziękuję Ci!
Maccath
1
Nie ma za co! Osobiście często stwierdzam, że mój mózg nie „klika”, jeśli chodzi o nowe wzorce / metodologie, dopóki nie zdaję sobie sprawy z rzeczywistego problemu, który rozwiązuje za mnie. Wzorzec sub / pub jest świetny w przypadku architektur, które są ściśle powiązane koncepcyjnie, ale nadal chcemy, aby były jak najbardziej oddzielne. Wyobraźmy sobie grę, w której masz setki obiektów, które wszystkie mają reagować na rzeczy dzieje się wokół nich, na przykład, a obiekty te mogą być wszystko: gracz, kula, drzewo, geometria, gui itp itd
Anders Arpi
3
JavaScript nie ma classsłowa kluczowego. Prosimy o podkreślenie tego faktu np. klasyfikując swój kod jako pseudokod.
Rob W
Właściwie w ES6 jest słowo kluczowe class.
Minko Gechev
5

Inne odpowiedzi wykonały świetną robotę, pokazując, jak działa wzór. Chciałem odpowiedzieć na sugerowane pytanie „ co jest nie tak ze starym sposobem? ”, Ponieważ ostatnio pracowałem z tym wzorcem i stwierdziłem, że wiąże się to ze zmianą w moim myśleniu.

Wyobraź sobie, że subskrybowaliśmy biuletyn ekonomiczny. Biuletyn publikuje nagłówek: „ Obniż Dow Jones o 200 punktów ”. To byłaby dziwna i nieco nieodpowiedzialna wiadomość. Jeśli jednak opublikował: „ Enron złożył dziś rano wniosek o ochronę przed bankructwem na mocy rozdziału 11 ”, to jest to bardziej przydatna wiadomość. Zauważ, że wiadomość może spowodować spadek Dow Jones o 200 punktów, ale to już inna sprawa.

Istnieje różnica między wysłaniem polecenia a powiadomieniem o czymś, co właśnie się wydarzyło. Mając to na uwadze, weź oryginalną wersję wzorca pub / sub, na razie ignorując procedurę obsługi:

$.subscribe('iquery/action/remove-order', removeOrder);

$container.on('click', '.remove_order', function(event) {
    event.preventDefault();
    $.publish('iquery/action/remove-order', $(this).parents('form:first').find('div.order'));
});

Istnieje już tutaj domniemane silne powiązanie między działaniem użytkownika (kliknięciem) a odpowiedzią systemu (usunięcie zlecenia). Skutecznie w twoim przykładzie, akcja jest wydaniem polecenia. Rozważ tę wersję:

$.subscribe('iquery/action/remove-order-requested', handleRemoveOrderRequest);

$container.on('click', '.remove_order', function(event) {
    event.preventDefault();
    $.publish('iquery/action/remove-order-requested', $(this).parents('form:first').find('div.order'));
});

Teraz opiekun reaguje na coś interesującego, co się wydarzyło, ale nie ma obowiązku usunięcia zamówienia. W rzeczywistości program obsługi może robić różne rzeczy, które nie są bezpośrednio związane z usunięciem zamówienia, ale nadal mogą być związane z akcją wywołującą. Na przykład:

handleRemoveOrderRequest = function(e, orders) {
    logAction(e, "remove order requested");
    if( !isUserLoggedIn()) {
        adviseUser("You need to be logged in to remove orders");
    } else if (isOkToRemoveOrders(orders)) {
        orders.last().remove();
        adviseUser("Your last order has been removed");
        logAction(e, "order removed OK");
    } else {
        adviseUser("Your order was not removed");
        logAction(e, "order not removed");
    }
    remindUserToFloss();
    increaseProgrammerBrowniePoints();
    //etc...
}

Rozróżnienie między poleceniem a powiadomieniem jest przydatne do zrobienia tego wzorca, IMO.

Trevedhek
źródło
gdyby ostatnie 2 funkcje ( remindUserToFloss& increaseProgrammerBrowniePoints) znajdowały się w oddzielnych modułach, czy opublikowałbyś 2 zdarzenia, jedno po drugim, w tym miejscu, handleRemoveOrderRequestczy też flossModuleopublikowałbyś wydarzenie w browniePointsmodule po zakończeniu remindUserToFloss()?
Bryan P
4

Aby nie trzeba było na stałe kodować wywołań metod / funkcji, po prostu publikujesz zdarzenie bez dbania o to, kto słucha. To uniezależnia wydawcę od subskrybenta, zmniejszając zależność (lub łączenie, jakkolwiek wolisz) między 2 różnymi częściami aplikacji.

Oto kilka wad sprzężenia, o których wspomina wikipedia

Ściśle powiązane systemy mają zwykle następujące cechy rozwojowe, które często są postrzegane jako wady:

  1. Zmiana w jednym module zwykle wymusza efekt falowania zmian w innych modułach.
  2. Montaż modułów może wymagać więcej wysiłku i / lub czasu ze względu na zwiększoną zależność między modułami.
  3. Ponowne użycie i / lub testowanie określonego modułu może być trudniejsze, ponieważ muszą być uwzględnione moduły zależne.

Rozważmy coś w rodzaju obiektu hermetyzującego dane biznesowe. Ma zakodowane wywołanie metody, która aktualizuje stronę po ustawieniu wieku:

var person = {
    name: "John",
    age: 23,

    setAge: function( age ) {
        this.age = age;
        showAge( age );
    }
};

//Different module

function showAge( age ) {
    $("#age").text( age );
}

Teraz nie mogę przetestować obiektu osoby bez uwzględnienia showAgefunkcji. Ponadto, jeśli chcę pokazać wiek również w innym module GUI, muszę zakodować wywołanie tej metody .setAge, a teraz istnieją zależności dla 2 niepowiązanych modułów w obiekcie osoby. Jest to również trudne do utrzymania, gdy widzisz nawiązywane połączenia i nie ma ich nawet w tym samym pliku.

Zauważ, że w tym samym module możesz oczywiście mieć bezpośrednie wywołania metod. Jednak dane biznesowe i powierzchowne zachowanie GUI nie powinny znajdować się w tym samym module według jakichkolwiek rozsądnych standardów.

Esailija
źródło
Nie rozumiem tutaj pojęcia „zależności”; gdzie jest zależność w moim drugim przykładzie, a gdzie brakuje w moim trzecim? Nie widzę żadnej praktycznej różnicy między moim drugim a trzecim fragmentem - po prostu wydaje się, że dodaje nową „warstwę” między funkcją a zdarzeniem bez prawdziwego powodu. Prawdopodobnie jestem ślepy, ale myślę, że potrzebuję więcej wskazówek. :(
Maccath
1
Czy możesz podać przykładowy przypadek użycia, w którym publikuj / subskrybuj byłoby bardziej odpowiednie niż zwykłe tworzenie funkcji, która wykonuje to samo?
Jeffrey Sweeney
@Maccath Mówiąc najprościej: w trzecim przykładzie nie wiesz lub nie musisz wiedzieć, że w removeOrderogóle istnieje, więc nie możesz być od niego zależny. W drugim przykładzie musisz wiedzieć.
Esailija,
Chociaż nadal uważam, że istnieją lepsze sposoby na to, co tutaj opisałeś, jestem przynajmniej przekonany, że ta metodologia ma cel, zwłaszcza w środowiskach z wieloma innymi programistami. +1
Jeffrey Sweeney
1
@Esailija - Dziękuję, myślę, że trochę lepiej rozumiem. Więc ... gdybym całkowicie usunął subskrybenta, nie byłoby błędu ani nic, po prostu nic by nie zrobił? Czy powiedziałbyś, że może to być przydatne w przypadku, gdy chcesz wykonać jakąś czynność, ale niekoniecznie wiesz, która funkcja jest najbardziej odpowiednia w momencie publikacji, ale subskrybent może się zmienić w zależności od innych czynników?
Maccath
1

Implementacja PubSub jest często spotykana tam, gdzie jest -

  1. Istnieje taka implementacja portletu, w której istnieje wiele portletów, które komunikują się za pomocą magistrali zdarzeń. Pomaga to w tworzeniu w architekturze aync.
  2. W systemie zniszczonym przez ścisłe sprzężenie pubsub jest mechanizmem, który pomaga w komunikacji między różnymi modułami.

Przykładowy kod -

var pubSub = {};
(function(q) {

  var messages = [];

  q.subscribe = function(message, fn) {
    if (!messages[message]) {
      messages[message] = [];
    }
    messages[message].push(fn);
  }

  q.publish = function(message) {
    /* fetch all the subscribers and execute*/
    if (!messages[message]) {
      return false;
    } else {
      for (var message in messages) {
        for (var idx = 0; idx < messages[message].length; idx++) {
          if (messages[message][idx])
            messages[message][idx]();
        }
      }
    }
  }
})(pubSub);

pubSub.subscribe("event-A", function() {
  console.log('this is A');
});

pubSub.subscribe("event-A", function() {
  console.log('booyeah A');
});

pubSub.publish("event-A"); //executes the methods.
user2756335
źródło
1

Artykuł „Wiele twarzy publikowania / subskrybowania” jest dobrą lekturą i jedną z rzeczy, na którą kładą nacisk, jest rozdzielenie w trzech „wymiarach”. Oto moje surowe podsumowanie, ale proszę również odwołać się do artykułu.

  1. Odsprzężenie przestrzeni. Strony wchodzące w interakcję nie muszą się znać. Wydawca nie wie, kto słucha, ilu słucha ani co robi z wydarzeniem. Abonenci nie wiedzą, kto produkuje te wydarzenia, ilu jest producentów itp.
  2. Oddzielenie czasu. Strony wchodzące w interakcję nie muszą być jednocześnie aktywne podczas interakcji. Na przykład subskrybent może zostać rozłączony, gdy wydawca publikuje pewne wydarzenia, ale może zareagować na to, gdy pojawi się online.
  3. Odsprzężenie synchronizacji.Wydawcy nie są blokowani podczas tworzenia wydarzeń, a subskrybenci mogą być asynchronicznie powiadamiani przez wywołania zwrotne o nadejściu zdarzenia, które subskrybują.
Peheje
źródło
0

Prosta odpowiedź Pierwotne pytanie dotyczyło prostej odpowiedzi. Oto moja próba.

JavaScript nie zapewnia żadnego mechanizmu dla obiektów kodu do tworzenia własnych zdarzeń. Potrzebujesz więc pewnego rodzaju mechanizmu zdarzeń. wzorzec publikowania / subskrypcji odpowiada na tę potrzebę, a wybór mechanizmu, który najlepiej odpowiada Twoim potrzebom, zależy od Ciebie.

Teraz widzimy potrzebę wzorca pub / sub, czy wolałbyś więc obsługiwać zdarzenia DOM inaczej niż w przypadku zdarzeń pub / sub? W celu zmniejszenia złożoności i innych koncepcji, takich jak oddzielenie problemów (SoC), możesz dostrzec korzyści płynące z jednolitości wszystkiego.

Tak więc, paradoksalnie, więcej kodu zapewnia lepszą separację problemów, co dobrze skaluje się nawet do bardzo złożonych stron internetowych.

Mam nadzieję, że ktoś uzna to za wystarczająco dobrą dyskusję bez wchodzenia w szczegóły.

Simon Miller
źródło