AOP w Laravel

Z tego artykułu dowiesz się, czym jest programowanie aspektowe (AOP) i jakie są jego podstawowe założenia, poznasz ciekawy przykład jego zastosowania w praktyce (Case Study z fragmentami kodu), a także wyniesiesz kilka nowych słówek, którymi zabrylujesz w towarzystwie ;)

Transparent Data
Blog Transparent Data
9 min readNov 13, 2019

--

Niniejszy artykuł powstał w oparciu o prezentację wygłoszoną przez jednego z członków naszego zespołu, Mateusza, 7.11.2019 roku podczas Laravel Poznań MeetUp #13, cyklicznego wydarzenia, o którym więcej przeczytać możecie TUTAJ.

AOP Case Study zamiast wstępu

(krótkie wprowadzenie do tego, czym tak naprawdę jest programowanie aspektowe i kiedy warto się zastanowić, czy tego nie użyć)

Załóżmy, że pracujemy w firmie, która przygotowuje aplikację dla rodzin, wspomagającą zarządzanie pieniędzmi. Starsi programiści pracują nad bezpieczeństwem przelewów i informacjami o tych przelewach, ale w firmie jest też młodszy programista. PM postanawia więc przydzielić mu jakieś proste, łatwe zadanie.

Zadanie dla Juniora:

Warto żebyśmy mogli gdzieś w kodzie zalogować informację o większych przelewach, czyli przelewach na kwotę powyżej 1000 zł.

Zrób to najprościej jak się da.

Zatem Junior otworzył kod.

Stan początkowy kodu, który otworzył Junior

Młody programista odnalazł w kodzie następujące klasy istotne dla jego zagadnienia:

  • BankAccount — klasę składającą się z trzech prostych metod pozwalających mu wypłacić środki z konta jeśli znajdzie się na nim dostatecznie dużo, wpłacić pieniądze na to konto oraz wypisać informacje o stanie aktualnym tego konta,
  • MoneyTransfer — klasę odpowiedzialną za nadzorowanie samego procesu przelewu pieniędzy. Inicjowana poprzez przekazanie jej 2 obiektów klasy BankAccount, z których odpowiednio najpierw wyciągamy pieniądze a następnie je deponujemy. Klasa ta posiada też osobną metodę odpowiedzialną za samą transakcję.

Zatem co Junior mógł zrobić, skoro miał wykonać zadanie najprościej jak się da? Spójrzcie na jego dzieło:

Rozwiązanie Juniora nr 1

Po skończeniu pracy, nasz Junior postanowił przekazać swój fragment kodu do review komuś bardziej doświadczonemu.

Poszedł więc do Juniora z większym doświadczeniem, który trochę już w firmie przepracował.

Ten spojrzał na kod i zaczął swoje dywagacje:

A co to jest właściwie ten 1000?

Ta metoda, to jakaś duża się wydaje…

Takie proste rzeczy? Może wydzielmy tę funkcjonalność do innej metody, żeby było czytelniej?

No to Junior wziął kod. Przepisał go. Wydzielił wszystko do innej metody. Użył stałej.

Rozwiązanie Juniora nr 2

I OK. Jest niby trochę lepiej, ale jak widać, złamał zasadę “S” z SOLID, czyli zasadę pojedynczej odpowiedzialności (jeśli o niej nigdy nie słyszeliście, trochę ją opisaliśmy w innym artykule TUTAJ LINK). A tak przecież nie może być w czystym kodzie.

Junior dostaje więc kolejne review z sugestią, żeby rozwiązał ten task przy użyciu wzorca projektowego Dekorator. Jak to nowy programista, tym razem czyta sobie trochę czym jest SOLID, czym jest Dekorator, po czym wraca do poprawek.

Oto fragment napisanego przez niego kodu:

Rozwiązanie Juniora nr 3

Jak widać, Junior zgodnie z użytym wzorcem napisał warstwę abstrakcji, czyli MoneyTransferDecorator oraz konkretną implementację tej abstrakcji LoggedMoneyTransfer, która odpowiedzialna jest za wypisanie istotnej informacji. Tym razem wygląda to w miarę w porządku. Kod poszedł na produkcję.

Mija miesiąc…

PM znów ma zadanie dla Juniora.

Zadanie dla Juniora #2:

Implementujemy teraz proces związany z podejmowaniem pieniędzy z konta (np. w okienku banku lub bankomacie).

Chcemy nadal logować większe transakcje, tak jak miało to miejsce w przypadku większych przelewów z jednego konta na drugie.

Już robiłeś coś podobnego, więc sobie poradzisz.

Junior otwiera zatem kod:

Wszystko jest jasne i działa.

Junior idzie więc sprawdzoną metodą — tworzy klasę abstrakcji MoneyWithdrawDecorator, konkretną implementację, modyfikuje kontener zależności i jest OK.

Rozwiązanie Juniora:

Wszyscy się ucieszyli. Dobra robota. Task zakończony.

Mija kolejnych kilka miesięcy…

Zaimplementowaliśmy kolejnych kilkanaście procesów.

