Jeszcze słowo o TDD

O TDD napisano wiele, sam napisałem całkiem sporo i mówiłem całkiem sporo podczas kilku prelekcji. Używam TDD od ponad 5 lat już. Powinienem być super mega ninja pro TDD master. Mimo tego jakiś czas temu, pisząc bardzo prosty kod, na prawdę super prosty, naszła mnie taka refleksja:

Jest zielone, jest ok.

[Fact]
public void Example_test()
{
    _pinger.Ping().Returns(c =>
    {
        throw new Exception();
    });
    Assert.DoesNotThrow(()=>_monitor.CheckAvaliability());
    _destinationSystem.Received(1).Ping(Arg.Is<IDiagnostics>(diag => diag.State == ""));
}

Powyższy kawałek kodu używa NSubstitute (.Ping().Returns(c=>…..) do mockownia. Oraz XUnit-a do testów. Napisałem test jak wyżej, napisałem kod. Niby wszystko zielone, niby ok. Jakimś cudem jednak pokusiło mnie, żeby dorzucić AutoData. AutoData to taki sprytny atrybut, który pozwala generować dane testowe. W moim przypadki Message z wyjątki ma znaczenie, dlatego stwierdziłem, że Message można spokojnie generować – tutaj kod jest taki, że mam pudełko, które jak zrobi coś złego to to złe ma wyjść z drugiej strony – case dla AutoData jak malowany.

[Theory,AutoData]
public void Example_test(string exceptionMessage)
{
    _pinger.Ping().Returns(c =>
    {
        throw new Exception(exceptionMessage);
    });<
    Assert.DoesNotThrow(()=>_monitor.CheckAvaliability());
    _destinationSystem.Received(1).Ping(Arg.Is<IDiagnostics>(diag => diag.State == exceptionMessage));
}

No i zonk!!! Test już nie bardzo to przechodzi. Pomyślałem, bez jaj, przecież implementacja jest tak prosta, że tego nie da się zepsuć – nawet nie wiem po co pisałem na to test, ale nie uprzedzajmy faktów…. Debug testu i….. okazało się, że jednak w kodzie gdzie łapię wyjątek, to oprócz logowania tak jak to powinno być to już message-a  nie przepisuję. Kto by pomyślał.

Testy jednostkowe nie dają 100% gwarancji, że powstanie kod bez bugów, nie dają nawet jakiejkolwiek gwarancji, co jednak dają, to dużo wyższą szansę, że powstały kod będzie miał mniej bugów niż bez testów. W moim przypadku, okazało się, ze prosta refaktoryzacja – lub bez AutoData dodanie kilku przypadków testowych, pozwoliła złapać buga zanim kod poszedł na produkcję.

Czytelny kod to przyjemna praca

Chlebem naszym powszednim jest kod zatem niech będzie on prosty, czytelny, po prostu smaczny. Czasem 3 minuty wystarczą aby kod był bardziej czytelny a co za tym idzie łatwiejszy do utrzymania w przyszłości. Do napisania tego wpisu natchnęła mnie poniższa linijka kodu*

var p = new CacheItemPolicy {
    SlidingExpiration = new TimeSpan(0,0,2,0)
};

Niby jest czytelnie, niby wiadomo o co chodzi… ale można lepiej:

var policy = new CacheItemPolicy{
    SlidingExpiration = 2.Minutes();
}

Czyta się lepiej. Dla uściślenia ten kawałek kodu znajduje się w funkcji

private void CacheViewResultForTwoMinutes(.....){
    ...
}

i już wiadomo co autor miał na myśli. Nie potrzeba komentarza, ot nazwa funkcji jest wystarczającym komentarzem. Co ważne, to jest metoda prywatna więc mogę ją bezkarnie zmieniać choćby przy każdym buildzie; A wracając do meritum, Minutes to funkcja która konwertuje inta na odpowienią ilość minut zatem zaglądnijmy do środka:

namespace Rekord.ZOS.Lider2.Service.GatewayCommunication.Queries
{
  public static class IntExtensions
  {
    private const int SecondsInMinute = 60;

    public static int MinutesInSeconds(this int minute)
    {
       return minute*SecondsInMinute;
    }

    public static TimeSpan Minutes(this int value)
    {
       return TimeSpan.FromSeconds(value.MinutesInSeconds());
    }
  }
}

Takie proste zabiegi powodują, że kod jest dużo bardziej czytelny niż początkowe new TimeSpan(0,0,2,0) od którego to się zaczęło. Prosta refaktoryzacja spowodowała, że teraz wszędzie gdzie mówimy o czasie możemy napisać 20.Minutes() i już wiadomo że chodzi o 20 mintu – bez wnikania co autor miał na myśli. Jakby ktoś kiedyś zapomniał co to minuta i ile ma sekund to też może sprawdzić 😉

A teraz najważniejsze, wszystko po to aby napisać w funkcji


...

...

CacheViewResultForTwoMinutes(...,...,...);

..

..

zamiast powiększać funkcję zbędnymi detalami implementacyjnymi:


...

...

...

var p = new CacheItemPolicy {
 SlidingExpiration = new TimeSpan(0,0,2,0)
 };
 MemoryCache.Default.AddOrGetExisting(key, view, p);

...

...

*) tak, wiem, że można napisać TimeSpan.FromMinutes(2). Celowo nie zostało to tutaj użyte, żeby zobrazować pewien mechanizm.

