Na różnych etapach rozwoju kariery chyba każdego specjalisty IT przychodzi czas, kiedy zadajemy sobie pytanie, czy to, co robię, faktycznie jest tak dobre jak tylko może być? Jestem przekonany, że osoby, które zwracają szczególną uwagę na jakość świadczonych przez siebie usług bądź oferowanych produktów mierzą staranność wykonania przez pryzmat pytania: Czy nie wstydzę się pod tym podpisać?

W tym artykule zebrałem moim zdaniem najlepsze praktyki pisania kodu źródłowego Apex jako programista technologii Salesforce. Usiądź wygodnie i sprawdź, czy stosujesz je wszystkie.

Nazwa ma znaczenie

Jako pierwszą i kluczową w mojej ocenie umiejętnością świadczącą o tym, jak dobry kod piszesz, jest kwestia nazewnictwa poszczególnych jego elementów. Kwestia, która w oczach części społeczności programistów wydaje się być trywialna, tak naprawdę odgrywa kluczową rolę w zrozumieniu ogólnej koncepcji i nurtu myślowego, jaki wybrał autor kodu, aby osiągnąć konkretny cel.

Czasem można spotkać się z opinią, że dobrze napisany kod jest jak historia, którą nam ktoś opowiada albo jak dobra książka. I zanim przejdziesz dalej zastanów się, proszę, czy faktycznie tak nie jest?

Case study

Załóżmy prosty scenariusz: mamy sytuację, w której od ostatniej dokonanej zmiany na rekordzie Case’a minęło 72 godziny i checkbox IsOnHold__c nie jest zaznaczony, to zamknij Case’a i ustaw wartość OpenCasesNumber__c na powiązanym Accouncie na wartość o jeden mniejszą od obecnej. Wartość nie może być mniejsza od 0.

Tak wyglądałby kod, który spełnia opisane wymagania, ale autor kodu nie do końca zwraca uwagę na nazewnictwo metod i zmiennych:

Fragment kodu przedstawiający przypisanie listy Casów z wyrażenia SELECT do zmiennej o nazwie cList. Inicjalizacja Setu Id o nazwie aIdList. Iterowanie po cList ze zmienną lokalną o nazwie c. Pobieranie listy Accountów i przypisywanie ich do aList. Przypisanie wartości OpenCasesNumber__c z obecnie iterowanego Accounta pomniejszonej o 1 do zmiennej o nazwie ocn

Natomiast tak wyglądałby kod, w którym autor zwraca uwagę na nazewnictwo zmiennych i metod:

Przypisanie wyniku zapytania listy Casów do zmiennej meaningCases. Inicjalizacja Setu Id o nazwie accountIds. Przypisanie wyniku zapytania listy Accountów do zmiennej linkedAccounts. Przypisanie wartości OpenCasesNumber__c pomniejszonej o 1 z obecnie iterowanego Accounta do zmiennej o nazwie openCasesNumber

Mimo, że wywołanie jednego bądź drugiego kodu przyniesie ten sam rezultat, to ten drugi czyta się wygodniej, a nazwy sugerują, co metoda ma na celu oraz co poszczególne zmienne przechowują. Taki kod nie tylko łatwiej jest zrozumieć i utrzymywać, ale pokazuje również “klasę” osoby, która go napisała. Myślę, że jest część osób, która ma obawy czy ich kod aby przypadkiem nie wygląda… za prosto. Te osoby uspokajam i zapewniam – znacznie gorzej jest przesadzić w drugą stronę i oddać coś, co działa, ale jest trudne do zrozumienia i w efekcie cięższe w utrzymaniu.

Jakimi zasadami kierować się nazywając zmienne, klasy i metody?

W Salesforce, oprócz ogólnych zasad Clean Code, takich jak nazywanie zmiennych i metod notacją camelCase, a klas PascalCase, przyjęło się kilka dodatkowych rekomendacji, które warto stosować.

Rekomendacje dla zmiennych:

  • unikaj stosowania słów generycznych, tj. var, variable, temp, value, data,
  • ogranicz zawieranie nazwy typu zmiennej w jej nazwie, za wyjątkiem SObjectów,
  • wybierz nazwę, która nie będzie budziła wątpliwości, co w danej zmiennej się znajduje,
  • nie skracaj nadmiernie słów – newCase to dobry pomysł, podczas gdy newC już nie; uważaj na to szczególnie przy pętlach.

Obrazek przedstawia przykłady złego nazewnictwa zmiennych. Przykłady: casNum typu Integer, htmlExpertDescriptionValue typu String, triggerContextOpportunity typu Opportunity. Obrazek przedstawia również przykłady dobrego nazewnictwa zmiennych. Przyklady: caseNumber typu Integer, expertDescription typu String, newOpp typu Opportunity

