Command pattern

Den här posten, den sista i den här miniserien, avhandlar Command Pattern. Introduktionen till serien hittar du här och posten om Query Object Pattern finns här.

Den främsta anledningen till att man ofta talar om dom här två mönstren tillsammans är att dom liknar varandra väldigt mycket, iallafall när det handlar om koddesign och gränssnitten som dom båda mönstren lutar sig mot.

Den absolut största skillnaden mellan mönstren är dock följande:

  • Query Object Pattern läser data, ställer frågor mot ett datalager, utan att påverka modellerna varken i domänen eller i datalagret
  • Command Pattern modifierar data i domänen och oftast även i datalagret.

Vi hoppar direkt vidare till att titta på kod och hur den här implementationen av mönstret ser ut. Koden hittar du här https://github.com/HeadlightAB/QueryObjectAndCommandPattern.

Grundstruktur

Grundstrukturen, som namnet antyder, består i att man vill få kommandon utförda, oftast kommer dessa att modifiera data i domänen och oftast kommer det modifierade datat att persisteras i ett datalager.

Grunden i mönstret är alltså Commands som implementerar följande interface, ICommand:

public interface ICommand<TDomainModel, TDataAccess> where TDataAccess : IDataAccess  
{
    void Execute(TDomainModel domainModel, TDataAccess dataAccess);
}

public interface IDataAccess  
{
    void Store<TEntity>(TEntity entity);
}

Notera interfacet IDataAccess, som innehåller en metod för att spara entiteter.

Vi ser att metoden Execute tar in en domänmodel, som den ska modifiera, tillsammans med datalagret där en entitet som motsvarar domänmodellen ska sparas. Tanken med mönstret är att varje modellförändrande operation motsvaras av en implementation av ICommand, vilket gör att rättningar av eventuella fel endast påverkar det aktuella kommandot samt att bygga ut systemet med fler kommandon inte på något sätt påverkar dom existerande kommandona. Varje operation har alltså sin helt egen silo.

Implementation

Nedan följer ett exempel där en bil ska underkännas i en besiktning, genom att på domänobjektet applicera ett kommando. Datalagret i exemplet utgörs av en lista i en static variabel i en implementation av IDataAccess:

  • CarInspectionFailed implementerar ICommand,
  • som opererar på ett Car-objekt i domänen och
  • sparar en entitet i en InMemoryDataSource
namespace CommandPattern.Domain.Commands  
{
    public class CarInspectionFailed : ICommand<Domain.Models.Car, InMemoryDataSource>
    {
        private readonly DateTimeOffset _when;

        public CarInspectionFailed(DateTimeOffset when)
        {
            _when = when;
        }

        public void Execute(Domain.Models.Car domainModel, InMemoryDataSource dataAccess)
        {
            domainModel.ApplyInspectionFailed(_when);

            dataAccess.Store(new DataAccess.Entities.Car
            {
                RegNo = domainModel.RegNo, 
                InspectionApproved = domainModel.InspectionApproved,
                InspectedAt = domainModel.InspectedAt
            });
        }
    }
}

namespace CommandPattern.Domain.Models  
{
    public class Car
    {
        public string RegNo { get; }
        public bool InspectionApproved { get; private set; }
        public DateTimeOffset InspectedAt { get; private set; }

        public Car(string regNo)
        {
            RegNo = regNo;
        }

        public void ApplyInspectionFailed(DateTimeOffset when)
        {
            InspectedAt = when;
            InspectionApproved = false;
        }
        ...
    }
}

namespace CommandPattern.DataAccess.DataSources  
{
    public class InMemoryDataSource : IDataAccess
    {
        public IQueryable<Entities.Car> Cars { get; } = new List<Entities.Car>
        {
            new Entities.Car {RegNo = "GLW975"},
            new Entities.Car {RegNo = "RNY293"},
            new Entities.Car {RegNo = "TSP372"}
        }.AsQueryable();

        public void Store<TEntity>(TEntity entity)
        {
            // Type safety checks and other checks not included here
            ...
            var carStored = Cars.Single(x => x.RegNo == car.RegNo);
            carStored.InspectedAt = car.InspectedAt;
            carStored.InspectionApproved = car.InspectionApproved;            
        }
    }
}

