logo

Jak hackować brak doświadczenia optymalizując pod kątem zmiany

Długość wpisu: 12 min

W poprzednim wpisie pisałem o tym, że zmiana jest najważniejszą rzeczą w programowaniu, bo wszystko się wokół niej kręci.

Nie mamy innego wyboru niż tylko jej oczekiwać.

Wszystkie nowoczesne metodyki oraz dobre praktyki tworzenia oprogramowania powstały po to, żeby zoptymalizować pracę pod kątem wprowadzania zmian.

Jednak każda dobra praktyka ma swój koszt.

📝 Note: Jeśli nie czytałeś poprzedniego wpisu, to zrób to koniecznie zanim przeczytasz ten.

Koszt stosowania dobrych praktyk

Żeby zobrazować problem kosztu dobrych praktyk, posłużę się przykładem z mojego ostatniego szkolenia, gdzie wyjaśniałem zasady SOLID na przykładzie JS-a i Reacta.

Przy okazji jest to pierwszy post, w którym pokazuję jakiś kodzik. Uczcijmy to minutą tańca 🕺💃!

Do rzeczy. Poniżej jest przykład zasady SRP, czyli S z solida, czyli Single Responsibility Principle.

Mamy kilka komponentów i każdy z nich ma wydzieloną pojedynczą odpowiedzialność, którą programista nadał, posługując się zdrowym rozsądkiem:

// Odpowiedzialność: formatuje typ number na stringa,
// przy pomocy podanego z góry formatu.
// Nazwa numeralJS pożyczona jest od świetnej libki, która robi to samo.
const numeralJs = format => (
  number => formatNumber(format, number)
)

// Odpowiedzialność: generuje poprawny markup dla sformatowanej liczby,
// czyli, w tym wypadku, wyświetla ją w divie.
function NumberFormatter({ format, number }) {
  return (
    <div>
      {numeralJs(format)(number)}
    </div>
  )
}

Następnie dwa przykłady użycia w tej samej aplikacji, ale w różnych miejscach:

// Przykład użycia:
function SimpleComponent({ number }) {
  return (
    <div>
      The number is: <br />
      <NumberFormatter format={'0.000'} number={number} />
    </div>
  )
}
// Input:
// <SimpleComponent number={0} />
//
// Output:
// The number is:
// 0.000

// Drugi przykład użycia:
function ComponentWithPicture({ number }) {
  return (
    <div>
      <img src="/img/the-number-is.jpg" /><br />
      <NumberFormatter format={'0'} number={number} />
    </div>
  )
}
// Input:
// <ComponentWithPicture number={0} />
//
// Output:
// <the number is picture>
// 0

I teraz uwaga, przychodzi klient i 🚨 zmienia wymagania 🚨.

Opis zmiany: jeśli podana liczba === 0 to NumberFormatter powinien mieć przypisaną klasę is-zero.

Potrzebne jest to jakiemuś zewnętrznemu teamowi od CSSów, żeby mogli sobie postylować.

Więc programista siada i kodzi zmianę:

function NumberFormatter({ format, number }) {
  const className = classnames({ 'is-zero': number === 0 })
  return (
    <div className={className}>
      {numeralJs(format)(number)}
    </div>
  )
}

Wprowadzona zmiana wygląda sensownie - NumberFormatter odpowiedzialny jest za formatowanie, a w dodatku wymagane jest, żeby klasa była przypisana do diva właśnie w NumberFormatterze, więc czemu nie wrzucić kodu właśnie tam?

Łatwo poszło.

Problem w tym, że rozwiązanie jest niepoprawne. Dlaczego?

“Problem” z tym kodem jest taki, że teraz niezależnie od tego, czy wyświetlany jest SimpleComponent, czy ComponentWithPicture to zawsze ta nowo dodana klasa zostanie dopisana.

Gdy wyżej wspomniany team od CSSów zacznie dopisywać sobie jakieś style, to chcąc nie chcąc, te zmiany zostaną odzwierciedlone w tych dwóch komponentach.

Okazuje się, że zmiana była wymagana tylko dla użytkowników komponentu SimpleComponent. Ten drugi, z obrazkiem, ma pozostać bez zmian.