Z długiem czy bez długu

Z tematu dług technologiczny zrobił się całkiem spory cykl. Mimo, że nie wszystkie aspekty zostały poruszone, to myślę, że poruszone zostały wszystkie najważniejsze jego aspekty zatem nadszedł czas na odpowiedzenie sobie czy da się realizować projekty bez długu.

Odpowiedź krótka brzmi NIE.

Jeśli w każdym aspekcie będziemy korzystali z wszystkiego NAJ to przy dzisiejszym tempie rozwoju okaże się, że nie robimy nic oprócz zmian wersji narzędzi, bibliotek, framework-ów, wrzucaniu coraz to nowych lepszych języków i innych tego typu wynalazków. Projekt sam w sobie nie będzie szedł za bardzo do przodu – o ile w ogóle. Cała sztuka z długiem technologicznym polega na tym, że po pierwsze primo MUSIMY być świadomi takiego zjawiska a po drugie primo MUSIMY nim jakoś rozsądnie zarządzać. Tak jak banki zarządzają długiem, tak jak kraje zarządzają długiem tak i my programiści powinniśmy zarządzać swoimi długami.

Nie wszystko co najlepsze na rynku możemy wykorzystać, podstawowy hamulec to nasi klienci. Jeśli kilku strategicznych klientów korzysta ze starych systemów operacyjnych, na których nasze nowe zabawki nie będą działać to chcąc nie chcąc nie przeskoczymy tego.

Jeśli goni nas termin (czy to prezentacji czy wdrożenia czy też po prostu kończą się pieniądze na projekt) to nie ma sensu wrzucać nowych wynalazków.

Jeśli nie mamy odpowiednich ludzi, którzy są wstanie w miarę rozsądnie rozpoznać nową technologię, to nie ma sensu w nią wchodzić.

W każdym momencie projektu warto natomiast wiedzieć gdzie jesteśmy do tyłu i wiedzieć dlaczego. W każdej chwili powinniśmy być również przygotowani na zmianę jeśli znikną nasze hamulce.

W przypadku bibliotek zewnętrznych i kontrolek oraz wszelkich kanałów komunikacji (programu) ze światem zewnętrznym wydaje się idealnie sprawdzać architektura a ’la Robert Martin. Mianowicie, co nie zależy w 100% od nas powinno być odizolowane – zapakowane w odpowiednie pudełko. To pozwoli nam zmienić zawartość tych pudełek w sytuacji gdy elementy blokujące nas znikną. Myślę, że zarówno nasz klient jak i kierownik czy prezes będą woleli usłyszeć że wymiana bazę danych na inną będzie wymagała miesiąca pracy zamiast 2 lat – lub w skrajnej sytuacji przerobienia całego projektu od nowa.

Wiedza o długu pozwala nam również go zaciągać świadomie. Moim zdaniem nie wolno, pod karą chłosty klawiaturami, iść na skróty w częściach korowych systemu ale już w detalach np. interfejsu użytkownika (KTÓRY NIE POSIADA LOGIKI APLIKACJI I LOGIKI BIZNESOWEJ!!!), możemy sobie czasem pozwolić na gorszą jakość. Pójście na skróty w pierwszym przypadku będzie miało brzemienne skutki dla całego systemu, w drugim bardziej lokalne. Jeśli w obu sytuacjach pójście na skróty oznacza zysk 5h to gdzie lepiej zostawić małe lokalne piekiełko?

Od tej pory bardziej świadomi powinniśmy podejmować bardziej świadome decyzje. Jeśli macie jakieś przemyślenia na ten temat, zapraszam do dyskusji.

Dobrze napisany kod nie powinien wprowadzać w błąd

Kod powinien robić to czego się po nim spodziewamy, tego uczy SOLID a w szczególności LSP. Również funkcje powinny robić to co mówi ich nazwa. To ułatwia pracę z kodem a jeśli nie zgadzasz się z tym to popatrzmy na taki kod:

class car {
    int wheels = 4;
    string engine;
}
car mybike = new car();
mybike.wheels = 2;
mybike.engine = null;
Niby kod prawidłowy, kompilator nie będzie marudził ale…
… skąd biedny programista ma wiedzieć, że:
var someCar = mybike;
w zmiennej someCar tak naprawdę jest rower? Taki kod może kusić napisanie takiego potwora:
var isBike = false;
if(someCar.wheels == 2 && someCar.engine == null){
isBike = true;
}
.... a potem to już tylko gorzej
Powyższy przykład z mybike znalazłem na StackOverflow tutaj i moim zdaniem bardzo dobrze obrazuje, dlaczego kod powinien robić to czego się po nim spodziewamy. Dodatkowa ukryta logika to nic innego jak miejsce do pojawiania się błędów.