Rekomendacje dla metod:

  • wybieraj nazwy, które dobrze opisują za co odpowiedzialna jest dana metoda,
  • unikaj nadmiernych spójników w nazwach, np. “callAndDeserialize”,getCaseOrNull”,
  • nie łącz zbyt wielu słów w nazwie – od 1 do 3 słów to optymalna nazwa, nie powinniśmy przekraczać 4 słów,
  • kieruj się zasadą, że dobra nazwa metody to taka, która opisuje akcję lub cel metody przy zachowaniu jasnego i opisowego wyrażenia napisanego w camelCasie.

Apex rekomendacje dla metod

Rekomendacje dla klas:

  • stosuj koncepcję “serwisówek”, czyli łączenia nazwy SObject ze słowem Service, np. CaseService, OpportunityService
  • sugeruj już w nazwie czy klasa wykorzystuje mechanizm “sharingów”: dla klasy, która go zachowuje i operuje w obrębie rekordów Account , dobrym pomysłem będzie AccountService, w przeciwnym przypadku AccountServiceNoShare
  • warto wykorzystywać prefixy, gdy w ramach jednego repozytorium jest wiele projektów

Obrazek przedstawia pustą klasę z wyrażeniem kluczowym "with sharing" o nazwie "SFS_CaseService" oraz inną pustą klasę z wyrażeniem kluczowym "without sharing" o nazwie "SFS_CaseServiceNoShare"

Co z hardcodowaniem? - unikać czy znaleźć dobre miejsce?

Programowanie, jak wszystko dookoła nas, nie jest idealne i – choć przyjemnie byłoby mieć kod bez stałych wartości zapisanych na sztywno – to zamiast walczyć z faktem ich występowania lepiej jest znaleźć dla nich odpowiednie miejsce. Sprawdzi się w takiej sytuacji klasa, która będzie zawierać wyłącznie wartości stałe.

Stosując dobre praktyki nazewnictwa wiemy już, że nazwa Constants jest dobra, bo dokładnie określa jej przeznaczenie, a nazwa spełnia warunki notacji PascalCase’a.

Spójrzmy na przykład:

Obrazek przedstawia klasę o nazwie "SFS_Constants", w której znajdują się statyczne pola ze słowem kluczowym "final". Przykład: CASE_STATUS_DRAFT z przypisaniem wartości "Draft" w momencie inicjalizacji. Inny przykład: CONTRACT_STATUS_DRAFT z przypisaną wartością "Draft" w momencie inicjalizacji

  • Słowem uzupełnienia – czasem zdarza się tak, że mamy dokładnie tę samą wartość, ale przeznaczoną do różnych zastosowań. Przykładem niech będzie status Draft – ta sama wartość będzie pasowała, gdy chcemy zaznaczyć status na obiekcie Contract jak również na obiekcie Case. Mimo, że wartość jest taka sama – sugeruję zrobić oddzielne stałe.

Wzorce projektowe i frameworki - zasady dobrego programowania

TriggerHandler to framework, który występuje w wielu wersjach, są opcje prostsze i bardziej złożone – implementowane zależnie od potrzeb projektowych. Zaletą tej konstrukcji jest to, że TriggerHandler jest:

  • wysoce re-używalny (do każdego SObjectu),
  • umożliwia mechanizm włączania i wyłączania konkretnych triggerów bez konieczności deploymentu na środowisko ( gdy je wyłączymy, to nie zapomnijmy ich włączyć)
  • umożliwia model wymuszający (oparty o metody abstract) lub dobrowolny (oparty o metody virtual)
  • umożliwia implementację modułu włączania / wyłączania konkretnego triggera z poziomu kodu w ramach transakcji.

Fragment klasy TriggerHandler z widoczną metodą isTriggerEnabled przyjmującą argument o nazwie triggerName typu String. Metoda pobiera instancję custom metadaty o nazwie TriggerDriver. Nazwa instancji to argument metody. W przypadku błędu rzucany jest wyjątek z własnym tekstem

 

Obrazek przedstawia "AccountTrigger", który w ciele tworzy instancję klasy "AccountrTriggerHandler" i uruchamia metodę "start", podając jako argument wywoływanej metody statyczne finalne pole klasy "Constants" o nazwie TRIGGER_DRIVER_ACCOUNT_TRIGGER_DEVELOPER_NAME