Programista powinien spytać samego siebie lub klienta kto dokładnie oczekuje tej zmiany? Dla kogo ona jest?

Nie zrobił tego i przez to zabetonował kod, nie w ten sposób co trzeba. A chciał, tylko żeby było DRY i SRP 😢.

Oczywiście problem jest trywialny w tym przypadku. Ale tylko dlatego, że jest to maksymalnie uproszczony przykład, żeby było widać, o co chodzi.

W normalnych aplikacjach taka prosta zmiana może wpłynąć znacznie gorzej na aplikację. Taki komponent może być używany w wielu miejscach, w różnych kombinacja, może być opakowany przez inne komponenty itd.

Jak wykrywać takie problemy?

Co zrobić, gdy nie masz na karku doświadczenia wyniesionego z wielu różnych projektów i ciężko Ci wykrywać takie sytuacje jak ta opisana wyżej?

Nie ma jednego sprawdzonego przepisu na każdy problem.

Użycie jakiejś zasady, libki lub frameworka, to nie jest sposób na wszystkie problemy. To są wysokopoziomowe narzędzia, które rozwiązują problemy ich twórców. Niekoniecznie Twoje.

Nawet SOLID powstał, żeby rozwiązywać problemy jakiś ziomeczków 20-40 lat temu, gdy jeszcze nikt nie wiedział, co to jest JavaScript. I teraz front-end junior developerzy na całym świecie próbują sobie tłumaczyć to na komponenty w Reakcie 😂.

Owszem, da się, bo są to dość uniwersalne zasady, które opierają się na logicznym myśleniu i jeszcze bardziej niskopoziomowych zasadach. Jednak mają bardzo duży narzut ze strony programowania obiektowego. A React nie jest OOP.

Dwie takie niskopoziomowe “zasady”, którym można ufać to:

  1. pamiętać, że “to zależy”
  2. pamiętać, że zmiana jest czymś pewnym

Gdy jeszcze nie wiesz, od czego “to zależy”, to najlepszym hackiem, jaki znam, jest optymalizacja pod kątem zmiany.

Wspominałem w poprzednim wpisie, że zmiana jest czymś, co pomaga podejmować bardziej świadome decyzje.

W przypadkach takich jak ten z SRP/DRY, najlepszym sposobem na identyfikowanie gdzie wprowadzić zmianę jest zidentyfikowanie “aktorów”, którzy ją zainicjowali.

Aktorem w tym naszym całym teatrzyku może być zarówno materia ożywiona, jak i nieożywiona. Jest to po prostu osoba, proces, lub nawet fragment kodu, wokół którego zmiana się kręci.

W niektórych kręgach jest to inaczej nazywane osią zmiany, ale łatwiej tłumaczy się to przy pomocy aktorów.

aktorzy

Poniżej, najprawdopodobniej niekompletna, ale dająca pogląd na sprawę lista możliwych aktorów:

  • użytkownicy
  • klienci
  • klienci klientów (…)
  • każdy członek zespołu z osobna
  • product ownerzy (w zależności od tego, jak wytłumaczysz funkcjonalność możesz dostać 2 różne aplikacje)
  • programiści
  • doświadczenie programistów
  • frameworki
  • biblioteki
  • wykorzystane wzorce architektoniczne

Zastosujmy tę wiedzę do problemu z kodzikiem, który pokazałem wyżej.

Powinniśmy zapytać o to, kto w tym scenariuszu jest aktorem oczekującym zmiany. Dzięki temu dowiedzielibyśmy się gdzie wprowadzić zmianę.

W tym wypadku dostalibyśmy informację, że użytkownicy komponentu SimpleComponent. Nikt inny. Tylko oni chcą, żeby klasa is-zero była przypisywana, gdy liczba === 0.

Mając taką informację, można zakodzić to trochę lepiej:

function SimpleComponent({ number }) {
  return (
    <div>
      The number is: <br />
      <NumberFormatter
        format={'0.000'}
        number={number}
        // 👉 to jest nowe:
        additionalClassName={classnames({ 'is-zero': number === 0 })}
      />
    </div>
  )
}
function NumberFormatter({ format, number, additionalClassName }) {
  return (
    <div className={additionalClassName}>
      {numeralJs(format)(number)}
    </div>
  )
}
NumberFormatter.defaultProps = { additionalClassName: '' }

