Рефакторинг интерфейса, демонстрирующий несколько возможных вариантов поведения, но только один из них может быть вызван в каждом контексте - PullRequest
0 голосов
/ 12 апреля 2019

Извините за длинный пост. Я старался показать свои попытки и мыслительный процесс как можно больше.

Я получил интерфейс, демонстрирующий несколько возможных вариантов поведения, но существует только одна реализация этого интерфейса, которая была реализована, и только один из представленных методов, которые можно вызывать в каждом контексте, где реализован интерфейс. Этот интерфейс будет использоваться в совершенно ином контексте приложения, и я хочу избежать раскрытия метода, который нельзя вызвать. Я хочу найти способ, чтобы вызывающий IRescheduler мог знать только одно поведение, несмотря на разные сигнатуры методов. Я подробно опишу и пример и то, что я пробовал до сих пор

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

AmountByTimeInterval содержит Amount, а TimeInterval связывает строку с интервалом времени от текущей даты. Например, "1Day" будет временным интервалом с завтрашнего дня до завтра, а "1Year" начнется через год и закончится годом позже.

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;
    }
}

Для примера рассмотрим интерфейс IRescheduleAmountCalculator, который Amount использует для создания других Amount

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

Вот пример реализации моего IRescheduler интерфейса. У меня есть шаблон репозитория, который связывает меня TimeInterval с 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);
    }
}

Я не знаю заранее, в каком контексте будет вызываться IRescheduler, он предназначен для использования многими компонентами. Вот абстрактный класс, который я собираюсь предоставить, и пример конкретной реализации

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);
}

Использование интерфейса для общего параметра TInput не подлежит сомнению, так как компонент предназначен для использования в большом количестве ограниченных контекстов. Каждый будущий пользователь всего этого компонента перепланирования будет реализовывать свою собственную реализацию AbstractReschedule и IMapper.

Я попробовал шаблон стратегии , но другой аргумент метода заблокировал меня, так как я не мог определить интерфейсный контракт, который позволял бы все поведение, не подвергая действительной реализации IRescheduler.

Затем я реализовал шаблон посетителя, где IRescheduler будет иметь Accept метод и реализацию по поведению:

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);
    }
}

Как вы заметили, DateTime здесь отсутствует, потому что я на самом деле вставляю его в посетителя , который построен на 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);
    }
}

Наконец (извините за длинный пост), RescheduleImpl, который должен был бы реализовать каждый пользователь, выглядел бы так:

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);
    }
}

Хотя это работает, я весьма недоволен решением. Я чувствую, что разработчик моего решения примет решение о стратегии реструктуризации дважды. Первый раз при выборе реализации IRescheduler для использования сборки последний класс RescheduleImpl, который я показал, и второй раз при решении, какой метод фабрики вызывать. В настоящее время у меня нет идей, и я открыт для любого, кто может решить исходную проблему. Я также открыт для совершенно другой реализации, чем моя попытка посетитель + фабрика.

Спасибо, что нашли время, чтобы прочитать или ответить на мою проблему.

Ответы [ 2 ]

2 голосов
/ 12 апреля 2019

Я думаю, что основная причина, по которой это стало настолько сложным, заключается в следующем:

Я получил интерфейс, демонстрирующий несколько возможных вариантов поведения, но существует только одна реализация этого интерфейса, которая была реализована и только однаоткрытого метода, который можно вызывать в каждом контексте, где реализован интерфейс.

Вот способ перефразировать следующее:

Мне нужно различное поведение в разных контекстах,но я хочу, чтобы все они были в одном интерфейсе.

Ответ не в том, чтобы сделать это.Если вам нужно одно поведение здесь и другое поведение там, лучше определить один интерфейс для поведения, которое вам нужно здесь, и другой для того, что вам нужно там.

Это относится к Принципу разделения интерфейсов .Грубо говоря, это говорит о том, что у нас не должно быть одного класса, зависящего от интерфейса, мы должны использовать some , если его члены.Когда класс зависит от интерфейса, этот интерфейс должен содержать только то, что ему нужно.

