vendredi 12 avril 2019

Refactoring an interface exposing several possible behaviors but only one can be ever called per instantiation context

Sorry for the lenghty post. I tried to show my attempts and thought process as much as possible.

I got an interface exposing several possible behavior, but there is only one implementation of this interface that is instantiated and only one of the exposed method that can be called in each context where the interface is realized. This interface will be used in very different context of an application and I wish to avoid exposing method that can't be called. I wish to find a way so that the caller of IRescheduler would only know one behavior despite different method signatures. I'll detail and example and what I tried so far

public interface IRescheduler
{
    AmountByTimeInterval RescheduleTomorrow(Amount amount);
    AmountByTimeInterval RescheduleAtGivenDate(Amount amount, DateTime rescheduleDate);
    // there will probably be more date strategies in the future
}

AmountByTimeInterval contains an Amount and a TimeInterval associates a string with a timespan from the current date. For example "1Day" would be the timespan from tomorrow to tomorrow and "1Year" would starts a year from now and ends a year later.

public class AmountByTimeInterval
{
    public Amount Amount { get; private set; }
    public TimeInterval TimeInterval { get; private set; }

    public AmountByTimeInterval(Amount amount, TimeInterval timeInterval)
    {
        Amount = amount;
        TimeInterval = timeInterval;
    }
}

public class Amount
{
    public double Value { get; private set; }
    public string Currency { get; private set; }

    public Amount(double amount, string currency)
    {
        Value = amount;
        Currency = currency;
    }
}

public class TimeInterval
{
    public string Name { get; private set; }
    public DateTime StartDate { get; private set; }
    public DateTime EndDate { get; private set; }

    public TimeInterval(string name, DateTime startDate, DateTime endDate)
    {
        Name = name;
        StartDate = startDate;
        EndDate = endDate;
    }
}

For the sake of this example, let's suppose an IRescheduleAmountCalculator interface that takes Amount to make other Amount

public interface IRescheduleAmountCalculator
{
    Amount ComputeRescheduleAmount(Amount amount);
}

Here is an example implementation of my IRescheduler interface. I got a repository pattern that gets me the TimeInterval associated to the DateTime.

public interface ITimeIntervalRepository
{
    TimeInterval GetTimeIntervalByName(string name);
    TimeInterval GetTimeIntervalByDate(DateTime date);
}

public class Rescheduler : IRescheduler
{
    private const string _1Day = "1Day";
    private readonly ITimeIntervalRepository _timeIntervalRepository;
    private readonly TimeInterval _tomorrow;
    private readonly IRescheduleAmountCalculator _calculator;

    public Rescheduler (ITimeIntervalRepository timeIntervalRepository, IRescheduleAmountCalculator calculator)
    {
        _calculator = calculator;
        _timeIntervalRepository = timeIntervalRepository;
        _tomorrow = timeIntervalRepository.GetTimeIntervalByName(_1Day);
    }

    public BucketAmount RescheduleTomorrow(Amount amount)
    {
        Amount rescheduledAmount = _calculator.ComputeRescheduleAmount(amount);
        return new TimeInterval(_tomorrow, transformedAmount);
    }

    public AmountByTimeInterval RescheduleAtGivenDate(Amount amount, DateTime reschedulingDate)
    {
        TimeInterval timeInterval = _timeIntervalRepository.GetTimeIntervalByDate(reschedulingDate);
        Amount rescheduledAmount = _calculator.ComputeRescheduleAmount(amount);
        return new TimeInterval(timeInterval, transformedAmount);
    }
}

I don't know beforehand the context in which IRescheduler would be called, it is meant to be used by many components. Here is an abstract class I intend to provide and an example of specific implementation

public abstract class AbstractReschedule<TInput, TOutput>
{
    private readonly ITransformMapper<TInput, TOutput> _mapper;
    protected readonly IRescheduler Rescheduler;

    protected AbstractReschedule(IMapper<TInput, TOutput> mapper, IRescheduler rescheduler)
    {
        _mapper = mapper;
        Rescheduler = rescheduler;
    }

    public abstract TOutput Reschedule(TInput entityToReschedule);

    protected TOutput MapRescheduledEntity(TInput input, TimeInterval timeInterval)
    {
        return _mapper.Map(input, timeInterval);
    }
}



public class RescheduleImpl : AbstractReschedule<InputImpl, OutputImpl>
{
    public RescheduleImpl(IRescheduleMapper<InputImpl, OutputImpl> mapper, IRescheduler rescheduler) : base(mapper, rescheduler)
    {
    }

    public override OutputImpl Reschedule(InputImpl entityToReschedule)
    {
        AmountByTimeInterval rescheduledAmountByTimeInterval = Rescheduler.RescheduleTomorrow(entityToReschedule.AmountByTimeInterval.Amount);
        return Map(entityToReschedule, rescheduledAmountByTimeInterval);
    }
}

public interface IMapper<T, TDto>
{
    TDto Map(T input, AmountByTimeInterval amountByTimeInterval);
}

Forcing an interface on TInput generic parameter is out of question, as the component is meant to be used in a large number of bounded contexts. Each future user of this whole rescheduling component would implement its own implementation of AbstractReschedule and IMapper.

I tried a strategy pattern but the different method argument blocked me as I couldn't define an interface contract that would allow all behaviour without exposing the actual implementation of IRescheduler.