Zrobiliśmy dla wszystkich dekoratory.

Dla wszystkich dekoratorów napisaliśmy warstwy abstrakcji.

Zaczęliśmy się wkurzać na myśl o kolejnym “kopiuj/wklej”.

Zaplanowaliśmy dev-meeting, aby przemyśleć całą architekturę od nowa jeszcze raz. I to właśnie na tym spotkaniu, ktoś cichutko i nieśmiało zaproponował, że doskonałym rozwiązaniem może być programowanie aspektowe.

Trochę teorii AOP

Czym tak naprawdę jest programowanie aspektowe?

Aspect-oriented programming (w skrócie: AOP) zrodziło się w połowie lat ’90 z inicjatywy zespołu Xerox Palo Alto Research Center (PARC), jako odpowiedź na potrzebę dobrej organizacji w kodzie dużej ilości zagadnień funkcjonalnie niezależnych jednak przecinających się w wielu miejscach systemu.

Koncept wspomaga separację niezależnych zadań poprzez umieszczenie każdego zagadnienia w oddzielnym aspekcie i logiczne, świadome zdefiniowanie punktów interakcji pomiędzy nimi.

AOP opiera się na 3 założeniach:

  1. Jesteśmy zgodni z SOLID i innymi dobrymi praktykami obiektowymi

2. Rozdzielamy w kodzie “niezależne zagadnienia” (cross cutting concerns). Przez niezależne zagadnienia należy rozumieć różne funkcjonalności aplikacji, np. cachowanie, warstwy bezpieczeństwa, ale też zadania z płaszczyzny procesów biznesowych, np. moduł do wysyłania sms-ów, gdy wydarzy się zdarzenie X.

3. Świadomie definiujemy punkty interakcji “niezależnych zagadnień” (join point), tak żeby było dla nas jasne, gdzie punkty styku występują a gdzie taka interakcja jest niemożliwa.

Paradygmat AOP został początkowo uznany za “dziwny”, a to dlatego, że odbiega od tego, do czego większość programistów jest przyzwyczajona. W klasycznym podejściu do programowania często trafiamy na kod, który w ramach jednego procesu biznesowego musi zawierać w sobie np. walidację danych, dodatkowe logowanie informacji do bazy i wiele innych procesów, które przysłaniają nam sam właściwy proces. Używanie wzorców projektowych pozwala nam często choć trochę uporządkować kod tak, aby był bardziej czytelny, jednak rozwiązanie to ma swoje doskonale znane ograniczenia.

Jeśli chcemy zajrzeć trochę głębiej w programowanie aspektowe, koniecznie potrzebujemy dodać kilka podstawowych pojęć do naszego słownika.

Słowniczek AOP

Aspekt (Aspect):. niezależna funkcjonalność oprogramowania, przecinająca wiele innych np. cachowanie, logowanie, bezpieczeństwo.

Wskazówka/Rada (Advice):. akcja wykonywana przez aspekt w określonych warunkach działania oprogramowania, np. przed wywołaniem metody.

Punkt połączenia/styku/interakcji (Join point):. punkt w kodzie aplikacji, w którym przecinają się niezależne funkcjonalności.

Punkt przecięcia (Pointcut):. wyrażenie regularne, będące warunkiem uruchomienia aspektu w danym punkcie styku.

Trochę kodu, żeby odrobinę liznąć jak wygląda AOP w Laravelu

Instalacja rozszerzenia dla Laravela odbywa się przez użycie composera:

Następnie czeka nas konfiguracja:

  • Tworzymy Aspekty
  • Tworzymy Provider (żeby gdzieś aspekty inicjować)
  • Zarządzamy konfiguracją w config/go_aop.php (niewymagane, ale przydatne).

Spójrzmy teraz na definicję aspektów:

Definiowanie aspektów w AOP

Jak widzimy, nasz zdefiniowany aspect musi implementować interfejs Aspect. Interfejs ten jest pusty i nie wymaga od nas implementacji żadnych dodatkowych metod.

Odnosząc się do wcześniej przedstawionego słowniczka pojęć zdefiniujemy poszczególne elementy:

  • Aspekt — aspektem w tym przypadku jest funkcjonalność logowania informacji o przelewach na kwoty przekraczające 1000. W przypadku naszej implementacji aspektem jest w takim razie cała klasa LoggingAspect,
  • Advice — w naszym przypadku aspekt nie jest zbyt rozbudowany, jedynym advice jaki mamy zdefiniowany jest metoda beforeMethod. W przypadku bardziej rozbudowanych aspektów jak np. funkcjonalność cachowania danych nasz aspekt mógłby składać się z 2 osobnych advice odpowiedzialnych odpowiednio za odczyt danych z cache i zapis tych danych do cache,
  • Punkty styku — w naszym przypadku punktem styku jest moment przed wywołaniem określonej metody. Mówi nam o tym tag @Before wraz z parametrem execution,
  • Punkt przecięcia public App\Processes\*->transfer|withdraw(*) — wzorzec określający metodę publiczną z przestrzeni nazw App\Processes\ o nazwie transfer lub withdraw przyjmującą dowolne parametry wywołania.