Если вы поместите все эти поведения в один интерфейс, то он, вероятно, будет реализован в одном большом классе.И затем каждый раз, когда вам нужно другое поведение, вы добавляете его в этот интерфейс, что означает, что класс, реализующий его, должен измениться.Если этот один класс используется (чтобы делать совершенно разные вещи) многими другими классами, то каждое изменение одного класса может повлиять на другие.

Или вы можете пройти часть пути и понять, что вы хотите перестроить это.Вы можете увидеть какой-то способ упростить или улучшить.Но затем происходит то же самое.Многие классы зависят от этого интерфейса по разным причинам, поэтому теперь одно ваше изменение влияет на множество классов.

Или, проще говоря: я написал этот класс.Я использую части этого в десяти других классах.Следующий класс, с которым я хочу использовать это, нуждается в чем-то немного другом.Поэтому, чтобы удовлетворить потребности одного класса, я собираюсь изменить интерфейс (и реализацию), от которого зависят десять других классов.Это может означать необходимость изменения всех этих классов, и мне не нужно менять десять классов из-за одного.Или изменение может случайно нарушить другие десять классов.

Это волновые эффекты, и провайдер помогает нам минимизировать их, чтобы изменение одной вещи не влияло на нас или не заставляло нас менять другие вещи.

Если существуют разные типы поведения и разные классы нуждаются в разных классах, то лучше «разделить» эти интерфейсы, предоставляя каждому классу только то, что ему нужно.Один из способов сделать это - определить каждый интерфейс с точки зрения класса или классов, которые в них нуждаются.

Иногда мы можем попытаться объединить больше в один класс, чтобы различные типы поведения могли совместно использовать некоторые функции иликод, но есть и другие способы сделать это.Если мы обнаружим, что двум из этих реализаций нужно что-то похожее или идентичное, мы можем просто скопировать эту дублированную часть в отдельный класс, и тогда обе реализации будут зависеть от этого.

Другая причина, по которой этот подход полезен, заключается в том, что он заставляет нас писать только тот код, который нам нужен прямо сейчас.Затем, когда мы пишем один, два, три класса, мы можем обнаружить общие черты и возможности для повторного использования и рефакторинга.Это происходит намного более гладко, чем если бы мы попытались спланировать эту общность заранее, написать код, основанный на этом, и затем, как только мы начнем использовать его в других классах, мы обнаружим, что это не то, что нам нужно.

0 голосов
/ 14 апреля 2019

@ СкоттХаннен прав, что проблема связана с разделением интерфейса.Вам повезло, что у вас есть текущие требования, которые демонстрируют проблему прямо сейчас, вместо того, чтобы узнавать позже и менять целый набор развернутого кода.

Но выявление нарушенного принципа SOLID - это не то же самое, что исправлениепроблема, и я думаю, что вам нужен более практичный ответ, поэтому:

Поскольку разные контексты могут требовать разных служб от используемых ими перепрограммирующих устройств, вы не должны пытаться заставить все перепланировщики использовать один и тот же интерфейс.Вы определили два разных типа прямо сейчас, но нет никаких причин, что не может быть больше позже.

Теперь вы можете подумать, что вам следует разделить свои IRescheduler на INextDayRescheduler и IFutureRescheduler,или что-то еще, и оставьте место для произвольных других позже, НО произвольный интерфейс не дает значения, так что это действительно означает, что вы должны убрать требование для интерфейса IRescheduler, поскольку на самом деле неттакой интерфейс вообще.

Вы используете этот интерфейс в конструкторе AbstractReschedule, но AbstractReschedule не использует его.Так что просто прекрати это.Удалите аргумент конструктора или (если вы пропустили важный код) выберите другой интерфейс, который дает ему именно то, что ему нужно.

С этим одним изменением разработчики AbstractReschedule могут просто сделать это так, как они хотят, иВаша проблема решена.

Если есть много разработчиков, которые могли бы использовать INextDayRescheduler или что-то еще, тогда сделайте несколько удобных служебных классов, которые могут упростить обработку этих общих случаев использования, но всегдаимейте в виду, что это служебные классы, которые клиенты могут использовать или не использовать в соответствии с их прихотями вместо требований вашего API.

...