31 marca 2017

W ciemność

Ostatnio na naszym kanale Slackowym kolega polecił mi, żebym zmienił bibliotekę do unit testów na nowszą i częściej używaną. Zachęcał mnie do wybrania raczej biblioteki minitest. Rzeczywiście jest to biblioteka bardziej popularna i więcej może. Tylko ja z moją starą biblioteką test-unit nadal spełniam warunek numer 3 z przysięgi programisty:
3. Dostarczyć z każdą wersją aplikacji szybki, pewny i powtarzalny dowód, że każda część kodu działa tak jak powinna.
Jeżeli tak, to po co bić pianę? Jeżeli będę potrzebował większych możliwości w przyszłości rzeczywiście zmienię bibliotekę. Na razie postąpię według reguły KISS (nie komplikuj głuptasku).


Dopiszę kolejne wymaganie do naszej aplikacji. Klienci i Goście z IT mogą zasubskrybować Media. Mogą też wysłać swoją reklamę, która ukaże się cykliczne w medium proponowanym przez Media.

Do mojego pliku z testami dodałem nowy test, w którym oczekuję, że będę mógł dodać reklamę z telefonem do swojej firmy.

def test_if_it_possible_to_add_advertising                                                                                                                                                                                                 
    it_people = ItPeople.new                                                                                                                                                                                                                 
    media = Media.new                                                                                                                                                                                                                        
    media.add_advertising(it_people.telephone_numer);                                                                                                                                           
end

Zgodnie z drugim prawem TDD skoro mam błędy kompilacji ...

