czwartek, 27 września 2018

Życie bez wyboru

Podczas refaktoryzacji mojego kodu, eliminowaniu powtórzeń, odwracaniu zależności itp... zaczęła mnie dręczyć myśl czy da się wyeliminować instrukcje warunkowe z kodu.
Pierwszy raz te pytanie pojawiło się kilka miesięcy temu gdy oglądałem na pluralsiajcie wykład na temat SOLID-a. Zaciekawił mnie trochę temat, zgłębiłem ale nic szczególnego nie odkryłem.
Dopiero kilka dni temu gdy  oglądałem na yuotubach, filmik o wzorcach projektowych spotkałem się z wzorcem Strategia. W książce "gangu czterech" jest opis, że dzięki "Strategi" można wyeliminować instrukcje warunkowe. Ciekawe, pytanie czy wszystkie IF-?, do tej pory nie umiałem sobie wyobrazić jak miałby wyglądać kod bez IF-a. Czy to możliwe?
Próbowałem tego przy refaktoryzacji filtrów w aplikacji Maratonu, owszem udało się wyeliminować warunki ale nie wszystkie. Zostały te, które uznałem za najpotrzebniejsze, przeniosłem je do osobnych  funkcji.
Temat "strategi" będę jeszcze drążył ale takie proste sposoby na zastąpienia instrukcji warunkowej można by opisać np tak (przykład z mojej refaktoryzacji).:

Mam funkcje, jednym z parametrów jest  "kontrola" typu int (parametr o nazwie kontrola), parametr ten opisuje pewną kolumnę w bazie, która przyjmuje wartość True, False lub null dlaczego i po co nie, ma teraz znaczenia. Oryginalnie tam są jeszcze inne parametry ale je pomijam.
Do nie dawna kod wyglądał by tak

void writeToViewBag(int kontrola)
        {
            if (kontrola == 0)
            {
                ViewBag.kontrola = null;
            }
            if (kontrola == 1)
            {
                ViewBag.kontrola = true;
            }
            if (kontrola == 2)
            {
                ViewBag.kontrola = false;
            }      
      }

Funkcje nic nie zwraca, trzy warunki, sprawdzam stan parametru i na tej podstawie przekierowuje "ruch" i wrzucam wartość do ViewBag-a.
Czym jest dla mnie instrukcja warunkowa? Nie będę pisał regułki bo jej nie pamiętam, opis jaki mi się nasuwa to IF jest jak zwrotnica (szyny, pociąg, itd...) przekierowuje nas na odpowiedni tor, celem dojechania w odpowiednie miejsce tutaj celem jest VieBag.Kontrola i przypisanie wartości ale może być to cokolwiek innego.
To jest sytuacja prosta bo nie mamy zagnieżdżonych warunków, sprawdzamy prostego int-a, a gdy by to był obiekt którego stan determinuje zachowanie całej aplikacji?
Dobra usunę teraz z funkcji instrukcję warunkową, sposobów jest kilka mi się nasuwają dwa ale jest więcej (jak mi się przypomni resztę też napiszę):
  • Dodaje słownik (Dictionary<key,value>) listę: klucz, wartość
  void writeToVieBag(int kontrola)
        {
          Dictionary<int, bool?> slownikKontrola = new Dictionary<int, bool?>();
            slownikKontrola.Add(0, null);
            slownikKontrola.Add(1, true);
            slownikKontrola.Add(2, false);

   ViewBag.kontrola =slownikKontrola.Where(x => x.Key == kontrola).FirstOrDefault().Value;
 }

Uproszczone? tak, difinicja słownika może być poza funkcją,  trzy instrukcje warunkowe zastąpione jedną linijką. Ktoś powie że to nieprawda przecież muszę zdefiniować "slownikKontrola" tak ale  mogę go wykorzystać w wielu miejscach. Jeżeli będę musiał zwiększyć liczbę możliwości odszukam definicję słownika i po prostu dodam kolejne pozycje. Zmiana typów to też nie jest problem.
  • Dodaje klasę SlownikKontrola

public class SlownikKontrola

        {
            public int key { get; set; }
            public bool? Value { get; set; }
        }