Obrazek prezentuje różnicę w podejściu wymuszającym frameworku "TriggerHandler" poprzez zastosowanie słowa kluczowego "abstract" oraz podejściu dobrowolnym ze słowem kluczowym "virtual". Przykład wymuszający w całości brzmi: "protected abstract void beforeUpdate();" Przykład dobrowolny w całości brzmi: "protected virtual void beforeInsert();"

Obrazek przedstawia implementację funkcji wyłączania konkretnego triggera z kodu poprzez sprawdzenie w metodzie „isTriggerEnabled” dodatkowego warunku, który nie występuje w klasycznej wersji TriggerHandlera. Dodatkowy warunek brzmi: negacja „triggerDisabled.contains”(parametr „triggerName”). „triggerDisabled” to kolekcja danych typu „Set” typu „String” o prywatnym dostępnie wraz z publicznymi „setterem” do niej o nazwach: „disableTrigger” (dodający do kolekcji) oraz „enableTrigger” (usuwający z kolekcji)

Czy Singleton ma jeszcze sens w Salesforce?

Od razu odpowiem – jak najbardziej.  Singleton to wzorzec projektowy znany ze znacznie starszych technologii niż Salesforce, ale przy programowaniu w Apex sprawdza się znakomicie.

Dlaczego?

  • Ponieważ jest on bardzo skuteczny w oddalaniu widma “uderzenia” w limity, które obowiązują w ramach transakcji.

Singleton sprawdzi się w obiektach, które przechowują rzadko (bądź nie w ramach transakcji) zmieniające się dane. Przykładem może być obiekt User, RecordType, Profile, Custom Metadata i inne, własne, przechowujące na przykład dane konfiguracyjne. Nie warto natomiast korzystać z Singletona przy Accountach, Case’ach czy Oppkach, które stanowią fundament systemu.

Dlaczego?

  • Singleton zakłada wyquerowanie danych raz, a potem zwracanie pobranych danych bez ich ponownego querowania, a w części sytuacji oczekujemy właśnie pobrania “świeżych” danych.
Korzyści płynące ze stosowania Singletona

Najważniejsze korzyści to:

  • wysoka uniwersalność (konstrukcja nie zmienia się dla różnych SObjectów),
  • łatwy w implementacji,
  • zapobiega nadmiernemu wykorzystaniu SOQL’ek w ramach transakcji.

Zwróćmy uwagę na  ważną rzecz – na obrazku obok zaznaczono żółtą ramką prywatne pole klasy UserService, nazwane currentLoggedUser, które stanowi sens Singletona. Została do pola dodana adnotacja @TestVisible. Jej celem jest możliwość ustawienia w tym polu wartości null w sytuacji, gdy w ramach jednego Unit Testu chcemy pobrać informacje o Userze wykonującym daną akcję w ramach metody System.runAs().

Klasyczny przykład Singletona dla obiektu "User" ze statycznym polem prywatnym "currentLoggedUser" oraz publiczną metodą "getLoggedUser", która, gdy pole "currentLoggedUser" jest puste, pobiera dane o użytkowniku, wypełnia pole i wywołuje się ponownie, oraz zwraca jego zawartość, jeśli pole nie jest puste

Obrazek przedstawia "Unit Test" z czterokrotnym wywołaniem metody "getLoggedUser" z klasy "UserService". Asercje sprawdzają, czy zwrócone pole ID użytkownika jest równe ID obecnie zalogowanego użytkownika uruchamiającego "unit test" oraz czy liczba użytych SOQL w ramach transakcji to jeden. Test kończy się powodzeniem. Informacja o ilości zużytych SOQL pochodzi z metody "getQueries()" z klasy "Limits"

Logger framework w projektach Salesforce

Podzielę się z Wami żartem, który bardzo mi się spodobał: „czasem dzięki kilku godzinom debuggowania możemy zaoszczędzić kilka minut spędzonych na czytaniu dokumentacji”.

Najprawdopodobniej przynajmniej raz w miesiącu każdy z nas musi coś zdebuggować. W zależności od projektu proces ten może trwać krócej lub dłużej. Zakładam również, że znacie  sytuację, gdy użytkownik mówi, że mu system nie działa, a gdy my próbujemy odtworzyć kroki, które wykonał, to następuje słynne,  “u mnie działa”. Może się okazać, że nie mając informacji o tym, jakie dokładnie operacje, w jakiej sekwencji i z jakimi danymi wejściowymi wykonał użytkownik, błąd jest niemożliwy do odtworzenia. Powstaje niepewność czy kod na pewno jest odporny na błędy.

Wtedy na ratunek przychodzi Logger, który pozwala uzyskać cenniejsze informacje niż logi z konsoli deweloperskiej bez system debugów pozostawionych w kodzie na stałe.