NoMethodError: undefined method `category' for #<Media:0x007fd3eefabaa8>

... muszę przerwać pisanie testu i dodaję kod produkcyjny. A więc teraz klasa ItPeople musi wyglądać tak:

class ItPeople

  attr_accessor :telephone_number

end

Przy czym attr_accessor dodaje zmienną do klasy z możliwością jej edycji i odczytu.

A klasa Media przybierze postać:


class Media

  attr_accessor :categories                                                                                                                                                                                                                  
                                                                                                                                                                                                                                             
  def add_advertising telephone_number                                                                                                                                                                                                       
                                                                                                                                                                                                                                             
  end                                                                                                                                                                                                                                        
                                                                                                                                                                                                                                             
end  

I tu muszę wspomnieć, że po nazwie metody, po spacji dajemy listę parametrów metody. Mogę dać nawiasy, ale nie muszę.

Teraz, ponieważ testy przechodzą przerywam pisanie kodu produkcyjnego. Ponieważ mam leciutką duplikację w testach w deklaracji obiektów zrobię refaktor.

Zamienię to:


require "test-unit"                                                                                                                                                                                                                          
require "./src/Client.rb"                                                                                                                                                                                                                    
require "./src/ItPeople.rb"                                                                                                                                                                                                                  
require "./src/Media.rb"                                                                                                                                                                                                                     
                                                                                                                                                                                                                                             
class FirstSmokeTestClass < Test::Unit::TestCase                                                                                                                                                                                             
                                                                                                                                                                                                                                             
  def test_if_this_test_framework_works                                                                                                                                                                                                      
                                                                                                                                                                                                                                             
    assert_equal(2, 1 + 1)                                                                                                                                                                                                                   
                                                                                                                                                                                                                                             
  end                                                                                                                                                                                                                                        
                                                                                                                                                                                                                                             
  def test_if_there_are_actors_of_that_tragedy                                                                                                                                                                                               
                                                                                                                                                                                                                                             
    client = Client.new                                                                                                                                                                                                                      
    it_people = ItPeople.new                                                                                                                                                                                                                 
    media = Media.new                                                                                                                                                                                                                        
                                                                                                                                                                                                                                             
    assert_not_nil(client);                                                                                                                                                                                                                  
    assert_not_nil(it_people);                                                                                                                                                                                                               
    assert_not_nil(media);                                                                                                                                                                                                                   
                                                                                                                                                                                                                                             
  end                                                                                                                                                                                                                                        
                                                                                                                                                                                                                                             
  def test_if_it_possible_to_add_advertising                                                                                                                                                                                                 
    it_people = ItPeople.new                                                                                                                                                                                                                 
    media = Media.new                                                                                                                                                                                                                        
    media.add_advertising(it_people.telephone_number);                                                                                                                                                                                       
  end                                                                                                                                                                                                                                        
                                                                                                                                                                                                                                             
                                                                                                                                                                                                                                             
end  


W to:


require "test-unit"                                                                                                                                                                                                                          
require "./src/Client.rb"                                                                                                                                                                                                                    
require "./src/ItPeople.rb"                                                                                                                                                                                                                  
require "./src/Media.rb"                                                                                                                                                                                                                     
                                                                                                                                                                                                                                             
class FirstSmokeTestClass < Test::Unit::TestCase                                                                                                                                                                                             
                                                                                                                                                                                                                                             
  def setup                                                                                                                                                                                                                                  
                                                                                                                                                                                                                                             
    @client = Client.new                                                                                                                                                                                                                     
    @it_people = ItPeople.new                                                                                                                                                                                                                
    @media = Media.new                                                                                                                                                                                                                       
                                                                                                                                                                                                                                             
  end                                                                                                                                                                                                                                        
                                                                                                                                                                                                                                             
                                                                                                                                                                                                                                             
  def test_if_this_test_framework_works                                                                                                                                                                                                      
                                                                                                                                                                                                                                             
    assert_equal(2, 1 + 1)                                                                                                                                                                                                                   
                                                                                                                                                                                                                                             
  end                                                                                                                                                                                                                                        
                                                                                                                                                                                                                                             
  def test_if_there_are_actors_of_that_tragedy                                                                                                                                                                                               
                                                                                                                                                                                                                                             
    assert_not_nil(@client);                                                                                                                                                                                                                 
    assert_not_nil(@it_people);                                                                                                                                                                                                              
    assert_not_nil(@media);                                                                                                                                                                                                                  
                                                                                                                                                                                                                                             
  end                                                                                                                                                                                                                                        
                                                                                                                                                                                                                                             
  def test_if_it_possible_to_add_advertising                                                                                                                                                                                                 
                                                                                                                                                                                                                                             
     @media.add_advertising(@it_people.telephone_number);                                                                                                                                                                                    
                                                                                                                                                                                                                                             
  end                                                                                                                                                                                                                                        
                                                                                                                                                                                                                                             
                                                                                                                                                                                                                                             
end    

Dodaję metodę setup, która odpala się za każdym razem, gdy odpala się test.
Dodatkowo zamiast zmiennych lokalnych ustawiam zmienne klasowe, których używam w dwóch ostatnich testach.

Nie podoba mi się jeszcze, że wszystkie testy są w jednym pliku testowym. Przeniosę nowe testy do nowego pliku actors_tests.rb i zaktualizuję Rakefile.

Powinienem chyba utworzyć tak zwane jarzmo testowe:

coola@sv26 [/home/coola/dsp2017/xp-simulator]# touch ./test/all_tests.rb    

Z zawartością:

require 'test/unit/testsuite'                                                                                                                                                                                                                
require './test/smoke_test.rb'                                                                                                                                                                                                               
require './test/actors_tests.rb' 

Odkryłem przypadkiem, że wystarczy odpalić taki plik poleceniem ruby i wszystkie testy wystartują automatycznie.

No dobra, czyli Rakefile powinien teraz wyglądać tak:


task default: %w[test]                                                                                                                                                                                                                       
                                                                                                                                                                                                                                             
task :test do                                                                                                                                                                                                                                
  ruby "./test/all_tests.rb"                                                                                                                                                                                                                 
end 

Dalsze przypadki testowe dopiszę wkrótce.

Link do commita zmian tu: https://github.com/coola/xp-simulator/commit/56d56a37e1c46b571924cf3d640540e6d548cc63

Mam wrażenie, że krzywa wejścia w Ruby jest mimo wszystko stroma, błądzę po omacku, szukam konstrukcji znajomych z innych języków, czując pod skórą, że mimo wszystko warto.

"Zabierz Pan nas stąd, Panie Sulu" Kapitan James T. Kirk



28 marca 2017

Czysta Architektura Mikroserwisów

Jak skalujesz system komputerowy?
Jedna rzecz powinna być oczywista: w którymś momencie potrzebujesz mieć więcej niż jeden komputer. Był taki czas, i to było całkiem niedawno, kiedy skalowanie mogło być osiągnięte przez czekanie. Po prostu czekałeś, aż komputery staną się szybsze i potężniejsze. Po każdych kilku miesiącach miałeś automatycznie wzrost na skali.

Poniższy tekst jest luźnym tłumaczeniem wpisu bloga Roberta Cecila "Wujka Boba" Martina ze strony :


Proszę o komentarze, jeżeli ta luźność jest zbyt daleko posunięta.

Pomijając, czy była to dobra strategia czy nie; to już tak nie działa. Wraz z nadejściem nowego tysiąclecia, projektanci sprzętu przestali próbować podnosić częstotliwość taktowania zegara i zamiast tego zaczęli rozmnażać rdzenie. Faktem jest, że aby osiągnąć to rozmnożenie - projektanci sprzętu usuwali pamięć podręczną i potoki, których zwykli używać do zwiększania prędkości maszyn jednordzeniowych.
Więc dzisiaj, skalowanie systemów komputerowych oznacza dodawanie więcej rdzeni, i dodawanie więcej serwerów. Nie ma drogi na około. No więc, jak to robisz? Jak rozdzielasz swoją aplikację, aby mogła być uruchamiania na wielu rdzeniach i wielu serwerach?
Jak skalujesz?
Twoja karta graficzna używa jednego podejścia. Posiada wiele procesorów, które działają w strategii lockstep; robią te same operacje w innych obszarach pamięci wewnętrznej. Ta forma obliczeń równoległych na masową skalę jest idealna dla kart graficznych odkąd szybką wydajność grafiki można osiągnąć przez te same przekształcenia na dużych macierzach tych samych danych. W rzeczywistości, superkomputery używały tego podejścia przez dziesięciolecia żeby przewidywać pogodę lub symulować wybuchy jądrowe.
Inną techniką jest tradycyjne podejście trzywarstwowe. Dzielisz swój system na GUI, warstwę pośrednią i bazę danych. Przyporządkowujesz serwery do GUI, kilka do warstwy pośredniej, i jeszcze kilka do bazy danych. Tworzysz zestaw wiadomości (zwykle zawierający zserializowane obiekty), które mogą być przesyłane pomiędzy warstwami. I voilà! Skalowanie.
Mikroserwisy
Ostatnio widzimy jeszcze inną strategię skalowania. Mikroserwisy. Pisałem o nich tu i tu. Jest sławny artykuł od Martina Fowlera i Jamesa Lewisa tu.
Mikroserwis jest małym programem wykonywanym gdzieś na serwerze. Odpowiada na asynchroniczne wiadomości. Zwykle te wiadomości są dostarczane przez HTTP w formacie REST; chociaż to szczegół, a nie wymaganie.
System posiada architekturę mikroserwisów wtedy kiedy jest ułożony z wielu współpracujących mikroserwisów; zwykle bez centralnej kontroli.
Czysta Architektura i Mikroserwisy
Teraz rozważ tak zwaną Czystą Architekturę. Zauważ, że używa ona wiele elementów, włączając Przypadki Użycia, Prezenterów, Bramki. Te komponenty otrzymują żądania w postaci podstawowych struktur danych (POJO), które przychodzą ze źródła, oddzielonego od komponentu przez polimorficzną granicę wejściową.
Zauważ, że komponenty odpowiadają na te żądania poprzez tworzenie podstawowych struktur danych i wysyłanie ich na wyjście, które jest oddzielone od komponentu przez polimorficzną granicę wyjściową.
Czy ten układ może być użyty do stworzenia architektury mikroserwisów?
Oczywiście.
Nic w Czystej Architekturze nie żąda, aby wiadomości były synchroniczne czy asynchroniczne. Nic nie stoi na przeszkodzie, by te żądania i odpowiedzi były przesyłane do innego serwera. Nic w architekturze nie przeszkadza, żeby komponenty były małymi programami komunikującymi się przez HTTP przy użyciu REST.
Więc, architektura mikroserwisów może łatwo dopasować się do Czystej Architektury. W rzeczywistości, gdybym tylko budował system używając mikroserwisów, z całkowitą pewnością podążałbym tą ścieżką.
Miara skalowalności komponentów
Mikroserwis jest tylko sposobem do wdrażania komponentów oprogramowania. Są inne;
i mają różne skalowalności. Oto lista kilku innych opcji wdrażania, w kolejności poziomu skalowalności.

  1. Mikroserwisy wdrożone na wiele serwerów.
  2. Mniejsza liczba serwerów, każdy odpala więcej niż jeden mikroserwis.
  3. Tylko jeden serwer z grupą mikroserwisów uruchamianych jako proste programy.
  4. Usługi odpalane jako wątki w pojedynczej maszynie wirtualnej komunikujące się poprzez kolejki komunikatów.
  5. Dynamicznie linkowane komponenty (JARki albo DLLki) przesyłające wiadomości ze strukturą danych poprzez wywołania funkcji.
Znowu, to powinno być oczywiste, że Czysta Architektura działa tak samo w każdym przypadku z tej listy. Powód tego jest taki, że Czysta Architektura nie przykłada wagi do tego jak komponenty są wdrażane. W rzeczywistości system z dobrą Czystą Architekturą nie wie jakiego sposobu wdrażania używa.
Jeszcze raz powtarzam. Kod w środku komponentów Czystej Architektury nie ma pojęcia, że:
  • to jest działający mikroserwis na niezależnym serwerze komunikującym się z innymi niezależnymi serwerami przez internet,
  • albo to jest mały program pośród wielu odpalonych na pojedynczej maszynie komunikujący się na prostych socketach,
  • albo to jest lekki wątek komunikujący się z innymi lekkimi wątkami na tej samej maszynie wirtualnej używając skrzynek pocztowych czy kolejek,
  • albo to prosty JAR czy DLL komunikujący się z innymi komponentami przy użyciu polimorficznych wywołań funkcji.
I to powinno naprowadzić Cię o czym tak naprawdę jest ten artykuł.
Model Wdrożenia jest Szczegółem.
Jeżeli kod komponentów może być tak napisany, aby mechanizmy komunikacyjne i mechanizmy rozdzielenia procesów były nieistotne, wtedy te mechanizmy są szczegółami. A szczegóły nigdy nie są częścią architektury.

To oznacza, że nie ma czegoś takiego jak architektura mikroserwisów. Mikroserwisy są opcją wdrożenia, a nie architekturą. Dobra architektura wszystkie opcjonalne kwestie utrzymuje otwartymi tak długo, jak to tylko możliwe. Dobra architektura odkłada decyzję, w jaki sposób system będzie wdrożony, aż do ostatniego możliwego momentu.
Zamierzona ignorancja.
Wiele osób z pewnością będzie narzekać na przedstawiony punkt widzenia, że jeżeli nie zaprojektujesz swojego systemu dla mikroserwisów na początku, nie będziesz mógł ich podmienić na końcu.
Tere - fere. Przecież to BDUF
Dobrzy architekci systemów tworzą struktury, których komponenty systemu - czy to przypadki użycia, komponenty widoku, bazy danych, czy czego tam chcesz - nie mają pojęcia w jaki sposób są wdrażane i w jaki sposób komunikują się z innymi komponentami systemu. Zamierzona ignorancja pozwala architektom wybrać właściwy model wdrożeń, który działa w aktualnej sytuacji i pozwala dopasowywać ten model do zmian. Jeżeli system musi się szeroko skalować wdrażasz to na mikroserwisach. Jeżeli system potrzebuje dwóch, trzech serwerów, wdrażasz to na mieszance procesów, wątków i JARów/DLLów. Jeżeli nigdy nie potrzebowałeś więcej niż jednego serwera, możesz wdrożyć to na jednym JAR/DLL.
Łamanie tej zamierzonej ignorancji jest dobrą drogą do przekombinowania projektu systemu. Za często widziałem systemy, które przyjęły trzywarstwowe architektury licząc na skalowalność, tylko po to żeby odkryć, że te systemy nigdy nie będą musiały być wdrażane na więcej niż na jednej maszynie. Jak dużo prostsze mogłoby być to oprogramowanie, gdyby tylko projektanci na początku spróbowali opcji z jednym serwerem i zachowali niezależność komponentów od modelu wdrażania.
Inne sprawy
Oczywiście są inne sprawy do rozważenia. Po pierwsze, jeżeli wdrażasz na mikroserwisach, masz wolną rękę co do wyboru Twojego ulubionego języka. Możesz pisać swoje mikroserwisy w Ruby, Clojure, Javie, C#, C++, C, Assemblerze, Pascalu, Delphi, PHP, Javascriptcie lub nawet w COBOLu. Po drugie, możesz wybrać dowolny framework jaki tylko Ci się podoba. Jeden mikroserwis może używać Ralisów, inny może używać springa, jeszcze inny może używać BOOSTa. Podobnie, każdy mikroserwis może używać innej bazy danych. Jeden może użyć Couch-a, podczas gdy inny może używać SQLServer i jeszcze inny może używać MySQL czy Datomic. Na koniec, mikroserwisy zapewniają duży poziom izolacji. Granice mikroserwisów są ostateczną formą rozdzielenia.
Ten ostatni punkt wymaga uwypuklenia. Jeżeli dwa komponenty komunikują się ze sobą przez HTTP używając REST są bardzo mocno rozdzielone. Jedyną rzeczą łączącą te dwa komponenty razem jest schemat wiadomości REST; innymi słowy interfejs. Nie tylko są rozłączone przez interfejs, są rozłączone także w czasie wdrożenia. Te dwa serwisy nie muszą być odpalone w tym samym czasie; tak samo nie muszą być wyłączane w tym samym czasie. Jest oczywiście możliwość przeładowania mikroserwisu bez przeładowywania tych, które od niego zależą. To oznacza ogromne rozdzielenie.
Ograniczenia idąc w dół skali.
Jak idziesz w dół skali, od mikroserwisów do procesów, do wątków, do JARków zaczynasz tracić niektóre z tych możliwości. Im bliżej jesteś JARków, tym mniej dowolności masz w wyborze języka. Masz także mniej dowolności co do frameworków i baz danych. Jest także większe ryzyko, że interfejsy pomiędzy komponentami będą z czasem coraz bardziej powiązane. I oczywiście ciężko przeładować komponenty, które żyją w jednym skompilowanym programie.
Ale czy na pewno? Właściwie OSGi istnieje w świecie Javy już od jakiegoś czasu. OSGi pozwala Ci przeładować pliki JAR w locie. Nie jest to tak wygodne jak kopnąć mikroserwis, ale nie jest też daleko od tego.
Jeżeli chodzi o języki, prawdą jest, że w ramach pojedynczej maszyny wirtualnej będziesz ograniczony. Z drugiej strony, JVM pozwoli Ci pisać w Javie, Clojure, Scali i JRuby, a to tylko niektóre wymienione.
Więc, tak, jeżeli pójdziesz w dół skali ograniczenia wzrosną; ale być może nie tak bardzo.
Jeżeli chodzi o frameworki i bazy danych, czy to naprawdę coś złego, zwłaszcza we wczesnej fazie rozwoju aplikacji, aby ograniczyć ich liczbę? Czy chcemy naprawdę zacząć z jednym zespołem używającym JPA, a drugim używającym Hibernate? Czy naprawdę chcemy, aby jeden komponent używał bazy Datomic, a drugi używał Oracla? I czy jeżeli pozwolimy na to, to czy nie tworzymy mnóstwa złożoności konfiguracyjnej?
I, na koniec, łączenie przy użyciu interfejsów jest sprawą dyscypliny i dobrego projektowania. Nade wszystko, zwykły obiekt Java (POJO) przesłany przez polimorficzny interfejs nie jest bardziej powiązany niż REST. Trochę troski podczas projektowania komponentów sprawi, że będziemy mieli JARki, których interfejsy są tak luźno rozłączone jak mikroserwis.
Kiedy poruszasz się w górę skali, te ograniczenia odpadają, ale nowe problemy zaczynają się pojawiać. W jakim porządku startujesz system? W jakim porządku go wyłączasz? Jak radzisz sobie z konfiguracją i kontrolą tych wszystkich usług? Co z powielonym kodem? Co z wersjonowaniem formatów wiadomości? Ale zamiast mnie wyliczającego kwestie tutaj, możesz przeczytać o niektórych z nich tu i tu. Wystarczy w tym miejscu powiedzieć, że decyzja, aby użyć mikroserwisów jest decyzją typu "coś za coś" i nie jest przysłowiowym darmowym lunchem.
Monolity i Marketerzy.
Na koniec słówko o nazewnictwie. Niektórzy obrońcy mikroserwisów lubią szufladkować inne podejście jako monolityczne. To ujmujące pojęcie wybrane po to, aby zasugerować: “To jest złe”. Słowo monolit oznacza “jedną skałę”. W domyśle oznacza to, że jeżeli nie używasz mikroserwisów, to musisz mieć wielkiego, zbitego potwora.
To są byzydury marketingowe.
Dobrze zaprojektowany system podążający za Czystą Architekturą jest tak daleko od monolitu jak tylko się da. Wszak, jest to zestaw niezależnych dynamicznie wdrażanych komponentów (JARek albo DLLek), które są luźno połączone ze sobą, mogą być utrzymywane przez różne zespoły, mogą być pisane w ogromie różnych języków i mogą być podmieniane w locie używając narzędzi podobnych do OSGi. Trudno to nazwać monolitycznym.
Podsumowanie i Rada [1]
Z tego wszystkiego mógłbyś sobie pomyśleć, że uważam mikroserwisy za zły pomysł; i że nie powinieneś ich używać. To nie tak. Mikroserwisy są doskonałym modelem wdrożeniowym, jeżeli tylko postarasz się, żeby pasował do Twoich rozwiązań. Jeżeli nie możesz wdrażać na mikroserwisy, to oznacza, że powiązałeś swoją architekturę z konkretnym modelem wdrożenia.
Z tego samego powodu, jeżeli możesz wdrożyć Twój system jedynie na mikroserwisy, wtedy masz powiązaną architekturę z tym konkretnym modelem wdrożenia; i to jest tak samo złe.
To, do czego chcę Cię przekonać, to abyś pomijał każdy konkretny model wdrożenia. Traktuj model wdrożenia jako szczegół, i zostaw sobie otwarty wybór. Zbuduj swój system tak, abyś mógł go wdrażać albo na JARki, albo na mikroserwisy, albo na cokolwiek pomiędzy.
Rozpocznij wdrażanie Twojego systemu na dynamicznie linkowane komponenty (JARki lub DLLki), i stopniowo idź w górę skali jak będzie potrzeba. Nie skacz na górę skali przewidując masową skalowalność. Zostaw sobie tę opcję otwartą zgodnie z Czystą Architekturą.
[1] Kimże jestem żeby dawać takie rady? Przecież jak napisałem w poprzednim artykule, spotkałem się z pojęciem “Mikroserwisy” kilka tygodni temu.
Niedawno odkryłem pojęcie; ale w ciągu ostatnich 40 lat mojej kariery miałem wielokrotnie możliwość projektowania i budowania systemów, które wdrażały komponenty jako niezależne programy dogadujące się ze sobą przez komunikaty. Mikroserwisy mogą być nowym pojęciem; ale ciężko powiedzieć, że to nowy pomysł.

Powyższy tekst jest luźnym tłumaczeniem wpisu bloga Roberta Cecila "Wujka Boba" Martina ze strony :


Proszę o komentarze, jeżeli ta luźność jest zbyt daleko posunięta.

23 marca 2017

Zagraj to jeszcze raz, Sam

OK, czyli ostatnio skończyliśmy na tym, że trzeba ustawić wartość środowiskową przed startem skryptu. Chciałem do tego zaprząc narzędzie o nazwie rake - czyli menadżer budowania dla ruby.


Idea, która stoi za rake jest ciekawa. Postanowiono w skryptach do budowania ruby pisać w ..... ruby. Proste i genialne jednocześnie. Poza tym fajna gra słów w nazwie. Nazwa pochodzi od wcześniejszego narzędzia make dla C++ - tylko zamieniono literkę "M" na literkę "R" jak Ruby. Poza tym rake w tłumaczeniu to grabie.



Pasuje idealnie, jeżeli przyjąć ostatni trend porównywania tworzenia oprogramowania do projektowania i utrzymywania ogrodu zamiast do budowania domów.

W toku okazało się, że nie muszę ustawiać żadnej zmiennej środowiskowej.

 To zadziała:

coola@sv26 [/home/coola/dsp2017/xp-simulator]# ruby ./test/smoke_test.rb 

Jeżeli tylko ustawie prawidłowo ścieżki require:

require "./src/Client.rb"                                                                                                                                                                                                                    
require "./src/ItPeople.rb"                                                                                                                                                                                                                  
require "./src/Media.rb"     

Te kropki na początku ścieżki są tu kluczowe.

Warto wiedzieć, że null w Ruby nazywa się nil:

def test_if_there_are_actors_of_that_tragedy                                                                                                                                                                                               
                                                                                                                                                                                                                                             
    client = Client.new                                                                                                                                                                                                                      
    it_people = ItPeople.new                                                                                                                                                                                                                 
    media = Media.new                                                                                                                                                                                                                        
                                                                                                                                                                                                                                             
    assert_not_nil(client);                                                                                                                                                                                                                  
    assert_not_nil(it_people);                                                                                                                                                                                                               
    assert_not_nil(media);                                                                                                                                                                                                                   
                                                                                                                                                                                                                                             
  end   

A więc ostateczny skrypt rake o nazwie Rakefile będzie po prostu:

task default: %w[test]                                                                                                                                                                                                                       
                                                                                                                                                                                                                                             
task :test do                                                                                                                                                                                                                                
  ruby "./test/smoke_test.rb"                                                                                                                                                                                                                
end 

Odpalanie projektu poleceniem rake daje takie wyniki:

coola@sv26 [/home/coola/dsp2017/xp-simulator]# rake                                                                                                                                                                                          
/usr/share/ruby-2.3.1/bin/ruby ./test/smoke_test.rb                                                                                                                                                                                          
Loaded suite ./test/smoke_test                                                                                                                                                                                                               
Started                                                                                                                                                                                                                                      
..                                                                                                                                                                                                                                           
                                                                                                                                                                                                                                             
Finished in 0.000537369 seconds.                                                                                                                                                                                                             
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
2 tests, 4 assertions, 0 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications                                                                                                                                                        
100% passed                                                                                                                                                                                                                                  
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
3721.84 tests/s, 7443.67 assertions/s  

Napisaliśmy to jeszcze raz, Sam. Tym razem działa :)

Cały czas  mam z tyłu głowy potrzebę wersjonowania projektu i wysyłania go na serwer.
Dodaję pliki:

coola@sv26 [/home/coola/dsp2017/xp-simulator]# git add .                                                                                                                                                                                     
coola@sv26 [/home/coola/dsp2017/xp-simulator]# git status                                                                                                                                                                                    
On branch master                                                                                                                                                                                                                             
Your branch is up-to-date with 'origin/master'.                                                                                                                                                                                              
Changes to be committed:                                                                                                                                                                                                                     
  (use "git reset HEAD <file>..." to unstage)                                                                                                                                                                                                
                                                                                                                                                                                                                                             
        new file:   Rakefile                                                                                                                                                                                                                 
        deleted:    smoke_test.rb                                                                                                                                                                                                            
        new file:   src/Client.rb                                                                                                                                                                                                            
        new file:   src/ItPeople.rb                                                                                                                                                                                                          
        new file:   src/Media.rb                                                                                                                                                                                                             
        new file:   test/smoke_test.rb     

Kommituję lokalnie:

coola@sv26 [/home/coola/dsp2017/xp-simulator]# git commit -m "Frist actors of the system and rake building tool addition"                                                                                                                    
[master dcfeb38] Frist actors of the system and rake building tool addition                                                                                                                                                                  
6 files changed, 36 insertions(+), 11 deletions(-)                                                                                                                                                                                          
create mode 100644 Rakefile                                                                                                                                                                                                                 
delete mode 100644 smoke_test.rb                                                                                                                                                                                                            
create mode 100644 src/Client.rb                                                                                                                                                                                                            
create mode 100644 src/ItPeople.rb                                                                                                                                                                                                          
create mode 100644 src/Media.rb                                                                                                                                                                                                             
create mode 100644 test/smoke_test.rb 

Ups, literówka w kommicie w wyrazie "Frist... Miałem na myśli "First....
Jak to naprawić?
Wujek Stack mówi, że tak:

coola@sv26 [/home/coola/dsp2017/xp-simulator]# git commit --amend -m "First actors of the system and rake building tool addition"      

Historia projektu:

coola@sv26 [/home/coola/dsp2017/xp-simulator]# git log 

Rzeczywiście potwierdza zmiany:

commit 1f6a0632411d98ca1340a4ce6d48dffd68b79f88
Author: coola <coola@o2.pl>                                                                                                                                                                                                                  
Date:   Thu Mar 23 16:37:59 2017 +0100                                                                                                                                                                                                       
                                                                                                                                                                                                                                             
    First actors of the system and rake building tool addition                                                                                                                                                                               
                                                                                                                                                                                                                                             
commit 984f97a26cbf9ec32eed6b6304f0ac1ede1770ef                                                                                                                                                                                              
Author: coola <coola@o2.pl>                                                                                                                                                                                                                  
Date:   Fri Mar 10 11:03:08 2017 +0100                                                                                                                                                                                                       
                                                                                                                                                                                                                                             
    First class. Test class                                                                                                                                                                                                                  
                                                                                                                                                                                                                                             
commit aef10c1c8f493e5a3ef0cfe654a12c89c3a0c616                                                                                                                                                                                              
Author: coola <coola@o2.pl>                                                                                                                                                                                                                  
Date:   Tue Mar 7 13:36:07 2017 +0100                                                                                                                                                                                                        
                                                                                                                                                                                                                                             
    Pierwszy commit z plikiem README       

Teraz, jak już mam stabilne środowisko, mogę się pokusić o próbę implementacji komunikacji między aktorami tej tragedii, ale to już w następnym odcinku :)

Ups, zapomniałem wysłać kommitów na serwer:

coola@sv26 [/home/coola/dsp2017/xp-simulator]# git push origin                                                                                                                                                                               
Username for 'https://github.com': coola                                                                                                                                                                                                     
Password for 'https://coola@github.com':                                                                                                                                                                                                     
Counting objects: 9, done.                                                                                                                                                                                                                   
Delta compression using up to 8 threads.                                                                                                                                                                                                     
Compressing objects: 100% (5/5), done.                                                                                                                                                                                                       
Writing objects: 100% (9/9), 904 bytes | 0 bytes/s, done.                                                                                                                                                                                    
Total 9 (delta 0), reused 0 (delta 0)                                                                                                                                                                                                        
To https://github.com/coola/xp-simulator.git                                                                                                                                                                                                 
   984f97a..1f6a063  master -> master        

Uff, zawsze czuję ulgę, gdy zapisuję coś gdzieś indziej niż w jednym miejscu.

22 marca 2017

Trochę architektury

Chcę zostać architektem oprogramowania.

To dobry cel dla młodego dewelopera.

Chcę kierować zespołem i podejmować wszystkie ważne decyzje dotyczące baz danych, frameworków, serwerów webowych i całego tego stuffu.

Acha. No cóż, czyli tak naprawdę nie chcesz być architektem oprogramowania.

Poniższy tekst jest luźnym tłumaczeniem wpisu bloga Roberta Cecila "Wujka Boba" Martina ze strony :
Proszę o komentarze, jeżeli ta luźność jest zbyt daleko posunięta.


Oczywiście, że chcę! Chcę być tym, który podejmuje te wszystkie ważne decyzje.

No dobrze, ale Ty nie wymieniłeś ważnych decyzji. Wymieniłeś te nieistotne.

Co masz na myśli? Baza danych nie jest ważną decyzją? Czy wiesz ile pieniędzy wydajemy na nią?

Prawdopodobnie za dużo. I nie, baza danych nie jest jedna z najważniejszych decyzji.

Jak możesz tak mówić? Baza danych to serce systemu! To tam wszystkie dane są poukładane, posortowane, poindeksowane i dostępne. Bez niej nie byłoby systemu!

Baza danych jest jedynie urządzeniem wejścia - wyjścia. Tak się składa, że dostarcza kilka użytecznych narzędzi do sortowania, wybierania i raportowania, ale są one tylko pomocnicze w stosunku do architektury systemu.

Pomocnicze? To szaleństwo.

Tak pomocnicze. Reguły biznesowe systemu mogą używać niektórych z tych narzędzi; ale te narzędzia nie są powiązane z regułami biznesowymi. Jeżeli musisz, to możesz zastąpić te narzędzia innymi narzędziami; ale reguły biznesowe pozostaną takie same.

No, tak, ale wtedy trzeba by przepisać je, bo one wszystkie używają narzędzi z oryginalnej bazy danych.

No cóż, to Twój problem.

Co masz na myśli?

Twój problem polega na tym, że wierzysz, że reguły biznesowe zależą od narzędzi bazy danych. One nie zależą. Albo przynajmniej nie powinny zależeć jeżeli chcesz stworzyć dobrą architekturę.

Co za wariactwo. Jak mam stworzyć reguły biznesowe, które nie używają narzędzi, których muszą używać?

Nie powiedziałem, że nie używają narzędzi bazy danych. Powiedziałem, że nie powinny od nich zależeć. Reguły biznesowe nie powinny wiedzieć jakiej używają konkretnej bazy danych.

Jak zrobić, żeby reguły biznesowe używały narzędzi nie wiedząc o nich?

Odwracasz zależności. Masz bazę danych zależną od reguł biznesowych. Upewniasz się, że reguły biznesowe nie zależą od bazy danych.

Gadasz bzdury.

Wręcz przeciwnie, mówię językiem Architektury Oprogramowania. To jest Zasada Odwracania Zależności. Niskopoziomowe polityki powinny zależeć od wysokopoziomowych polityk.

Więcej bzdur! Polityki wysokiego poziomu (przypuszczam, że masz na myśli reguły biznesowe) wołają polityki niskiego poziomu (zakładam, że chodzi o bazę danych). A więc polityki wysokiego poziomu zależą od polityk niskiego poziomu w ten sam sposób jak nadawcy zależą od odbiorców. Każdy to wie!

To jest prawda w czasie wykonania programu. Ale w czasie kompilacji potrzebujemy odwróconych zależności. Kod źródłowy wysokopoziomowych polityk nie powinien wspominać kodu źródłowego niskopoziomowych polityk.

No, proszę Cię! Nie możesz wołać czegoś bez wspominania o tym.

Oczywiście, że możesz. Wokół tego kręci się całe Zorientowane Obiektowo.

Zorientowane Obiektowo jest o tworzeniu modeli prawdziwego świata, jest o łączeniu danych i funkcji w spójne obiekty. Jest o organizacji kodu w intuicyjną strukturę.

Czy to właśnie Ci powiedzieli?

Każdy to wie. To jest oczywista prawda.

Bez wątpienia. Bez wątpienia. I jeszcze, wykorzystując zasady programowania obiektowego możesz rzeczywiście zawołać coś bez wspominania o tym.

No, dobra? Jak?

Wiesz, że w projekcie zorientowanych obiektowo - obiekty wysyłają do siebie wiadomości?

Tak. Oczywiście.

I wiesz, że nadawca wiadomości nie zna typu odbiorcy?

To zależy od języka. W Javie ten który wysyła zna przynajmniej typ klasy bazowej tego, który odbiera. W Ruby nadawca wie przynajmniej, że odbiorca może odebrać wysłaną wiadomość.

Prawda. Ale w obu przypadkach nadawca nie zna dokładnego typu odbiorcy.

Taaa. OK. Pewnie.

W związku z tym, nadawca może spowodować, że uruchomienie funkcji u odbiorcy bez wspominania dokładnego typu odbiorcy.

Taaa. Prawda. Łapię to. Ale nadawca nadal zależy od odbiorcy.

W czasie wykonania tak. Ale nie w czasie kompilacji. Kod źródłowy odbiorcy nie wspomina, ani nie zależy od kodu źródłowego odbiorcy. W rzeczywistości kod źródłowy odbiorcy zależy od kodu źródłowego nadawcy.

Nieee! Nadawca nadal zależy od klasy, do której wysyła.

Może kod źródłowy rozjaśni to. Napiszę to w Javie. Najpierw pakiet nadawcy:
package sender;

public class Sender {
  private Receiver receiver;

  public Sender(Receiver r) {
    receiver = r;
  }

  public void doSomething() {
    receiver.receiveThis();
  }

  public interface Receiver {
    void receiveThis();
  }
}
Następnie pakiet odbiorcy:
package receiver;

import sender.Sender;

public class SpecificReceiver implements Sender.Receiver {
  public void receiveThis() {
    //do something interesting.
  }
}
Zauważ, że pakiet odbiorcy zależy od pakietu nadawcy. Zauważ też, że SpecificReceiver zależy od Sender. Zauważ też, że nic w pakiecie nadawcy nie wie o niczym w pakiecie odbiorcy.

No tak, ale oszukiwałeś. Włożyłeś interfejs odbiorcy w pakiet nadawcy.

Zaczynasz rozumieć, Pasikoniku.

Rozumieć co?

Zasady architektury oczywiście. Nadawcy zawierają interfejsy, które odbiorcy implementują.

No cóż, jeżeli to oznacza, że użycie zagnieżdżonych klas to ....
Klasy zagnieżdżone to tylko jeden środek do celu. Są inne.

Do dobra, poczekaj. Co to wszystko ma wspólnego z bazami danych. Od tego zaczęła się nasza dyskusja.

Spójrzmy na trochę więcej kodu. Najpierw reguły biznesowe:
package businessRules;

import entities.Something;

public class BusinessRule {
  private BusinessRuleGateway gateway;

  public BusinessRule(BusinessRuleGateway gateway) {
    this.gateway = gateway;
  }

  public void execute(String id) {
    gateway.startTransaction();
    Something thing = gateway.getSomething(id);
    thing.makeChanges();
    gateway.saveSomething(thing);
    gateway.endTransaction();
  }
}


Ta reguła biznesowa za wiele nie robi.

To tylko przykład. Mógłbyś mieć więcej klas takich jak ta implementujących masę innych reguł biznesowych.

OK, a o co chodzi z tym całym Gateway?

Zapewnia dostęp do danych dla metod używanych przez regułę biznesową. Wygląda tak:
package businessRules;

import entities.Something;

public interface BusinessRuleGateway {
  Something getSomething(String id);
  void startTransaction();
  void saveSomething(Something thing);
  void endTransaction();
}
Zauważ, że jest w pakiecie businessRules.

Taaa, OK. A co to za klasa Something?

Ona reprezentuje prosty obiekt biznesowy. Wrzuciłem ją do pakietu entities.
package entities;

public class Something {
  public void makeChanges() {
    //...
  }
}
I na koniec implementacja BusinessRuleGateway. To jest ta klasa, która wie wszystko o rzeczywistej bazie danych.
package database;

import businessRules.BusinessRuleGateway;
import entities.Something;

public class MySqlBusinessRuleGateway implements BusinessRuleGateway {
  public Something getSomething(String id) {
    // use MySql to get a thing.
  }

  public void startTransaction() {
    // start MySql transaction
  }

  public void saveSomething(Something thing) {
    // save thing in MySql
  }

  public void endTransaction() {
    // end MySql transaction
  }
}
I znowu, zauważ, że reguły biznesowe wołają bazę danych w czasie wykonania; ale w czasie kompilacji to pakiet database wspomina i zależy od pakietu businessRules.

No dobra, dobra, myślę, że rozumiem. Ty po prostu używasz polimorfizmu żeby schować implementację bazy danych przed regułami biznesowymi. Ale nadal musisz mieć interfejs, który dostarcza wszystkie narzędzia bazy danych regułom biznesowym.

Nie, w żadnym wypadku. Nie próbujemy dostarczyć wszystkich narzędzi bazodanowych regułom biznesowym. Raczej, mamy tak, że to reguły biznesowe tworzą tylko takie interfejsy, których potrzebują. Implementacja tych interfejsów może wołać odpowiednie narzędzia.

Tak, ale jeżeli reguły biznesowe potrzebują wszystkich tych narzędzi, wtedy musisz wrzucić wszystkie te narzędzia do interfejsu gateway.

Ech. Widzę, że nadal nie rozumiesz.

Nie rozumiem czego? Wydaje mi się to całkiem jasne.

Każda reguła biznesowa definiuje interfejs tylko do takiego obiektu dostępu do danych, jakiego potrzebuje.

Poczekaj. Co?

To nazywa się Zasada Segregacji Interfejsów. Każda klasa reguł biznesowych będzie używała tylko niektórych obiektów bazy danych. A więc, każda reguła biznesowa dostarcza interfejsu, który daje dostęp tylko do tych obiektów.

Ale to oznacza, że będziesz miał mnóstwo interfejsów i mnóstwo klas, które je implementują wołających inne klasy bazy danych.

No dobrze. Widzę, że zaczynasz rozumieć.

Ale to jest burdel i strata czasu! Dlaczego miałbym tak robić?

Mógłbyś to robić, żeby mieć czysty kod i oszczędzić czas.

No weź, to po prostu pisanie mnóstwa kodu sztuka dla sztuki.

Przeciwnie, to są ważne decyzje architektury, które pozwolą Ci odraczać w czasie decyzje bez znaczenia.

Co przez to rozumiesz?

Pamiętasz, na początku, jak mówiłeś, że chcesz być Architektem Oprogramowania? Chciałeś podejmować te wszystkie ważne decyzje?

Tak, tego właśnie chcę.

Pośród tych decyzji, które chciałeś podejmować były bazy danych, serwery webowe i frameworki.

Tak, i ty powiedziałeś, że one nie były ważnymi decyzjami. Powiedziałeś, że były bez znaczenia.

To prawda. Takie są. Ważne decyzje podejmowane przez Architekta Oprogramowania to te które pozwalają Ci NIE podejmować decyzji o bazach danych, serwerze webowym i frameworkach.

Ale musisz podjąć te decyzje na początku!

Nie nie musisz. W rzeczywistości, chcesz, żebyś te decyzje mógł podjąć w późniejszym cyklu rozwoju - wtedy, kiedy będziesz miał więcej informacji.

Biada architektowi, który przedwcześnie decyduje o bazie danych, i potem dochodzi do wniosku, że płaskie pliki będą skuteczniejsze.

Biada architektowi, który przedwcześnie decyduje o serwerze bazy danych; tylko po to żeby przekonać się, że wszystko to, czego potrzebował zespół to otwarcie jednego gniazda sieciowego.

Biada zespołowi ludzi, w którym architekci przedwcześnie narzucają im framework, tylko by przekonać się, że framework dostarcza funkcjonalności, których nie potrzebują i dodaje ograniczeń, z którymi nie mogą żyć.

Błogosławiony zespół, w którym architekci dostarczyli środków, dzięki którym wszystkie te decyzje mogą być odłożone, aż będzie wystarczająco dużo informacji, aby je podjąć.
Błogosławione zespoły, w których architekci odizolowali je od wolnych i zasobożernych urządzeń wejścia/wyjścia i frameworków, że mogą dzięki temu stworzyć szybkie i lekkie środowiska testowe.

Błogosławiony zespół, w którym architekci dbają o to, co się naprawdę liczy i odkładają te rzeczy, które są nieważne.

Bzdury. Nie kupuję tego.

No cóż, być może uznasz to za swoje w ciągu dekady lub więcej …… no chyba, że zostaniesz do tego czasu menadżerem.

Poniższy tekst jest luźnym tłumaczeniem wpisu bloga Roberta Cecila "Wujka Boba" Martina ze strony :
http://blog.cleancoder.com/uncle-bob/2016/01/04/ALittleArchitecture.html
Proszę o komentarze, jeżeli ta luźność jest zbyt daleko posunięta.

Podstawy Programowania Funkcyjnego Epizod 3

Czy wszystkie Zasady Się Zmieniają? Kiedy tylko zaczynamy używać nowego paradygmatu , porównujemy z nim na...