Then I implemented a visitor pattern, where IRescheduler would have an Accept method and an implementation by behavior :

public interface IRescheduler
{
    AmountByTimeInterval Accept(IReschedulerVisitor visitor, Amount amount);
}

public class RescheduleTomorrow : IRescheduler
{
    public AmountByTimeInterval Accept(IReschedulerVisitor visitor, Amount amount)
    {
        return visitor.Visit(this, amount);
    }
}

public class RescheduleAtGivenDate : IRescheduler
{
    public AmountByTimeInterval Accept(IReschedulerVisitor visitor, Amount amount)
    {
        return visitor.Visit(this, amount);
    }
}

As you noticed, the DateTime is not present here, because I actually inject it in the visitor, which is built by a Factory

public interface IReschedulerVisitor
{
    AmountByTimeInterval Visit(RescheduleTomorrow rescheduleTomorrow, Amount amount);
    AmountByTimeInterval Visit(RescheduleAtGivenDate rescheduleAtGivenDate, Amount amount);
}

public class ReschedulerVisitor : IReschedulerVisitor
{

    private readonly ITimeIntervalRepository _timeIntervalRepository;
    private readonly DateTime _chosenReschedulingDate;
    private readonly IRescheduleAmountCalculator _rescheduleAmountCalculator;
    private const string _1D = "1D";

    public ReschedulerVisitor(ITimeIntervalRepository timeIntervalRepository, IRescheduleAmountCalculator rescheduleAmountCalculator)
    {
        _timeIntervalRepository = timeIntervalRepository;
        _rescheduleAmountCalculator = rescheduleAmountCalculator
    }

    public ReschedulerVisitor(ITimeIntervalRepository timeIntervalRepository, IRescheduleAmountCalculator rescheduleAmountCalculator, DateTime chosenReschedulingDate)
    {
        _timeIntervalRepository = timeIntervalRepository;
        _chosenReschedulingDate = chosenReschedulingDate;
        _rescheduleAmountCalculator = rescheduleAmountCalculator
    }

    public AmountByTimeInterval Visit(RescheduleTomorrow rescheduleTomorrow, Amount amount)
    {
        TimeInterval reschedulingTimeInterval = _timeIntervalRepository.GetTimeIntervalByName(_1D);
        Amount rescheduledAmount = _rescheduleAmountCalculator(amount);
        return new AmountByTimeInterval(reschedulingTimeInterval, rescheduledAmount); 
    }

    public AmountByTimeInterval Visit(RescheduleAtGivenDate rescheduleAtGivenDate, Amount amount)
    {
        TimeInterval reschedulingTimeInterval = _timeIntervalRepository.GetTimeIntervalByDate(_chosenReschedulingDate);
        Amount rescheduledAmount = _rescheduleAmountCalculator(amount);
        return new AmountByTimeInterval(reschedulingTimeInterval, rescheduledAmount); 
    }
}

public interface IRescheduleVisitorFactory
{
    IRescheduleVisitor CreateVisitor();
    IRescheduleVisitor CreateVisitor(DateTime reschedulingDate);
}

public class RescheduleVisitorFactory : IRescheduleVisitorFactory
{
    private readonly ITimeIntervalRepository _timeIntervalRepository;

    public RescheduleVisitorFactory(ITimeIntervalRepository timeIntervalRepository)
    {
        _timeIntervalRepository = timeIntervalRepository;
    }

    public IRescheduleVisitor CreateVisitor()
    {
        return new RescheduleVisitor(_timeIntervalRepository);
    }

    public IRescheduleVisitor CreateVisitor(DateTime reschedulingDate)
    {
        return new RescheduleVisitor(_timeIntervalRepository, reschedulingDate);
    }
}

Finally (sorry for lengthy post), the RescheduleImpl that every user would have to implement would become like this :

public class RescheduleImpl : AbstractReschedule<InputImpl, OutputImpl>
{
    public RescheduleImpl(IRescheduler rescheduler, IRescheduleVisitorFactory visitorFactory, IRescheduleMapper<InputImpl, OutputImpl> mapper)
        : base(cancel, visitorFactory, mapper) {}

    public override OutputImpl Reschedule(InputImpl entityToReschedule)
    {
        AmountByTimeInterval rescheduledAmountByTimeInterval = rescheduler.Accept(visitorFactory.CreateVisitor(), entityToReschedule.AmountByTimeInterval.Amount);
        // the second case would be :
        // AmountByTimeInterval rescheduledAmountByTimeInterval = rescheduler.Accept(visitorFactory.CreateVisitor(entityToReschedule.Date), entityToReschedule.AmountByTimeInterval.Amount);
        return Mapper.Map(entityToReschedule, rescheduledAmountByTimeInterval);
    }
}

While this works, I'm quite unhappy with the solution. I feel like the implementer of my solution would decide of the rescheduling strategy twice. The first time when chosing the implementation of IRescheduler to use build the last RescheduleImpl class I showed, and a second time when deciding which method of the factory to call. I'm currently out of ideas and open to any that could solve the original problem. I'm also open to totally different implementation than my visitor + factory attempt.

Thank you for taking the time to read or answer my problem.

Aucun commentaire:

Enregistrer un commentaire