Pomysłów na Logger jest wiele i każdy charakteryzuje się czymś innym. Istnieją gotowe paczki tj. NebulaLogger, ale można również wdrożyć własny Logger. Jeden z przykładów przedstawia poniższy rysunek:

 

Logger kod APEX

Logger - korzyści

Główne korzyści płynące ze stosowania Loggera to:

  • Możliwość sterowania tym, jakie dane mają znajdować się w generowanym w systemie logu,
  • Możliwość ustawienia, które obszary aplikacji podlegają logowaniu, a które nie,
  • Przechowywanie logów jako rekordów z możliwością sterowania dostępem do nich i tym, jak zdefiniowany jest rekord loga,
  • Możliwość przechwycenia informacji o działaniach użytkownika nawet, gdy w konsoli deweloperskiej nie jest włączone śledzenie działań tego użytkownika,

W przypadku podanego wyżej schematu dodatkowym atutem jest możliwość zalogowania loga do systemu nawet wtedy, gdy w kontekście triggera wykorzystamy metodę addError() – wynika to z zastosowania Platform Eventu.

Koncepcja organizacji Salesforce jako kokpitu samolotu

Sam, co prawda, nigdy za konsolą pilota samolotu nie siedziałem, ale to miejsce przedstawiane jest jako panel z bardzo dużą liczbą przycisków, potencjometrów, wskaźników, diod i dźwigni.

Myślę, że prawdziwym majstersztykiem jest napisanie kodu w taki sposób, żeby wykonywanie jego poszczególnych fragmentów uzależnione było od ustawień w takim właśnie kokpicie. Podobne podejście zastosowano w TriggerHandlerze, gdzie sterowanie triggerem może odbywać się właśnie poprzez Custom Metadatę czy własny, customowy obiekt konfiguracyjny.

Warto chyba zastanowić się nad taką koncepcją. Przyjemnie byłoby móc napisać na tyle elastyczny kod, który pozwalałby z poziomu organizacji Salesforce sterować czy dane sekcje na stronie są widoczne czy nie, konfigurować per profil, rola  w systemie czy kraj użytkownika. Podejście takie wymaga dodatkowego czasu na początku projektu, aby zainicjować obiekt konfiguracyjny i napisać do niego Singleton. Także każda nowa funkcjonalność wówczas zajmuje dodatkowe kilka minut, żeby móc ująć jej włączenie / wyłączenie w panelu kontrolnym. Biorąc jednak pod uwagę szerokie korzyści, warto ten czas zainwestować. Zwróci się w sytuacjach, gdy klient zmienia wymagania co do jakiejś funkcjonalności – zamiast usuwać kod, zmieniamy konfigurację w naszym “kokpicie”. Możliwość wdrożenia szybkiej zmiany z pewnością pokaże klientowi, że jesteśmy na każdą sytuację przygotowani.

Apex_Salesforce org
Generyczność kodu - tak czy nie?

Podstawową zasadą słyszaną w kontekście pisania kodu jest “Don’t repeat yourself”. To fakt, widok tego samego kodu wklejonego kilka razy w różnych miejscach w systemie budzi przestrzeń do pytań. W takich sytuacjach warto zastosować oddzielną metodę która będzie  w stanie zadziałać przy delikatnie różnych danych wejściowych, na przykład dla różnych SObjectów.

Zaoszczędzone linie kodu przekładają się na mniejszą ilość kodu do napisania przy Unit Testach i oszczędności w użytych znakach kodu Apex z ustalonego limitu.

Obrazek przedstawia implementację metody, która nazywa się "getFieldNameToSObjects", zwraca "Map<String, List> ", a jako parametry przymuje "fieldNameForKey" typu "String" i "List sobjectsToProcess". Niniejsza metoda stanowi przykład generyczności, gdzie podając Listę "Casów" i wskazując jako parametr "AccountId" otrzymamy z niej grupowanie "Casów" per "AccountId"

ByPass - ratunek w sytuacjach trudnych

Zdarzają się nam wszystkim sytuacje, kiedy jakaś część system nie działa zgodnie z oczekiwaniami. Najczęściej w efekcie rekordy wymagają datafixu po naprawie niedziałającej funkcjonalności. Jest to szczególnie stresująca sytuacja, gdy incydent ma miejsce na środowisku produkcyjnym. Gdy wykorzystujemy triggery czasami powrót do danego stanu rekordu sprzed incydentu jest utrudniony, a nawet niemożliwy (szczególnie, gdy część pól nie jest lub nie może być śledzona).

Ostatnią rzeczą jakiej wtedy nam potrzeba jest to, żeby zaplanowane mechanizmy tła, flowy czy batche dodatkowo zmieniały rekord, który musimy przeanalizować i stworzyć plan naprawczy.