Dodaliśmy możliwość konfiguracji NumberFormattera. SimpleComponent może teraz przekazać w propsach dodatkowe zasady do ustawienia CSSów, a ComponentWithPicture nie wymaga zmian, bo skorzysta z domyślnej wartości additionalClassName, która jest pustym stringiem.

Zmiana i jej pochodzenie nadało kierunek temu, w jaki sposób zmienić kod.

Optymalizujemy, biorąc pod uwagę zmiany, dlatego wiadomo jak działać.

offtop box

Przy okazji bonus:

poza DRY i SRP dostaliśmy dodatkowo OCP w pakiecie - kod NumberFormattera jest teraz bardziej otwarty na rozszerzanie/konfigurację i bardziej zamknięty na modyfikację. Słowo kluczowe to bardziej.

OCP to proces, żaden kodzik nie jest w 100% OCP, często nie musi być.

Więc jeśli następnym razem ktoś Ci powie, że konieczenie musisz przepisać wszystkie swoje switche na wzorzec strategii, żeby być zgodnym z zasadą OCP, to powiedz mu, żeby się jebnął w czoło zastanowił nad swoim zachowaniem 🤙.

Tutaj warto pamiętać, że nie ma czegoś takiego jak dobry kod. Jest tylko poprawny w danej sytuacji.

W tej sytuacji, na stan obecnej wiedzy, taki refactor był ok. Znając życie, zajdzie jeszcze potrzeba wprowadzania zmian i na razie nie wiemy, jakie one będą.

Swoją drogą, poprzedni refactor też był ok. Ale tylko dlatego, że na tamten - wybrakowany - stan wiedzy, takie rozwiązanie było ok. #trudnesprawy

Co, gdy piszesz apkę od zera i nie wiesz jakie zmiany Cię czekają?

Najwięksi eksperci w IT nabyli ekspertyzę, ponieważ siedzieli w projektach-molochach albo brali udział w tylu projektach, że widzieli problemy spowodowane przez niedostosowanie się do zmian. Albo jedno i drugie. Widzieli, jak projekty przez to upadają i jak trzeba było wszystko przepisywać.

Mają wyczucie jak pisać czysty kod, nawet gdy zaczynają z projektem od zera.

Co zrobić gdy nie masz tego doświadczenia?

Warto pamiętać, że doświadczony programista kieruje się tymi samymi niskopoziomowymi zasadami “to zależy” i “zmiana jest pewna”.

Podświadomie i z automatu zadają sobie pytania o aktorów.

Bo można ich szukać, nawet zaczynając projekt od zera. Aktorem może być framework lub libka, która narzuca to, w jaki sposób piszemy kod.

Czasem nie będzie Ci się podobać API jakiejś libki, której musisz używać, żeby nie wymyślać koła na nowo.

Możesz wtedy zamknąć jej obsługę w osobnym module i udostępnić metody, dzięki którym będziesz mógł jej używać w taki sposób, jaki jest dla Ciebie wygodny.

Będziesz nieświadomie stosował wzorce projektowe, nawet jeśli żadnych nie znasz. Użyjesz pewnie wzorca adaptera, proxy, dekoratora, fasady, czy czegokolwiek innego co pomoże w pracy z kodem.

Co jeśli faktycznie nie da się znaleźć żadnego aktora?

Możesz spróbować poznać jakie zmiany będą oczekiwane w przyszłości.

Zadaj sobie, PM-owi, albo klientowi poniższe pytania:

  • Jakie są długofalowe plany na ten moduł/tę aplikację, czy będzie się zmieniać?
  • Jak często?
  • Kiedy?
  • Jakie mogą być powody zmiany?
  • Jak będziemy ją śledzić?

W przypadku braku doświadczenia odpowiedź na to pytanie będzie zależała od Twojej umiejętności logicznego myślenia. Jednym wychodzi lepiej, innym gorzej, ale śpiewać każdy może. Warto spróbować.

Sam będziesz musiał ocenić czy ufasz temu, co mówi PM, klient, czy ktokolwiek do kogo udasz się po radę.