void writeToViewBag(int kontrola, bool popup, string komunikat, bool kontrolaPonowna)
        {

            List<SlownikKontrola> lista = new List<SlownikKontrola>();
            lista.Add(new SlownikKontrola { key = 1, Value = null });
            lista.Add(new SlownikKontrola { key = 2, Value = true });
            lista.Add(new SlownikKontrola { key = 3, Value = false });
            ViewBag.kontrola = lista.Where(x => x.key == kontrola).FirstOrDefault().Value;
        }

Zalety takie jak w przypadku słownika, tylko trochę więcej pisania (definicja klasy) ale można był by poprzez interfejs zrobić i nie powoływać obiektu do życia tylko wywołać samą metodę typu 
bool? getElementOfTheSlownik(int kontrola), która zwróci nam konkretną wartość, zależność odwrócić. 

To akurat proste przykłady, temat sposobów zastąpienia instrukcji warunkowej się nie wyczerpał.A dlaczego poruszyłem? no cóż ciekawi mnie to czy faktycznie da się osiągnąć pełne zastąpienie IF-a.
Myślę, że zysk był by duży ale czy kod nie okazał by się zbyt skomplikowany, a nakład pracy większy niż spodziewany efekt?
Za nie długo się dowiem...


środa, 19 września 2018

Dependency Injection - naprawa (refaktoryzacja)

Wczoraj podzieliłem kontroler odpowiedzialny za rejestracje na kilka mniejszych (dokładnie dwa), jeden z nich dalej jest "popuchnięty". Kontroler ten służy do weryfikacji adresu email. W tym roku podczas rejestracji po raz pierwszy wprowadziłem weryfikacje adresu email, wzorowałem się na Allegro aby dokończyć transakcje otrzymujemy na podany adres mejlowy kod weryfikujący.
W poprzednich edycjach maratonu pojawiły się problemy z niewłaściwymi adresami email oraz z powtarzającymi się rejestracjami, aby temu zapobiec stworzona została weryfikacja zarejestrowanego zawodnika poprzez adres email oraz dodanie do bazy unikalności na trzy kolumny (imię, nazwisko,email).

Po wydzieleniu części kodu kontroler wygląda następująco:


