This article describes how object roles can be indicated by the use of Role Interfaces.

In my overview article on Role Hints I described how making object roles explicit can help making code more object-oriented. One way code can convey information about the role played by an object is by implementing one or more Role Interfaces. As the name implies, a Role Interface describes a role an object can play. Classes can implement more than one Role Interface.

Example: Selecting a shipping Strategy

As an example, consider the shipping Strategy selection problem from the previous post. That example seemed to suffer from the Feature Envy smell because the attribute had to expose the handled shipping method as a property in order to enable the selection mechanism to pick the right Strategy.

Another alternative is to define a Role Interface for matching objects to shipping methods:

public interface IHandleShippingMethod
{
    bool CanHandle(ShippingMethod shippingMethod);
}

A shipping cost calculator can implement the IHandleShippingMethod interface to participate in the selection process:

public class ExpressShippingCostCalculator : 
    IBasketCalculator, IHandleShippingMethod
{
    public int CalculatePrice(ShoppingBasket basket)
    {
        /* Perform some complicated price calculation based on the
         * basket argument here. */
        return 1337;
    }
 
    public bool CanHandle(ShippingMethod shippingMethod)
    {
        return shippingMethod == ShippingMethod.Express;
    }
}

An ExpressShippingCostCalculator object can play one of two roles: It can calculate basket prices and it can handle basket calculations related to shipping methods. It doesn't have to expose as a property the shipping method it handles, which enables some more sophisticated scenarios like handling more than one shipping method, or handling a certain shipping method only if some other set of conditions are also met.

You can implement the selection algorithm like this:

public class ShippingCostCalculatorFactory : IShippingCostCalculatorFactory
{
    private readonly IEnumerable<IBasketCalculator> candidates;
 
    public ShippingCostCalculatorFactory(
        IEnumerable<IBasketCalculator> candidates)
    {
        this.candidates = candidates;
    }
 
    public IBasketCalculator GetCalculator(ShippingMethod shippingMethod)
    {
        return (from c in this.candidates
                let handles = c as IHandleShippingMethod
                where handles != null
                && handles.CanHandle(shippingMethod)
                select c).First();
    }
}

Notice that because the implementation of CanHandle can be more sophisticated and conditional on the context, more than one of the candidates may be able to handle a given shipping method. This means that the order of the candidates matters. Instead of selecting a Single item from the candidates, the implementation now selects the First. This provides a fall-through mechanism where a preferred, but specialized candidate is asked before less preferred, general-purpose candidates.

This particular definition of the IHandleShippingMethod interface suffers from the same tight coupling to the ShippingMethod enum as the previous example. One fix may be to define the shipping method as a string, but you could still successfully argue that even implementing an interface such as IHandleShippingMethod in a Domain Model object mixes architectural concerns. Detached metadata might still be a better option.

Summary

As the name implies, a Role Interface can be used as a Role Hint. However, you must be wary of pulling in disconnected architectural concerns. Thus, while a class can implement several Role Interfaces, it should only implement interfaces defined in appropriate layers. (The word 'layer' here is used in a loose sense, but the same considerations apply for Ports and Adapters: don't mix Port types and Adapter types.)


Comments

Could you expand upon how you might implement the fall-through mechanism you mentioned in your IShippingCostCalculatorFactory implementation, where more than one candidate can handle the shippingMethod?

How would you sort your IEnumerable<IBasketCalculator> candidates in GetCalculator() so that the candidate returned by First() is the one specifically meant to handle the ShippingMethod when one exists, and the default implementation when a specific one doesn't exist?

I considered using FirstOrDefault(), then returning the default implementation if the result of the query was nothing, but my default IHandleShippingMethod implementation always returns True from CanHandle() - I don't know what other value it could return.

2013-08-14 15:37 UTC

You could have super-specialized IBasketCalculator implementations, that (e.g.) are only active certain times of day, and you could have the ones I've shown here, and then you could have a fallback that implements IHandleShippingMethod.CanHandle by simply returning true no matter what the input is. If you put this fallback implementations last in the injected candidates, it's only going to picked up by the First method if no other candidate (before it) returns true from CanHandle.

Thus, there no reason to sort the candidates within GetCalculator - in fact, it would be an error to do so. As I wrote above, "the order of the candidates matters."

2013-08-14 17:39 UTC


Wish to comment?

You can add a comment to this post by sending me a pull request. Alternatively, you can discuss this post on Twitter or Google Plus, or somewhere else with a permalink. Ping me with the link, and I may add it as a comment.

Published

Thursday, 10 January 2013 10:37:35 UTC

Tags



"Our team wholeheartedly endorses Mark. His expert service provides tremendous value."
Hire me!