Bomba czasowa - przydatna rzecz
Czasami zdarza się scenariusz podobny do następującego. Oto jesteś jednym z uczestników sporego projektu. Pracuje w nim blisko 40 programistów oraz 8 analityków. Ty piastujesz w nim niewdzięczną rolę lidera jednego z zespołów developerskich. Razem z Tobą poci się w nim 9 developerów. Nie jest to projekt typu waterfall, gdzie do developmentu trafiają wymuskane, ostateczne wersje dokumentów analitycznych, których zmiana wymaga uruchomienia straszliwej maszynerii pod nazwą "procedura zarządzania zmianą". W tym projekcie panuje agile. A ściślej coś, co pod wieloma względami przypomina Scruma.
W projekcie panuje więc pewnego rodzaju chaos, który całkiem nieźle jest kontrolowany. Jednakże liczba wątków jest naprawdę duża. Oto przykład. Właśnie jesteś w środku iteracji. Twój zespół skupia się na zaimplementowaniu funkcjonalności wystawiania ofert handlowych. Kilka dni wcześniej otrzymaliście od analityka dokument pt. "Oferty". Brzmi nieźle - fajnie, że w ogóle powstał (wiele projektów bowiem, wciąż nie ma w swoim składzie analityków).
Jest jednak pewne ale... otóż dokument ten jest w wersji... 0.1. Oznacza to, że nie nadaje się nawet do tego, aby przejrzał go inny analityk z zespołu projektowego - o pokazaniu go klientowi, w ogóle nie ma mowy. Musicie jednak zacząć pracę nad tym papierem, ponieważ termin projektu jest nieprzekraczalny i w zasadzie nieosiągalny - więc, jak mawiał Jack Aubrey, "nie ma chwili do stracenia".
W minionych trzech dniach w Twojej skrzynce mailowej pojawiły się nowsze wersje dokumentów analitycznych, które Twój zespół zaimplementował już jakiś czas temu - będzie więc trzeba pochylić się nad tymi dokumentami jeszcze w tej iteracji, żeby rozplanować prace nad nimi w czasie najbliższego Sprint Planning Meetingu.
Dodatkowo nęka Cię lider zespołu testerów, który właśnie próbuje z klientem zamknąć odbiory modułu konfigurowalnego cennika. Ty i Twój zespół męczyliście się nad tą funkcjonalnością przez trzy miesiące, ale... było to pół roku temu. Niestety dział QA nie miał wówczas mocy przerobowych, żeby zająć się testami tego modułu. Dopiero teraz testerzy zaczęli się nad nim pastwić.
Dodatkowo Twoje myśli i czas zaprzątają różnego rodzaju zagadnienia takie jak: problemy architektoniczne, różnego rodzaju bezsensowne spotkania, ciągłe rozmowy rekrutacyjne, które prowadzisz w celu pozyskania kolejnych mózgów do zespołu, błędy i nieścisłości w dokumentach analitycznych, które musisz wyjaśnić z analitykami (masz tutaj naprawdę długą listę nieścisłości i pytań) itd. itd. Masz już za sobą kilka lat doświadczenia zawodowego, więc wiesz, że niektóre z tych zagadnień będziesz musiał sobie odpuścić z braku czasu, a o niektórych zapomnisz i wrócą do Ciebie w postaci ticksa w systemie rejestracji i śledzenia zgłoszeń (takim jak np. Redmine). Nie chcesz, żeby tak było - wiesz jednak, że tak będzie.
I oto więc, kiedy tak sobie rozmyślasz nad marnym losem syna Twojego ojca, jeden z developerów w Twoim zespole podchodzi do Ciebie i pokazuje Ci fragment dokumentacji analitycznej (wspomniany już wcześniej dokument "Oferty", wersja 0.1), w której znajduje się taki zapis:
- W chwili tworzenia oferty handlowej, użytkownik systemu ustawia na niej warunki płatności. Do dyspozycji ma dwa parametry:jaki procent całkowitej kwoty oferty zamawiający musi zapłacić dostawcy zanim ten wyśle towar zamawiającemu (tzw. przedpłata)
- jaki procent całkowitej kwoty oferty zamawiający musi zapłacić dostawcy po odbiorze towaru (tzw. płatność po odbiorze towaru)
- TODO: Uwaga! Dla zamawiających, którzy w historii współpracy mają opóźnienia w płatnościach, system nie pozwala na podanie wartości innej niż 0% dla parametru drugiego; w obecnej chwili nie wiadomo jednak, jak dokładnie taki algorytm powinien wyglądać
Nie do końca wiesz, co z zrobić z tym TODO. Oczyma wyobraźni architektonicznej widzisz taki oto diagram klas i taki oto kod.
Na diagramie mamy dwie klasy:
Offer
- opisuje obiekty reprezentujące oferty orazCompany
- opisuje obiekty reprezentujące firmy.
Klasa Offer
ma pole recipient
typu Company
, które zawiera informację do kogo oferta jest skierowana. Posiada również metodę updatePaymentTerms
, która służy do ustawiania warunków płatności. Metoda przyjmuje dwa argumenty: wysokość przedpłaty oraz wysokość płatności po odbiorze towaru.
Klasa Company
ma metodę hasBeenLateWithPayment
, która zwraca informację, czy w historii kontaktów z daną firmą zdarzały się sytuacje, w których firma ta zalegała z płatnością.
Tak więc kod tych klas będzie wyglądał następująco:
public final class Offer {
private final Company recipient;
private int prepayment;
private int paymentAfterAcceptanceOfGoods;
// Pozostałe pola, konstruktory i metody pominięte
// ze względu na zwięzłość
public void updatePaymentTerms(
final int prepayment,
final int paymentAfterAcceptanceOfGoods) {
// walidacja argumentów i stanu klasy pominięte przez zwięzłość
// o co chodzi z tą walidacją? zajrzyj tutaj
if ( recipient.hasBeenLateWithPayment() ) {
if ( paymentAfterAcceptanceOfGoods != 0 ) {
throw new IllegalArgumentException();
}
}
this.prepayment = prepayment;
this.paymentAfterAcceptanceOfGoods = paymentAfterAcceptanceOfGoods;
}
}
public class Company {
// Pozostałe pola, konstruktory i metody pominięte
// ze względu na zwięzłość
public boolean hasBeenLateWithPayment() {
final boolean result = false;
// Hmm... brakuj informacji jak to zaimplementować
return result;
}
}
Myślisz o tym, co zrobić z metodą hasBeenLateWithPayment
w klasie Company
. Rozważasz dwie opcje.
Pierwsza - można zostawić ten kod tak, jak wygląda teraz (czyli metoda zawsze zwraca false) i po prostu czekać na to, aż ktoś, kiedyś (może) wykryje ten błąd i założy zgłoszenie w issue trackerze. Oczywiście nawet jeżeli takie zgłoszenie powstanie, to nie ma pewności, że zostanie ono zauważone. Ty sam, jesteś wręcz pewien, że zostanie przeoczone.
Już w tej chwili zgłoszeń jest kilkaset, a do tego klient wszystkie swoje zgłoszenia oznacza jako krytyczne (nawet te dotyczące błędów ortograficznych w komunikatach dla użytkownika systemu). Nie łudzisz się więc, że zgłoszenie to będzie się w jakikolwiek sposób wyróżniało. Tak więc spodziewasz się, że Wasz system trafi na produkcję z tym falsem i że ładnych parę ofert, wygenerowanych w kilkudziesięciu filiach Twojego klienta na całym świecie, pójdzie do kontrahentów Twojego klienta z niedopuszczalnymi warunkami płatności. Wiesz, że jeżeli do tego dojdzie, będzie niezła chryja i wiele zarwanych nocy.
Drugie rozwiązanie to rzucać tutaj wyjątek... ale wówczas w ogóle nie będzie możliwe tworzenie ofert, w których wartość płatności po odbiorze byłaby większa niż 0%. Fatalnie! Zablokuje to bowiem możliwość testowania szeregu bardzo skomplikowanych przypadków użycia takich jak np.:
- tworzenie faktur zaliczkowych i faktur końcowych
- automatyczne wystawianie upomnień do kontrahentów, którzy otrzymali towar ale za niego nie zapłacili
- księgowanie przedpłat i płatności po odbiorze w module księgowym systemu.
Nie możesz więc zablokować tej funkcjonalności rzucając tam wyjątek. Masz jednak do dyspozycji inne rozwiązanie. Myślisz sobie tak:
Uruchomienie produkcyjne mamy 1 stycznia, czyli... policzmy... zostało nam jakieś dziewięć miesięcy. Dam nam więc wszystkim trzy miesiące na zaimplementowanie tej funkcjonalności, a gdyby jednak okazało się, że czas minął, to wysadzę w powietrze możliwość wystawiania ofert w tym za...nym systemie. Na pewno ktoś wówczas zauważy, że trzeba jak najszybciej pochylić się nad tematem zamawiających, którzy w swojej historii mają opóźnienia w płatnościach.
Oto więc, co robisz:
- Mówisz developerowi, który przyszedł do Ciebie po radę, żeby założył w issue trackerze zgłoszenie. Zgłoszenie ma mieć taki oto tytuł: "Błąd: istnieje możliwość tworzenia ofert z niezerową wartością płatności po odbiorze towaru dla zamawiających z historią opóźnień w płatnościach". W szczegółach zgłoszenia opisujesz, że to dlatego, że nie ma na ten temat dokumentacji analitycznej.
- Informujesz o istnieniu tego zgłoszenia analityka z Twojego zespołu i przypisujesz do niego to zgłoszenie. Ten, kiedy o tym słyszy, wścieka się - twierdzi, że nie jest to jego wina tylko klienta, który nie ma czasu o tym porozmawiać. Myślisz sobie: "to nawet lepiej!". Sugerujesz swojemu koledze analitykowi, żeby przepisał to zgłoszenie do kierownika projektu po stronie klienta z prośbą, o jak najszybsze zorganizowanie spotkania w tym temacie. Tak więc zgłoszenie leży teraz u klienta.
- Teraz prosisz swojego kolegę developera, żeby wyrzeźbił taki oto kod:
public class TimeBomb {
private TimeBomb() {
throw new UnsupportedOperationException("Nie waż się tego "
+ "konstruktora wywoływać... "
+ "nawet za pomocą refleksji łobuzie jeden!");
}
public static void blowUpOn(final LocalDate date,
final String message) {
// walidacja argumentów i stanu klasy pominięte przez zwięzłość
if (LocalDate.now().isAfter(date))
throw new RuntimeException(message);
}
}
... i poprawił implementację metody hasBeenLateWithPayment
w następujący sposób:
public boolean hasBeenLateWithPayment() {
final boolean result = false;
TimeBomb.blowUpOn(LocalDate.of(2020, 5, 31),
"Co się stało? Zajrzyj do zgłoszenia ACME-871");
return result;
}
Co takiego robi ten kod? Sprawa jest prosta. Metoda hasBeenLateWithPayment
będzie zwracała false, ale tylko przez trzy miesiące, a dokładnie do 31 maja 2020 roku włącznie. Od 1 czerwca 2020 roku będzie generowała wyjątek.
Wyobraźmy więc sobie, że oto nadszedł 1 czerwca 2020 roku, a świat zapomniał o tym, że należy określić, jak ma zachowywać się metoda hasBeenLateWithPayment
. Co się wówczas stanie? Może po prostu opowiem Ci, co się stało ostatnio, kiedy taka bomba, założona na moje polecenie, wyleciała w powietrze w pewnym projekcie. Nie był to mały projekt, skoro w jego kick-offie brało udział trzech ministrów, doradca Premiera i... jeszcze jakieś sto osób - a jednak nie baliśmy się umieścić w tym projekcie bomby czasowej. Znacznie bardziej baliśmy się wejść z bugiem na produkcję.
Oto więc pewnego dnia, na 3 miesiące przed uruchomieniem produkcyjnym, dostałem telefon od kierownika projektu z informacją, że "nic nie działa". Była 7:30 rano. Klient właśnie rozpoczął kolejny dzień testów systemu, ale niestety tego dnia, jedna z kluczowych funkcjonalności kończyła się błędem w 100% przypadków. Zapytałem co się dzieje. Kiedy przeczytał mi opis, który wyskakiwał użytkownikom na ekranie, od razu wiedziałem, że w powietrze wyleciała bomba. Poprosiłem, żeby zerknął do issue trackera pod wskazany w komunikacie numer.
Okazało się, że chodzi o zgłoszenie, która założyliśmy klientowi dwa miesiące wcześniej. Pomimo kilku naszych monitów, w strukturach klienta nie znalazł się nikt, kto chciałby wziąć odpowiedzialność za poruszony w zgłoszeniu problem. Pracownicy klienta woleli raczej przerzucać się tym zgłoszeniem niczym gorącym kartoflem - no i się doigrali.
W rozmowie z kierownikiem, ustaliliśmy, że bombę przesuniemy o dwa tygodnie do przodu. Prosta sprawa - wystarczyło zmienić jedną linijkę kodu i zainstalować nowszą wersję systemu na środowisku testowym klienta - 30 minut po odłożeniu słuchawki apka znów działała bez problemów. Efekt jednak osiągnęliśmy. Swąd po wybuchu obiegł niemal każdy zakamarek budynków ministerialnych i jeszcze tego samego dnia klient wyznaczył osobę odpowiedzialną za rozwiązanie poruszanego w zgłoszeniu zagadnienia - w zaledwie pół dnia osiągnęliśmy efekt, którego nie udało nam się osiągnąć przez minione dwa miesiące... a dwa tygodnie później funkcjonalność była już w masterze - nieźle, jak na dwie linijki kodu.
Faktem, wartym wspomnienia, jest to, że kierownictwo projektu ze strony klienta nie miało do nas w ogóle pretensji o zaistniałą sytuację. Wręcz przeciwnie. Dzięki bombie osiągnęło efekt, którego prośbami i naciskami samo nie mogło osiągnąć. Dopiero bowiem kiedy system przestał realizować jedną z kluczowych funkcjonalności i sparaliżował proces testowania aplikacji, właściciel biznesowy systemu potraktował zgłoszenie na poważnie, zabrał się za nie do pracy i określił kształt algorytmu, który mieliśmy zaimplementować.
A gdyby bomby nie było? Pewnie weszlibyśmy na produkcję z poważnym bugiem. W najlepszym wypadku, dzięki testerom, dowiedzielibyśmy się o jego istnieniu na kilka dni przed wdrożeniem produkcyjnym. Scenariusz wyglądałby wówczas zupełnie inaczej, znacznie mniej dla nas przyjemnie - w oczach klienta, bowiem, to my bylibyśmy winni zaistniałej sytuacji.
Słowo ostrzeżenia
Bomba czasowa to przydatne narzędzie, ale.... nie należy go nadużywać, to nie reminder. Jeżeli co dziesięć linii kodu będziesz umieszczał bombę i każdego dnia w Twoim systemie dojdzie do 20 wybuchów, technika ta stanie się całkowicie bezużyteczna. Jedynym jej efektem będzie to, że codziennie będziesz tych 20 bomb przesuwać o kolejne tygodnie do przodu... machinalnie - aż pewnego dnia (a będzie to na dwa dni przed uruchomieniem produkcyjnym) wybuchnie Ci trzy tysiące bomb i przed oczyma stanie Ci taki oto, dobrze znany napis: GAME OVER.