Cześć. W dzisiejszym odcinku cyklu poświęconego wzorcom projektowym, które możemy zastosować do tworzenia rozszerzeń naszego ulubionego systemu, przyjrzymy się wzorcu Komendy („Command”, w języku polskim znanego również jako: „Polecenie”). Czy jest owa „komenda”? Definicja zaczerpnięta z Wikipedii przedstawia się w następujący sposób:
In object-oriented programming, the command pattern is a behavioral design pattern in which an object is used to encapsulate all information needed to perform an action or trigger an event at a later time. This information includes the method name, the object that owns the method and values for the method parameters.
Hmmm… Muszę przyznać, że w pierwszej chwili brzmi to mocno enigmatycznie. Spróbujmy przełożyć jednak powyższą definicję do świata aplikacji Power Apps sterowanych modelem*.
W momencie pisania artykułu Microsoft udostępnia swoją platformę do tworzenia aplikacji biznesowych w modelu on-line (w tym przypadku mówimy o aplikacjach Power Apps sterowanych modelem i wykorzystujących do pracy platformę Common Data Service) oraz on-premise (tu z kolei w dalszym ciągu obowiązuje nazwa Dynamics 365 Customer Engagement). Ponieważ omawiane w tekście reguły mogą być zastosowane w obu scenariuszach (nowe aplikacje model-driven Power Apps lub rozszerzenia istniejących oraz rozszerzenia systemu Dynamics 365 CE) będę używał ww. nazw naprzemiennie.
W poprzednich częściach cyklu poświęconego programistycznym wzorcom projektowym wiele razy przewinął się model 3-warstwowej architektury składającej się z warstwy persystencji (repozytoria danych), warstwy logiki biznesowej (serwisy) oraz warstwy wykonania (pluginy lub aktywności workflow). Podział ten umożliwia nam przejrzystą organizację kodu oraz możliwość jego niezależnego testowania w poszczególnych warstwach za pomocą testów jednostkowych. Z omawianym wcześniej podejściem wiążą się jednak pewne zagrożenia. Wyobraźmy sobie sytuację, w której implementujemy rozszerzenie, którego kod wykonywany jest w momencie zapisania w systemie obiektu Klienta. Klasa implementująca interfejs IPlugin (np. AccountPostCreatePlugin) korzysta z kilku serwisów domenowych, wyrażeń warunkowych oraz innych zależności. Wszystko działa dobrze do czasu, w którym musimy zaimplementować kolejną część logiki biznesowej związanej z utworzeniem w systemie nowego rekordu klienta. Możemy sobie wyobrazić sytuację, w której leniwy lub niedoświadczony programista dopisuje kolejne linijki kodu oraz wywołania serwisów wewnątrz metody Execute naszego istniejącego rozszerzenia. Efektem tego działania jest klasa posiadająca wiele różnych odpowiedzialności i łamiąca zasadę SOLID, prawdopodobnie niedziałające testy jednostkowe oraz coraz bardziej skomplikowany i zagmatwany kod.
W jaki sposób możemy uniknąć opisanego powyżej problemu? Otóż możemy spróbować skorzystać ze wzorca komendy. Chcemy doprowadzić do sytuacji, w której kod odpowiedzialny za uruchamianie logiki związanej z danym zadaniem będzie znajdował się za każdym razem w osobnej klasie. Dodatkowo chcielibyśmy uporządkować naszą warstwę wykonania i wyeliminować problem rosnących i nieczytelnych metod Execute w tejże warstwie.
Na początku opiszemy generalizację polecenia za pomocą następującego interfejsu:
public interface ICdsCommand { bool CanExecute(); void Execute(); }
Wydaję mi się, że powyższy kod nie wymaga większych wyjaśnień. Metoda CanExecute zwraca wartość true/false w zależności od zadanych warunków wejściowych (w naszym przypadku będzie to zawartość kontekstu uruchomieniowego rozszerzenia) . Natomiast metoda Execute jest odpowiedzialna za uruchamianie właściwej logiki biznesowej związanej z danym zagadnieniem. Przykładowa komenda, odpowiedzialna za utworzenia zadania dla zespołu sprzedażowego w momencie, w którym nowy namiar posiada więcej niż 30 pracowników, może wyglądać w następujący sposób:
public class TryCreateTaskCommand : CdsCommandBase { public override bool CanExecute() { if (this.Context.PrimaryEntityName == Lead.EntityLogicalName && (this.Context.MessageName == "Create" || this.Context.MessageName == "Update")) { return true; } else { return false; } } public override void Execute() { var lead = TargetEntity.ToEntity<Lead>(); if (lead.NumberOfEmployees > 30) { var teamRepository = CdsRepositoryFactory.GetRepository<ITeamRepository>(); var team = teamRepository.GetSalesTeam(); if (team != null) { var taskRepository = CdsRepositoryFactory.GetRepository<ITaskRepository>(); taskRepository.Create(new Task() { Subject = "Important lead. Please do sales actions ASAP!", RegardingObjectId = lead.ToEntityReference(), OwnerId = team.ToEntityReference() }); taskRepository.SaveChanges(); } } } }
Klasa CsdCommandBase daje nam z kolei dostęp do elementów „infrastruktury” Dynamicsa (serwisy, właściwości ułatwiające dostęp do elementów kontekstu, fabryki obiektów itp.):
public abstract class CdsCommandBase : ICdsCommand { protected IPluginExecutionContext Context { get; set; } protected ITracingService TracingService { get; set; } protected ICdsRepositoryFactory CdsRepositoryFactory { get; private set; } protected Entity TargetEntity => Context?.InputParameters["Target"] as Entity; public abstract bool CanExecute(); public abstract void Execute(); public void Initialize(IPluginExecutionContext context, ITracingService tracingService, ICdsRepositoryFactory repositoryFactory) { this.Context = context; this.TracingService = tracingService; this.CdsRepositoryFactory = repositoryFactory; } }
W powyższym przykładzie cała logika biznesowa została zaimplementowana wewnątrz komendy. Nic nie stoi jednak na przeszkodzie, żeby wewnątrz metody Execute skorzystać z serwisów domenowych lub innych komponentów zawierających implementację uruchamianego kodu. Jest to wręcz wskazane w przypadku bardziej skomplikowanych rozwiązań. Wprowadzając dodatkową warstwę abstrakcji, ułatwimy sobie proces testowania oraz wyeliminujemy potencjalnie niebezpieczne zależności,
W jaki sposób uruchomić ww. komendę z poziomu naszego rozszerzenia? Pierwszą rzeczą, o której należy wspomnieć w tym miejscu, jest zmiana charakteru klasy implementującej interfejs IPlugin. W poprzednich odcinkach cyklu poświęconego wzorcom projektowym była ona odpowiedzialna za uruchamianie logiki biznesowej w bezpośredni sposób (np. korzystając z metod udostępnianych przez serwisy domenowe). Zastosowanie komend daje nam możliwość zmiany jej odpowiedzialności i sprowadzenie jej do roli hmmm… Invokera (wybaczcie angielszczyznę, ale żadne sensowne polskie słowo nie przychodzi mi w tym momencie do głowy).
public class LeadPostCreateHandler : PluginBase { public override void RegisterCommands(CdsCommandFactory commandFactory, List<ICdsCommand> registeredActions) { registeredActions.Add(commandFactory.GetCommand<TryCreateTaskCommand>()); } }
W powyższym przykładzie widzimy, że główna klasa reprezentująca rozszerzenie zawiera jedną przeciążoną metodę RegisterCommands. Za pomocą fabryki tworzymy instancję interesującej nas komendy. Następnie dodajemy ją do kolekcji zarejestrowanych akcji związanych z obsługiwanym przez plugin zdarzeniem (w naszym przypadku będzie to utworzenie nowego namiaru w systemie). Chcąc zarejestrować kolejne komendy – po prostu dodajemy je do wspomnianej kolekcji. W dołączonej do tego artykułu przykładowej aplikacji wykonywane będą one sekwencyjnie w kolejności dodawania do kolekcji. Nic nie stoi jednak na przeszkodzie, aby rozszerzyć metodę odpowiedzialną ich dodawanie o dodatkowy parametr decydujący o kolejności wykonania zarejestrowanych poleceń.
W jaki sposób osiągnęliśmy przedstawiony efekt? Fabryka komend, którą wykorzystujemy do tworzenia instancji obiektów, wygląda w następujący sposób:
public class CdsCommandFactory { protected ICdsServiceProvider serviceProvider { get; private set; } protected ICdsRepositoryFactory cdsRepositoryFactory { get; private set; } public CdsCommandFactory(ICdsServiceProvider serviceProvider, ICdsRepositoryFactory cdsRepositoryFactory) { this.serviceProvider = serviceProvider; this.cdsRepositoryFactory = cdsRepositoryFactory ?? new CdsRepositoryFactory(this.serviceProvider); } public ICdsCommand GetCommand<T>() where T : CdsCommandBase, new() { T command = new T(); command.Initialize(this.serviceProvider.Context, this.serviceProvider.TracingService, this.cdsRepositoryFactory); return command; } }
Generyczna metoda GetCommand odpowiedzialna jest za utworzenie nowego obiektu dziedziczącego po klasie CdsCommandBase. Przekazuje ona również do jego instancji kontekst uruchomieniowy rozszerzenia oraz dodatkowe atrybuty takie jak instancja TracingService’u i fabryka repozytoriów danych. Oczywiście lista ww. przekazywanych atrybutów może być modyfikowana w zależności od Waszych potrzeb.
Najważniejszą klasą, która umożliwia uruchamianie komend, jest oczywiście nasza klasa bazowa PluginBase implementująca interfejs IPlugin. W metodzie Execute tejże klasy tworzona jest nowa kolekcja komend. Następnie wywoływana jest abstrakcyjna metoda RegisterCommands odpowiedzialna za dodawanie do wspomnianej kolekcji komend obsługujących zdarzenie. W końcu dla każdej komendy uruchamiane są metody CanExecute oraz (w przypadku spełnienia zakładanych warunków wejściowych) Execute.
public abstract class PluginBase: IPlugin { protected ICdsRepositoryFactory CdsUnitOfWorkRepository { get; private set; } public abstract void RegisterCommands(CdsCommandFactory commandsFactory, List<ICdsCommand> registeredCommands); public void Execute(IServiceProvider serviceProvider) { using (var crmSericeProvider = new CdsServiceProvider(serviceProvider)) { var commandsFactory = new CdsCommandFactory(crmSericeProvider, this.CdsUnitOfWorkRepository); var registeredCommands = new List<ICdsCommand>(); RegisterCommands(commandsFactory, registeredCommands); foreach (ICdsCommand command in registeredCommands) { var commandName = command.GetType().Name.ToString(); if (command.CanExecute()) { crmSericeProvider.TracingService.Trace($"{commandName} - execution started"); command.Execute(); crmSericeProvider.TracingService.Trace($"{commandName} - execution completed" ); } } } } }
Opisywane powyżej wzorce oraz techniki miałem okazję wykorzystywać już przy okazji kilku projektów wdrożeniowych. Osobiście uważam, że sprawdzają się one doskonale i pozwalają niewielkim nakładem pracy utrzymać porządek w implementowanym i rozrastającym się w sposób ciągły kodzie. Dodatkowo umożliwiają łatwą implementację wstrzykiwania zależności (w prezentowanym powyżej kodzie, z uwagi na przejrzystość prezentowanych przykładów, nie wykorzystuję wspomnianej techniki). Dzięki temu uzyskujemy możliwość łatwego tworzenia automatycznych testów. Dodatkowo – bardzo łatwo integruje się ona z innymi wzorcami oraz mechanizmami, które są często wykorzystywane w tworzeniu rozszerzeń do aplikacji Dynamics 365 (serwisy, repozytoria, „managery” itp.).
Specjalne podziękowania dla Pawła Grądeckiego, dzięki któremu kilka lat temu pierwszy raz miałem okazję zapoznać się z opisywanymi w artykule technikami. Rispekt man!
Całość kodu prezentowanego w powyższym artykule znajdziecie pod adresem: https://github.com/gashupl/dyn365devbestpractices/tree/master/XrmLabs.Blog.Dyn365BestPractices/Chapter06/
Pingback: dotnetomaniak.pl
Super wpis 🙂 No i dzięki za wyróżnienie 🙂 oby więcej w ten sposób robionych projektów, dużo mniejszy bałagan, dużo łatwiej połapać się co i jak. Chociaż jest jeszcze parę CRMowych dinozaurów które by najchętniej robiły statyczne helperki, ale już na szczęście coraz mniej 🙂