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/

Total Views: 2096 ,
This Article Has 2 Comments
  1. Pingback: dotnetomaniak.pl

  2. G

    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 🙂

Comments are now closed.