06: Wzorzec Komendy (Polecenia) w programowaniu rozszerzeń platformy Dynamics 365 CE / Common Data Service
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/

Like
Report
*This post is locked for comments