Na podstawie takich właśnie adnotacji mechanizm określa kiedy co ma być wywoływane w systemie.

Poza używaniem tagu @Before określającego moment “przed” zdarzeniem do naszej dyspozycji mamy również tagi @After czyli “po” oraz @Around, która pozwala nam jednym wywołaniem przechwycić całe wywołanie danego zdarzenia i w skrajnych przypadkach nawet na nie dopuszczenie do jego wykonania. Ostatnim tagiem jaki możemy użyć jest tag @AfterThrowing, który jak łatwo się domyślić jest wykonywany w momencie wywołania błędu.

Parametrem wywołania naszych advice jest obiekt klasy MethodInvocation. Obiekt ten daje nam dostęp do takich informacji o wywołaniu jak parametry, nazwa metody, nazwa klasy obiektu czy sam obiekt, na którym dana metoda jest wykonywana.

Poza wywołaniem execution mamy dostępne jeszcze kilka opcji. Możemy ustawić nasze aspekty tak, by były wywoływane podczas odczytu parametrów danego obiektu — użyjemy wtedy metody access zamiast execution, bądź podczas tworzenia obiektów, do czego posłuży nam metoda inicialization.

W ramach opisu punktów przecięć dostępny mamy mechanizm wyrażeń regularnych oraz kilka podstawowych notacji logicznych jak np. zaprzeczenie. Dodatkowo możemy określić czy interesuje nas wyłączenie wywołania metod dynamicznych czy statycznych. Całość razem tworzy bardzo silne narzędzie pozwalające opisać praktycznie każdy możliwy przypadek filtrowania wywołań.

ServiceProvider — rejestracja aspektów

Po zdefiniowaniu naszego aspektu pozostaje nam jeszcze zarejestrowanie go w systemie. W tym celu najłatwiej będzie utworzyć klasę AopServiceProvider dla naszego mechanizmu zarządzania aspektami.

Może ona wyglądać następująco:

Ostatecznie co nam to może dać?

Spójrzmy jeszcze raz na stan początkowy kodu z Case Study, zanim Junior wziął się za jego modyfikację:

Podczas wszystkich swoich prac nasz przysłowiowy Junior zmuszony był dokonywać mniejszych lub większych zmian w kodzie reprezentującym nasze procesy biznesowe. Doprowadzało to do mniejszego lub większego bałaganu i przysłonięcia nam tego, za co ten kod tak naprawdę jest odpowiedzialny. Użycie wzorców projektowych typu Dekorator poprawiło znaczenie ten stan, jednak nie ustrzegło nas to przed powieleniem kodu w kolejnych dekoratorach.

W przypadku kiedy korzystamy z AOP, w oryginalnym kodzie nie ma potrzeby dokonywać jakichkolwiek zmian. Logowanie ma miejsce, jednak kod odpowiedzialny za logowanie jest w całości odseparowany od samych procesów.

W przypadku nowej osoby, która chce zapoznać się z naszym kodem, przejrzystość jaką osiągnęliśmy pozwala skupić się na faktycznym procesie biznesowym, który może być wszak niezwykle skomplikowany. Tak skomplikowany, że nawet bez zaciemniania go kodem odpowiedzialnym za inne funkcjonalności (możliwe, że z punktu widzenia czytającego całkowicie nieistotne) i tak wyciśnie z nowego programisty siódme poty.

Z drugiej strony, osoba odpowiedzialna za mechanizm logowania nie musi szukać odwołań do swojej funkcjonalności w innych miejscach kodu. Wszystkie potrzebne dane ma zapisane w ramach zdefiniowanego aspektu i tylko tutaj musi dokonywać zmian, rozszerzając zakres monitorowanych zdarzeń lub sposób realizacji funkcjonalności.

Często, gdy piszemy “klasycznie”, kod różnych funkcjonalności przeplata się. W jednym miejscu spotykamy fragmenty odpowiedzialne za cachowanie, a zaraz za nimi fragmenty opisujące jakiś proces biznesowy razem z warstwą bezpieczeństwa i wieloma innymi. Całość zaczyna przypominać nieczytelny, mocno splątany warkocz. Bardziej podatny na błędy i trudniejszy do utrzymania. To, co oferuje nam programowanie aspektowe, to coś, co pozwala nam tego uniknąć.

Link do dokumentacji AOP, gdzie wszystko jest dużo szerzej opisane znajdziecie TUTAJ.

O autorze artykułu:

Mateusz Antkowiak | Programista PHP w Transparent Data

Zawodowy programista od prawie dekady. Swoje podróże z kodem rozpoczynał od takich egzotycznych kierunków jak Lazarus Pascal czy Delphi, a obecnie, ciągnie go w stabilne wody programowania obiektowego. Posiada ogromne doświadczenie w automatyzacji procesów, co przydaje się w jego obecnych RegTech taskach w Transparent Data | Prześwietl.pl.

--

--