Co, gdy nie ma odpowiedzi na te pytania i logika podpowiada, że bawimy się tutaj w grę w zgadywanie, a zgadywanie to przeważnie nie jest dobry pomysł?

Easy. Nie pisz czystego kodu. Stawiaj na prostotę. Pisz kod łatwy do wywalenia i przepisania.

Jest to jedna z lepszych taktyk optymalizowania pod kątem zmiany.

Gdy nie wiadomo co trzeba napisać, to warto napisać najprostszy i najbardziej minimalistyczny kod, który rozwiązuje tylko jeden konkretny problem i ruszyć dalej.

Gdy nadejdzie zmiana, to podyktuje Ci, jak należy przepisać kod, żeby było dobrze.

Znacznie łatwiej jest przepisać kretyńsko prosty kodzik, który działa tylko w jednym określonym przypadku, niż ultrageneryczny, oparty o SOLIDA i wszystkie możliwe wzorce projektowe, czysty kodzik, który obsługuje wszystkie możliwe przypadki (często taki kod rozumiany jest tylko przez samego autora i czasem przez jego najlepszego kolegę, co dodatkowo utrudnia wprowadzanie modyfikacji).

Co z tego, że jest czysty, piękny i generyczny, jeśli rozwiążemy nim nie ten problem, co trzeba?

Jeśli zaczniesz zgadywać wymagania oraz oczekiwania względem kodu to może się okazać, że usztywniłeś kod, nie w ten sposób co trzeba. I zorientujesz się, dopiero gdy poznasz faktyczny problem, jaki apka rozwiązuje. Tak jak w pierwszym przykładzie tego wpisu.

“We should be mindful of the fact that we don’t really know what requirements will be placed upon our code in the future”

~Kent C. Dodds

Gdy piszesz (kretyńsko) prosty kodzik, to bardzo często okazuje się, że działa on potem przez lata i nikt go nie potrzebuje zmieniać. Bo robi robotę.

Są projekty gdzie napisany w spaghetti bashu skrypt do deploya działa od lat i nikt go nie dotyka, bo nie potrzeba wprowadzać żadnych zmian w procesie deploymentu. I wszyscy są szczęśliwi.

Są projekty oparte o 100 linijkowe mikroserwisy, które są totalnym spaghetti.

Gdy przychodzi zmiana, to się je po prostu przepisuje. Bo żaden magik nie bawił się we wciskanie tam wzorców projektowych na siłę. I wszyscy są szczęśliwi.

Wydaje mi się, że pisanie kodu łatwego do usunięcia to jest najlepszy sposób na optymalizacje pod kątem zmiany.

Możliwe, że lepszym jest tylko darować sobie pisanie kodu w ogóle. Warto poczekać, aż będziemy wiedzieć, to co musimy wiedzieć i dopiero wtedy bawić się w pisanie ładnego kodu.

Najłatwiej zmienia się kod, który nie został jeszcze napisany.

Nie zawsze jest tak pięknie

Często, np. gdy projekt jest krótki, to nawet nie dostaniesz wystarczającej ilości zmian, żeby stosowanie przedstawionych w tym artykule praktyk miało sens. Z tym też trzeba się pogodzić i nie komplikować kodu na siłę.

Jest też możliwe, że w szczególnych przypadkach projektów ze sztywnym, zamrożonym scopem nie będzie sensu optymalizować pod zmiany. Bo żadne zmiany nie są przewidziane. Wtedy bawienie się w czysty, generyczny kodzik to tylko marnowanie pieniędzy klienta.

Jeszcze w innych przypadkach klient może zastosować typowy brute force.

Jest to sposób optymalizacji pod kątem zmiany polegający na zarzuceniu projektu/problemu ludźmi, którzy walą nadgodziny tak długo, aż dowiozą, co trzeba dowieźć. Będą kleić projekt taśmą klejącą, własnym potem i krwią, byleby tylko dowieźć na czas.

W bardzo wielu przypadkach takie podejście działa i dopiero po latach, gdy nie zostaje już nic innego niż przepisanie apki na nowo, ludzie zaczynają rozumieć, dlaczego był to zły pomysł.