namespace CommandPattern.DataAccess.Entities  
{
    public class Car
    {
        public string RegNo { get; set; }
        public bool InspectionApproved { get; set; }
        public DateTimeOffset InspectedAt { get; set; }
    }

Värt att notera här är att Domain.Models.Car inte har några properties som är öppna för modifiering utan all förändring sker via metoder, i det här fallet metoden ApplyInspectionFailed. Här skulle man till exempel kunna hantera förändringarna genom events, i stil med det som finns på write-sidan i CQRS och Event sourcing.

Det här kan se lite komplicerat ut, men låt oss titta på en användning av ett kommando så syns det bättre hur rent gränssnittet är.

Användning

I förra posten, om Query Object Pattern, visades användningen av en query i ett ASP.NET Core Web API. För att visa motsvarande för Command Pattern tittar vi istället på ett enhetstest av kommandot ovan, CarInspectionFailed. Enhetstestet använder sig av xunit och FluentAssertions:

using CommandPattern.Domain.Commands;  
using CommandPattern.Domain.Models;  
using FluentAssertions;  
using Xunit;

namespace CommandPattern.Tests  
{
    public class CarInspectionFailedTests
    {
        private const string RegNo = "GLW975";

        private readonly CarInspectionFailed _sut;
        private readonly DateTimeOffset _inspectedAt;

        public CarInspectionFailedTests()
        {
            _inspectedAt = DateTimeOffset.Now;
            _sut = new CarInspectionFailed(_inspectedAt);
        }

        [Fact]
        public void CarInspectionFailedTest()
        {
            var domainModel = new Car(RegNo);
            var dataSource = new InMemoryDataSource();

            _sut.Execute(domainModel,  dataSource);

            var containSingle = dataSource.Cars.Should().ContainSingle(x => x.RegNo == RegNo).Subject;
            containSingle.InspectedAt.Should().Be(_inspectedAt);
            containSingle.InspectionApproved.Should().BeFalse();
        }
        ...
    }
}


Vad hände här?

  • Det första som händer i testet, i konstruktorn, är att besiktningen sker nu, _inspectedAt = DateTimeOffset.UtcNow.
  • _sut i testet är en instans av CarInspectionFailed, dvs ett kommando som ska markera bilen som underkänd i besiktningen.
  • Datakällan är en InMemoryDataSource.
  • Kommandot utförs vid anropet _sut.Execute(...) på domänobjektet i variabeln domainModel.
  • Efter anropet till Execute kontrolleras så att datakällan innehåller den uppdaterade entiteten.

Den uppmärksamme reagerade nog på att kontrollen efter att kommandot har exekverats görs på datakällan. Det strider lite mot grunden i ett enhetstest, att kontrollera resultatet utanför själva enheten som ska testas. Därför kommer här ett modifierat exempel, med två testfall, där kontrollerna görs på rätt sida gränsen. I första testet kontrolleras så att datakällan mottagit ett förväntat anrop och i det andra testet görs motsvarande kontroll på domänobjektet. Substitueringen görs i båda fallen med hjälp av NSubsitute:

...
using FluentAssertions;  
using NSubstitute;  
using Xunit;

namespace CommandPattern.Tests  
{
    public class CarInspectionFailedTests
    {
        private const string RegNo = "GLW975";

        private readonly CarInspectionFailed _sut;
        private readonly DateTimeOffset _inspectedAt;

        public CarInspectionFailedTests()
        {
            _inspectedAt = DateTimeOffset.Now;
            _sut = new CarInspectionFailed(_inspectedAt);
        }

        ...

        [Fact]
        public void CarInspectionFailed_ShouldInvokeDataSource()
        {
            var domainModel = new Car(RegNo);
            var dataSource = Substitute.For<InMemoryDataSource>();

            _sut.Execute(domainModel, dataSource);

            dataSource.Received().Store(Arg.Is<DataAccess.Entities.Car>(car =>
                car.InspectionApproved == false &&
                car.RegNo == RegNo &&
                car.InspectedAt == _inspectedAt));
        }

        [Fact]
        public void CarInspectionFailed_ShouldApplyDomainModelChanges()
        {
            var dataSource = Substitute.For<InMemoryDataSource>();
            var domainModel = Substitute.For<Car>(RegNo);

            _sut.Execute(domainModel, dataSource);

            domainModel.Received().ApplyInspectionFailed(_inspectedAt);
        }
    }
}

Det ser en smula märkligt ut att substituera dom riktiga klassinstanserna domainModel och inMemoryDataSource, men det är fullt möjligt så länge metoderna man ska kontrollera är virtual i klassen. Så är numera fallet i Domain.Models.Car och DataAccess.DataSources.InMemoryDataSource.

Gränssnittet för att uppdatera ett domänobjekt på ett kontrollerat sätt samt att persistera detta i ett datalager består alltså i två rader kod:

  • skapa ett kommando:
var command = new CarInspectionFailed(inspectedAt);
  • exekvera det:
command.Execute(domainModel, dataSource)

Notera gränssittets likhet med Query Object Pattern, där en query-instans först skapas och sedan exekveras frågan som är implementerad i queryn.

Fullständig källkod för miniserien
https://github.com/HeadlightAB/QueryObjectAndCommandPattern.