Aby zapobiec takim sytuacjom warto zastosować tzw. ByPass, dodatkowe pole (checkbox/picklista), którego rola sprowadza się do tego, że – gdy jest zaznaczone, to mechanizmy tła nie wprowadzają zmian na rekordzie, nawet jeśli są spełnione inne kryteria wejściowe.

Oczywiście decyzja o tym, jak taki ByPass miałyby działać, czy miałby dotyczyć wyłącznie operacji tła czy również triggerów oraz czy powinien być to checkbox czy picklista, pozostaje do decyzji programisty.

przykład-mechanizmu-typu-ByPass-w-formie-checkboxa
Sprawdź, zanim pobierzesz - limity transakcyjne

To podejście, podobnie jak Singleton, ma na celu odsunąć od nas zmorę limitów transakcyjnych Salesforce. Ten prosty pomysł najczęściej wykorzystywany jest w kontekście triggera. Zanim pobierzesz, sprawdź czy w ogóle jest to coś, czego szukasz.

 

limity transakcyjne Salesforce

Commit message - istotna część, która umożliwia śledzenie zmian wprowadzanych w kodzie

O ile w dobrze prowadzonych projektach pytanie “Kto to zrobił?” pada rzadko albo wcale, to  pytanie “Dlaczego coś działa tak, a nie inaczej” powinno być zadawane dość często.

Kluczem do połączenia kodu z wymaganiami mogą być commit messages, a im lepsza korelacja, tym łatwiej zrozumieć ciąg przyczynowo – skutkowy. Stąd w najlepszych praktykach programowania w Apex zaleca się stosowanie jasnego identyfikatora, który umożliwia lokalizację ticketu z wymaganiami w kodzie. Przykład: Jeśli realizujesz zadanie o identyfikatorze np. 1103, to dobrym pomysłem jest, aby Twój commit message wyglądał następująco: “1103 – [krótki opis tego, co zawiera commit]”.

Tego zdecydowanie unikaj

Na koniec zebrałem jeszcze kilka rzeczy, których warto unikać pisząc kod Apexa:

  • Hardcodowanie ID,
  • Ignorowanie istnienia instrukcji try catch,
  • DMLki i SOQLe w pętlach,
  • Pobieranie szerszego zbioru danych niż jest potrzebny i iterowanie po nim zamiast stosowania klauzuli WHERE w zapytaniu SELECT,
  • Brak konsekwencji w przyjętych koncepcjach projektowych,
  • Zaciąganie długu technologicznego podyktowane motywem dostarczania większej ilości funkcjonalności,
  • Pisanie nieefektywnych unit testów (pokrywających jedynie kod a nie sprawdzających funkcjonalność metod)
  • Niestosowanie Data Factory w testach
Podsumowanie

Stosowanie dobrych praktyk pisania kodu Apex to nie jest sztuka dla sztuki, tylko realne rozwiązania, które służą zarówno programistom jak i końcowym użytkownikom aplikacji. Stosowanie ich niejednokrotnie wpływa na kondycję systemu, możliwość szybkiego reagowania na incydenty i ogólny performance aplikacji.

Ponadto, gdy weźmiemy sobie do serca wskazówki, czego należy się wystrzegać, to zyskamy kolejne podpowiedzi, jak pisać dobre jakościowe oprogramowanie w ekosystemie Salesforce.

Stosowanie best practices programowania APEX naprawdę nie jest trudne. Uczy efektywnego pisania kodu i zapobiega trudnościom z utrzymaniem kodu w przyszłości. Decyzja o tym czy je stosować nie powinna podlegać dyskusji, a w ramach współpracy projektowej powinniśmy ustalić które przyjmujemy, a które nie stanowią wartości dodanej przy budowaniu lub rozwijaniu konkretnego rozwiązania.

Szczęśliwego kodowania!

Autor
  • Michał Łęcicki
  • Salesforce Developer
  • Salesforce Developer z ponad trzyletnim doświadczeniem, pasjonat optymalnych, zgodnych z zasadami rozwiązań. Posiada kilka certyfikatów Salesforce z zakresu programowania i konfigurowania platformy. Lubi wyzwania polegające na optymalizacji oprogramowania pod kątem wydajności i customer expirience. Prywatnie prawdziwy fan metodologii Agile, który stara się implementować ją w codzienności, człowiek, który chce dojść do wielkich rzeczy poprzez samorozwój. Lubi słuchać podcastów historycznych oraz zgłębiać wiedzę z zakresu psychologii.

Jak zwiększyć szansę na sukces Twojego projektu Salesforce?