Niektóre biznesy nie mają też z przepisywaniem większego problemu. Dla nich jest to okazja do pozyskania kolejnych funduszy i zapewnienia sobie pracy na najbliższe miesiące lub lata. I z tym też trzeba się pogodzić.

mud bath

Nie samym kodem człowiek żyje

Żeby nie było tak smutno, to teraz coś fajniejszego.

Praktyki zawarte w tym artykule, pokazałem na przykładzie kodu. Jednak to wszystko można stosować praktycznie do każdego aspektu naszej pracy.

W poprzednim wpisie wspomniałem o takich wyborach jak monorepo vs polyrepo, strategia deploymentu, struktura katalogów i plików, wybór libki, wybór frameworka, czy proces, w którym pracujemy.

Wszystkie te rzeczy optymalizujemy pod kątem zmiany. Jeśli nie masz doświadczenia, to zidentyfikuj aktorów, którzy te zmiany inicjują. Zadaj pytania - kto będzie deployował? Jak często? A potem dostosuj strategię właśnie pod te czynniki.

Gdy nie da rady tego zrobić, to postaw na prostotę - zastosuj najprostsze możliwe rozwiązanie, które można łatwo “przepisać”, czyli zastąpić innym, gdy będzie trzeba i move on. Poprawisz, gdy już będziesz miał więcej informacji.

Czasem najlepszym sposobem na zmianę w deploymencie jest po prostu powiedzieć komuś, żeby zaczął inaczej klikać na AWSie 🤷‍♂️.

I pewnie niektórzy wielcy fani Continuous Integration i Continuous Delivery mocno się gotują, jeśli czytają te słowa. Jednak trzeba pamiętać, że nawet jeśli postawimy sobie CI i CD za cel, bo to genialne praktyki, to nie zawsze łatwo je wprowadzić. Nie zawsze stanie się to od razu, a czasem nie stanie się to w ogóle. Więc zanim się to nie stanie, warto celować w proste rozwiązania.

Podsumowanie

Optymalizacja pod kątem zmiany to świetne narzędzie, które pomaga podejmować lepsze decyzje projektowe.

Pomaga też shackować brak doświadczenia i poradzić sobie ze słynnym “to zależy”. Bo to przeważnie zależy od jakiegoś rodzaju zmian.

Te zmiany zawsze porusząją się po jakiejś osi, zawsze jest jakiś “aktor”, którego dotyczy.

Dałem Ci kilka przykładów, skąd mogą pochodzić. Powinno Ci to pomóc w zdefiniowaniu jak czyścić swój kod i czy w ogóle warto to robić.

Czasem zmian będzie dużo i będziesz musiał bardziej dbać o to, żeby Twój kod był na nie odporny. Czasem będzie ich mniej i będziesz nawijał spaghetti klawiaturą.

Nie będzie miało znaczenia, że piszesz brzydki kod, bo nikt nigdy nie będzie do niego zaglądał. A jeśli się okaże, że jednak musi zajrzeć, to wystarczy, że przepisze ten fragment, nad którym pracowałeś.

Zmiana dyktuje nam jak pracować. Dostosowanie się do tego wymaga czasu i doświadczenia. Z moich obserwacji wynika, że najlepszym nauczycielem jest praca w dużych, zmieniających się systemach, na systemach, które są już na produkcji oraz w projektach legacy, które trzeba zmieniać i poprawiać.

Gdybym chciał naprawdę szybko nauczyć się reagowania na zmiany, to takich systemów bym szukał.

Brak doświadczenia to nie jest tylko przypadłość początkujących. Doświadczenia brakuje mi za każdym razem, gdy poznaję jakąś nową libkę lub zaczynam nowy projekt.

I dlatego zachęcam do popróbowania metod, które tu opisałem, bo ja korzystam z nich cały czas.

Jestem przekonany, że jest to znacznie lepsze podjeście niż uczenie się API każdej nowej libki na pamięć i rozczulanie się nad tym, czy kodzik jest już wystarczająco piękny, czy nie.

Koniecznie daj znać jak Ci poszło :)

Podziel się:

Instagram, Twitter, YouTube, Facebook, LinkedIn