public class VerificationEmailController : Controller
    {
        private readonly INewRecord _inew;
        private readonly IDataVerification _idata;
        private readonly IRandomNumber _irandom;
        private readonly ISMTP_Configuration _ismtp;
        private readonly IDescriptionVerificationNumber _idesc;
        private readonly ICheckWerification _icheck;
        private readonly IUniqueException _ithrow;
        private readonly IDataForDescription _idataDescription;
        private readonly IDescriptionEndCreate _iend;
        private readonly IExceptionBase _iexception;

public VerificationEmailController(INewRecord _inew, IDataVerification _idata, ISMTP_Configuration _ismtp, IDescriptionVerificationNumber _desc
, ICheckWerification _icheck, IRandomNumber _irandom, IExceptionBase _iexception
, IDataForDescription _idataDescription, IDescriptionEndCreate _iend, IExceptionBase iexception, IUniqueException _ithrow)
        {
            this._inew = _inew;
            this._idata = _idata;
            this._ismtp = _ismtp;
            this._idesc = _desc;
            this._icheck = _icheck;
            this._ithrow = _ithrow;
            this._iexception = _iexception;
            this._irandom = _irandom;
            this._idataDescription = _idataDescription;
            this._iend = _iend;
        }

Uznałem że nie będę pchał tu całego kodu, za duży bałagan, kontroler został wrzucony
Postanowiłem, że tam będę produkował kod poddawany refaktoryzacji ale nie tylko finalny efekt a postaram się wrzucać każdy krok. 


Pierwsze co się rzuca w oczy to za dużo interfejsów w konstuktorze oraz za dużo kodu.
Zmniejszamy kod.
  1. Utworzyłem nowy interfejs ISendingVeryfyingEmail do klasy SendingVeryfyingEmail która służy za wysyłanie mejla z numerem weryfikującym oraz ma za zadanie dopisać do tabeli kartoteka czasowa, że taki zawodnik próbuje się zarejestrować, w tej tabeli jest również czas rejestracji - limit ustawiony na 5 minut. Kod na  GitHub.
  2. Utworzyłem nowy interfejs ISendingConfirmationRegistrationEmail do klasy SendingConfirmationRegistrationEmail, która służy za przygotowanie i wysłanie mejla potwierdzającego rejestracje na maratonie  github.
  3. To nie koniec, uznałem że nowo powstała klasa SendingVerifyingEmail ma za dużo parametrów a dwa interfejsy ISMTP_Configuration oraz ICheckWerification zapakowałem do jednego miecha dzięki temu klasa SendingVerifyingEmail wykorzystuje trzy interfejsy zamiast czterech wartość ta dopuszczalna jest. Kod na github .
  4. Może by tak skończyć? nie, kolejnym krokiem mogła by być zmiana nazw klas bo są nie co mylące i podobne, po za tym dalej mam za dużo parametrów w kontrolerze, pięć.Teraz wygląda tak:
private readonly IDataVerification _idata;
        private readonly IUniqueException _ithrow;
        private readonly ISendingVerifyingEmail _isendingVer;
        private readonly ISendingConfirmationRegistrationEmail _isendingEnd;
        private readonly ICheckWerification _icheck;


        public VerificationEmailController( IDataVerification _idata,ISendingVerifyingEmail isendingE, IUniqueException _ithrow,
            ISendingConfirmationRegistrationEmail _isending, ICheckWerification _icheck)
        {
            this._idata = _idata;
            this._ithrow = _ithrow;
            this._isendingVer = isendingE;
            this._isendingEnd = _isending;
            this._icheck = _icheck;
        }
Zmniejszenie o 50% ale muszę dojść do trzech parametrów, teraz trochę bardziej dogłębna analiza.
Trochę się z tym re_fak_toringiem zamotałem, zmniejszyłem ilość metod w klasie CheckWerification z trzech na dwie, uznałem że to co wykonują te dwie można by wrzucić do jednej.
Miało się uprościć, a się skomplikowało (jak zawsze).

    5. Jednak się udało, a to z powodu nadużycia pierwszej zasady Solida (pojedynczej odpowiedzialności), czyli dziel z głową. Kod Github.

Pytanie czy to jeszcze działa pozostaje otwarte.

wtorek, 18 września 2018

Nadużywanie Dependency Injection - jak nie robić

Dziś zdałem sobie sprawę, że nadużywam wstrzykiwanie zależności bo jak można nazwać taki konstruktor:


public RegistrationController(IregistrationList registration, IDystans _dys, IGrupa _gr, IPlec _plec, ICreatingFilters _filters, INewRecord _inew, IDataVerification _idata, ISMTP_Configuration _ismtp, IDescriptionVerificationNumber _desc
,ICheckWerification _icheck,IRandomNumber _irandom,IExceptionBase _iexception
, IDataForDescription _idataDescription, IDescriptionEndCreate _iend, IUniqueException _ithrow, IFiltersOperations ifiltersOpereations, IFilterBool ifilterBool, IValueFilters ivalueFilter)

No faktycznie to pan pojechał (stworzyłem anty wzorzec (wstyd i hańba)).
Cóż z tym zrobić szybki rzut oka na kod, pierwsze rozwiązanie dzielimy kontroler na kilka mniejszych.
W tym kontrolerze mam:
  • rejestracje użytkownika,
  • validacje email,
  • lista zarejestrowanych,
  • dodawanie grupy kolarskiej. 
Dzięki temu będę miał mniej interfacjatów w konstruktorze. Inna kwestia to dotychczasowy zapis jest niezgodny z pierwszą zasadą Solid czyli zasadą pojedynczej odpowiedzialności, klasa RegistrationController odpowiada za cztery różne zadania (albo i więcej).

1. Podział kontrolera.
Usunąłem z kontrolera interfejsy dotyczące listy zarejestrowanych, przypomniałem sobie że stworzyłem klasę, która łączy w sobie wszystkie  filtry, trzy interfejsy zastąpiłem jednym, wydaje mi się że to wzorzec Fasada ale muszę się upewnić. Teraz konstruktor wygląda tak:

public RegistrationController(INewRecord _inew, IDataVerification _idata, ISMTP_Configuration _ismtp, IDescriptionVerificationNumber _desc, ICheckWerification _icheck, IRandomNumber _irandom, IExceptionBase _iexception, IDataForDescription _idataDescription, IDescriptionEndCreate _iend, IUniqueException _ithrow, ICreatingFilters _ifilters)

Trochę lepiej, teraz wyrzucę jeszcze walidacje email, to powinno mi dać o pięć interfejsów mniej.
Po usunięciu walidacji emaila konstruktor wygląda tak:

public RegistrationController( IExceptionBase _iexception , ICreatingFilters _ifilters)

2. Kolejny podział.
Nowo stworzony kontroler walidacja ( WerificationEmail) email przejął  część interfejsów po Registration kontroler jest ich stanowczo za dużo bo aż dziewięć, no cóż  należałoby zmniejszyć ilość, wydzielić kolejne zależności.

Podsumowując co spowodowało błąd zapomniałem o pierwszej zasadzie Solida, pojedynczej odpowiedzialności przez kontroler to też klasa!
Kolejne kroki to nic tylko wydzielanie zadań do nowych klas, z jednej strony mam więcej kontrolerów ale zakres ich odpowiedzialności jest znacznie mniejszy są dużo prostsze. Ilość widoków nie uległa zmianie, zależą od  akcji w kontrolerach, teraz są przynajmniej poukładane w katalogi odpowiednie.

poniedziałek, 10 września 2018

Maraton Rowerowy 2019 - kolejne zadania

Ostatnio tempo prac nie co zwolniło.

  • Jest działająca rejestracja (brak walidacji dwóch list oraz regulaminu , to jeszcze do poprawki), można się rejestrować testowo,  czekam tylko na zatwierdzenie przez komandora regulaminu 2019 co trwa już długo, a bez tego rejestracja jest niemożliwa.
  • Lista zarejestrowanych działa, filtry paging również
  • Nowe menu chociaż ubogie to jednak coś tam widać.
  • stara strona już tylko jako historia,
Zmiany w regulaminie
  • 200 km ma zmniejszony limit do 8 godzin i tym samym startuje godzinę później,
  • ceny wszystkich dystansów zwiększone o 10 zł z wyjątkiem 50 km gdzie 10 zł w dół,
  • 150 km limit 7 godzin 30 minut, dystans ten wchodzi do maratonu na stałe
Co teraz?
Pracy jeszcze dużo przede mną, najważniejsze to:
  • Zrobienie logowania do panelu administracyjnego (dawno tego nie robiłem, właściwie nigdy takiego porządnego),
  • Zrobienie samego panelu w którym będzie start zawodników,  zarządzenie danymi (opłata, przydzielenie numeru startowego, wpisanie do list startowych, wydruk listy, wydruk wyniku)
  • program do odczytu z czytnika, tu mam myśl żeby to na androidzie zrobić ale to jest na razie taki pomysł, człowiekiem leniwym jestem więc może mi się nie chcieć (ale coś nowego było by fajne noooooo).
  • Trzeba też pomyśleć nad rejestracją biurze zawodów.
Sporo tego dużo pracy przede mną.

czwartek, 6 września 2018

Wyrazy trudne do zrozumienia

Na początku "Nowego otwarcia" na programowanie pojawiły się różne dziwne hasła (którymi oblepiłem kalendarz, monitory komputer, całe biurko), hasła których poznanie i zrozumienie stało się celem.
Na pierwszy ogień poszło:
  • SOLID - dobre praktyki, znam i staram się stosować, dobra podstawienie Liskov mniej znane, przeczytałem czysty kod, przejrzałem dwa pokaźne tutoriale na plulajsajcie, przeczytałem mnóstwo artykułów aby w końcu zimplemntować to w aplikacji która  nie była pisana w tym stylu co wiązało się z wieloma zmianami działającego kodu, przekleństwami wyrywaniem włosów z głowy itp...Teraz jest trochę lepiej, Maraton piszę w takiej formie,
  • OOP - programowanie zorientowane obiektowo, bez obiektówki Solid nie istnieje ale trzeba było sobie trochę poprzypominać,
  •   DRY - przy okazji tutoriala  z Solida poznałem to coś czyli usuwanie powtarzającego się kodu, też staram się stosować Ostatnio czytałem, że są odstępstwa od tej zasady, 
  •  YAGNI, KISS - kolejne zasady ta pierwsza to coś o nie pisaniu kodu na zapas. Dziwne ale pamiętam, że takie rzeczy robiłem -mówiłem sobie- przyda się na zaś taki kod ("On jest złyyyy" jak mówi Mort),
  •  Don`t be STUPID - i tu kolejna zasada a właściwie zbiór zasad których nie powinno się stosować, ostatnio to odkryłem jest też taka ładna czerwona grafika (powiesiłem na kalendarz) link do artykułu STUPID.  
  • CQS i CQRS - CQS to właściwie wzorzec projektowy to drugie jak się do wiedziałem jest jego rozszerzeniem. CQS-a staram się stosować to rozbicie metod, na metody command które zmieniają stan obiektu ale nie zwracają wartości oraz query które zwracają wartość ale nie zmieniają stanu. Przy okazji implementacji tego wyszły pewne problemy czasami zdarzają się wyjątki. CQRS przeniesienie wzorca CQS poziom wyżej nie znam tego jeszcze chce poznać, trochę poczytałem i wydaje się interesujące,
  •  Programowanie kontraktowe wczoraj to znalazłem w książce pragmatyczny programista, zainteresowało mnie
  • TDD - programowanie przez testy jednostkowe - przygotowuje się do tego, na razie słabo idzie ale będzie lepiej, 
  • ROT - rozszerzenie DRY
Pewno znalazło by się jeszcze kilka haseł, na razie żadne nie przychodzą do głowy. To co napisałem to i tak sporo jak to dobrze poznam i nauczę się stosować to na pewno będę lepszy.
Wczoraj też do wiedziałem się strasznej prawdy, przypuszczałem że ona jest ale autor książki pragmatyczny programista dobitnie ją wykazał
PISANIE DOSKONAŁEGO OPROGRAMOWANIA JEST NIEMOŻLIWE.
 Cóż wszystkie zasady prowadzą do doskonałości kodu której nigdy nie osiągniemy tzn, że nie warto? świadomość, że kiedyś ktoś znajdzie mój kod i zrozumie go, będzie dla mnie największą satysfakcją i nagrodą za stosowanie i naukę tych zasad. (Czasami to sam nie rozumie własnego kodu)

Jak poznam nowe wyrazy nie zrozumiałe to wpiszę je tutaj.

Pomiar czasu - algorytm cz3 - założenia (rozważania)

Rozważania nad pomiarem czasu kolejne starcie. (nie zawsze potrzebne i sensowne)

Mógłbym przy rejestracji czasu zwracać uwagę na dystans przypisany do zawodnika, dlaczego? dzięki temu wiem ile powinien mieć odbić (zarejestrowanych czasów).
Dla przykładu zawodnik który jest zapisany na 100 km:
  1. pierwszy wpis - start - Mszana,
  2. drugi wpis wpis - punkt kontrolny - Bojanów,
  3. trzeci wpis - meta - Mszana
Dla zawodnika, który jedzie na 150 km (nowy dystans w Mszanie, historycznie już był):
  1. start - Mszana,
  2. punkt kontrolny - Bojanów,
  3. punkt kontrolny - Mszana,
  4. meta - Mszana.
Dla zawodnika jadącego na 200 km:
  1. start - Mszana,
  2. punkt kontrolny - Bojanów,
  3. punkt kontrolny - Mszana,
  4. punkt kontrolny - Bojanów,
  5. meta - Mszana 
  Dla zawodnika jadącego na 50 km:
  1. start - Mszana,
  2. meta - Mszana
Dla dystansów powyżej 200 km rejestracje czasu odbywają się według schematu 200 km, wyjątkiem jest dystans 750 km gdzie dochodzi krótka pętla 50 km która nie ma punktu kontrolnego (w Bojanowie).
Być może pośrednim rozwiązaniem (tudzież ułatwieniem) było by dołożenie do tabeli z dystansami kolumny z minimalną ilością rejestracji czasu. Są jednak pewne uwagi co do powyższego rozwiązania:
  1. jeżeli zawodnik nie przejedzie swojego dystansu?, może się tak zdarzyć i się zdarza, ktoś wybrał dystans np 600 km, a przejechał tylko 200 km,
  2. Zawodnik zapomniał się odbić na punkcie lub zdarzyła się nieprzewidziana awaria, 
Częściowym rozwiązaniem jest traktowanie informacji o ilości odbić jako informację pomocniczą , a nie warunek, tzn
  • Szukamy w tabeli Czas rejestracji dla danego zawodnika ,
  • wyliczamy czas całkowity (końcowy) odejmując od ostatniego pierwszy,
  • zliczamy odbicia,
  • porównujemy ilość odbić z tabelą dystanse w której mamy dodatkową kolumnę określająca minimalną ilość rejestracji czasu,
  • w tym momencie mamy jakąś wiedzę na temat jaki jest czas końcowy i ile odbić składa się na ten czas czyli ile przejechał (jaki dystans)
 Można by algorytm rozbudować nie sprawdzam na jaki dystans zapisany jest zawodnik a jedynie sprawdzam ilość rejestracji uzyskuje jaki to jest dystans, sprawdzam limit czasu, wtedy przypisuje zawodnika do konkretnego dystansu bez względu na to jaki dystans wybrał przy rejestracji.

Jak sprawdzić odcinek czasu (wyliczyć)?
Na dystansie 50 km  nie ma problemu  bo i tak będę brał po uwagę pierwszy i ostatni wpis. Problem pojawia się już na dystansie 100 km, mamy trzy rejestracji , jeżeli zawodnika ma dwie to świadczy o tym że się nie odbił czy można na to przymknąć oko? Zależy, jeżeli czas przejazdu jest porównywalny z innymi ale jak się to ma do regulaminu? regulamin mówi brak odbicia dyskwalifikacja lub brak klasyfikacji na danym dystansie (dylemat).
Jak powiedzieć człowiekowi, który przejechał 600 km, że raz się nie odbił i jest zdyskwalifikowany?
Przy nowym algorytmie wiedza na temat  pojedynczych odcinków jest drugorzędna, liczyć się będzie tylko pierwszy i ostatni czas dzięki któremu będę miał całkowity czas przejazdu.

Wczoraj podczas jazdy samochodem w padła mi myśl do głowy (na dziurze). Jak zawodnik odbija się  na dowolnym punkcie, do bazy wpada jego czas, mam czas startu i czas ostatniego odbicia. Dzię ki temu mogę wyliczyć czas przejazdu. Nie potrzebuje budować skomplikowanego algorytmu, wystarczy że po każdym odbiciu będę liczył różnicę (ostatni minus pierwszy) czasów i zapisywał ją do tabeli z wynikami dla danego zawodnika.
Dzięki temu otrzymam czasy na daną pętle, a nawet jej część.
Wydaje się to bardzo proste, struktura tabeli wyglądała by w ten sposób:
  1. wynik_id - wiadomo  klucz główny (PK) auto inkrementacja,
  2. wynik_wartosc - wynik różnicy czasów kolumna time,
  3. zawodnik_id - (FK) - klucz obcy tabeli zawodnik,
  4. dystans_prawdopodobny - ta kolumna może nie odzwierciedlać rzeczywistości. 
Jeszcze małą uwago spomaniało mi się, z ustaleniem faktycznego dystansu może być problem więc kolumna dystans_prawdopodobny będzie się inkrementować po każdym wyliczeniu czasu, to powinno dać przybliżony dystans, który i tak będę musiał wyliczyć gdyż odbić jest więcej niż dystansów.
Wyszukiwanie czasów dla zawodnika nie wymagało by żadnego liczenia ani skomplikowanie go zapytania


select Z.zaw_nazwisko,Z.zaw_imie,NS.numer_zawodnika,W.wynik_wartosc  Wynik W
inner join Zawodnik Z on Z.zaw_id = W.zawodnik_id
inner join NumerStartowy NS on NS.numer_id = Z.numer_id
where
      zaw_id = @zaw_id
order by W.wynik_id asc

Do tego informacje o dystansie wybranym przy rejestracji, a dodatkowo dystans rzeczywisty na podstawie rejestracji. 
Spomniało mi się jeszcze jedno można by się pokusić o sprawdzenie numeru czytnika tzn. zawodnik odbija się na czytniku ja wpisuje numer dyskietki do bazy oprócz tego można by spróbować ściągnąć numer urządzenia, dzięki temu bym był w stanie określić na którym odbił się dwa razy. Było by to dodatkowe zabezpieczenie (oprócz zwłoki 30 minut).

 Po wielu miesiącach przerwy Czteroletnim exodusie do bloga ismartdev, który zdechł w zeszłym roku w listopadzie, na powrót wstąpiłem w ten ...