Performing transformations of one object type to another type is a very common task in programming. It might be publishing an event based on a command, or an externally known DTO from an internal DTO. It's pretty common to see some use of the if or switch keywords to determine the code flow. I thought I'd take a minute to show how we can go from a typical implementation using if statements to one which uses Linq and AutoMapper to reduce the coupling in the implementation.
The example code uses the following tools:
- NUnit
- NUnit Test Adapter for VS2012 and VS2013
- Fluent Assertions
- FakeItEasy
- AutoMapper
For this example, we'll have three different publishers. Each will implement a common interface: IPublisher. The implementations will be responsible for accepting a Command object and publishing the associated Event object. We'll be using two commands and events: Start -> Started, Stop -> Stopped.
The Publish method on the IPublisher interface is intentionally not using a generic declaration.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
namespace PolyMap | |
{ | |
public interface IPublisher | |
{ | |
void Publish(Command command); | |
} | |
public abstract class Command | |
{ | |
public Guid Id { get; set; } | |
} | |
public abstract class Event | |
{ | |
public Guid Id { get; set; } | |
} | |
} |
The first Publisher accepts a command. It checks the type of the command received, and calls the appropriate overload. Each overloaded method creates the appropriate event, and publishes it.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
namespace PolyMap | |
{ | |
public class OverloadedPublisher : IPublisher | |
{ | |
private readonly IBus bus; | |
public OverloadedPublisher(IBus bus) | |
{ | |
this.bus = bus; | |
} | |
public void Publish(Command command) | |
{ | |
var commandType = command.GetType(); | |
if (commandType == typeof(Start)) | |
Publish((Start)command); | |
if (commandType == typeof(Stop)) | |
Publish((Stop)command); | |
} | |
private void Publish(Start command) | |
{ | |
bus.Publish(new Started { Id = command.Id }); | |
} | |
private void Publish(Stop command) | |
{ | |
bus.Publish(new Stopped { Id = command.Id }); | |
} | |
} | |
} |
This works, but it has a few problems. It both uses an if to determine which type to publish, and manually maps the inbound command to the outbound event. That means this class is responsible for both determining what kind of event to publish and creating that event.
Adding AutoMapper
AutoMapper removes the responsibility of creating the event from the publisher class. AutoMapper Profiles could be used to map more complex associations, but the DynamicMap method works just fine here. Our publisher class is relieved of this responsibility, limiting it to just sorting out the type of event to be published.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
namespace PolyMap | |
{ | |
using AutoMapper; | |
public class OverloadedAutomappingPublisher : IPublisher | |
{ | |
private readonly IBus bus; | |
private readonly IMappingEngine mappingEngine; | |
public OverloadedAutomappingPublisher(IBus bus, IMappingEngine mappingEngine) | |
{ | |
this.bus = bus; | |
this.mappingEngine = mappingEngine; | |
} | |
public void Publish(Command command) | |
{ | |
var commandType = command.GetType(); | |
if (commandType == typeof(Start)) | |
Publish((Start)command); | |
if (commandType == typeof(Stop)) | |
Publish((Stop)command); | |
} | |
private void Publish(Start command) | |
{ | |
var @event = mappingEngine.DynamicMap<Started>(command); | |
bus.Publish(@event); | |
} | |
private void Publish(Stop command) | |
{ | |
var @event = mappingEngine.DynamicMap<Stopped>(command); | |
bus.Publish(@event); | |
} | |
} | |
} |
Removing the 'if'
Introducing a map from the commands and a Linq query allows us to remove the if statements. The class is still responsible for selecting the appropriate action. The concept of associating commands to events is distilled into the dictionary. This leaves the class' methods to simply select the appropriate action and execute it.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
namespace PolyMap | |
{ | |
using System; | |
using System.Collections.Generic; | |
using System.Linq; | |
using AutoMapper; | |
public class MappedPublisher : IPublisher | |
{ | |
private readonly IBus bus; | |
private readonly IMappingEngine mappingEngine; | |
private readonly Dictionary<Type,Action<Command>> publishMap = new Dictionary<Type, Action<Command>>(); | |
public MappedPublisher(IBus bus, IMappingEngine mappingEngine) | |
{ | |
this.bus = bus; | |
this.mappingEngine = mappingEngine; | |
publishMap.Add(typeof(Start), Publish<Started>); | |
publishMap.Add(typeof(Stop), Publish<Stopped>); | |
} | |
public void Publish(Command command) | |
{ | |
var publishAction = GetPublishAction(command.GetType()); | |
publishAction(command); | |
} | |
private Action<Command> GetPublishAction(Type commandType) | |
{ | |
var publishAction = (from a in publishMap | |
where commandType == a.Key | |
select a.Value).Single(); | |
return publishAction; | |
} | |
private void Publish<TEvent>(Command command) where TEvent : Event | |
{ | |
var toPublish = mappingEngine.DynamicMap<TEvent>(command); | |
bus.Publish(toPublish); | |
} | |
} | |
} |
Wrapping It Up
This was a quick demonstration of removing two concerns from a class. The manual mapping of one class to another by introducing AutoMapper. The if statement was removed by introducing a map between the two types. I hope this helped describe a different was of building classes with reduced responsibilities.