Как удовлетворить потребность в доступе к конкретным реализациям в сценарии с полиморфизмом?

Я наткнулся на эту проблему, которую я не могу решить должным образом. Вот некоторые пояснения.

Код

У меня есть эти классы продуктов:

public abstract class Product
{
    public int BaseParam {get;set;}
}

public class SpecificProductA : Product
{
    public int ParamA {get;set;}
}

public class SpecificProductB : Product
{
    public int ParamB {get;set;}
}

И у меня есть эти потребительские классы:

public interface IConsumer
{
    void Consume(Product product);
}

public class ConcreteConsumerA : IConsumer
{
    public void Consume(Product product)
    {
        /* I need ParamA of SpecificProductA */
    }
}

public class ConcreteConsumerB : IConsumer
{
    public void Consume(Product product)
    {
        /* I need ParamB of SpecificProductB */
    }
}

Проблема

Мне нужны конкретные реализации интерфейса IConsumer для доступа к определенным частям Продукта. ConcreteConsumerA сможет потреблять только ProductA, а ConcreteConsumerB может потреблять только ProductB. Это разрушает прекрасную абстракцию, которая была у меня с Consumer & Product.

Решение 1. Трансляция

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

Решение 2. Прерывание наследования классов товаров

Другое решение состояло в том, чтобы разорвать наследование Продукта примерно так:

public class Product
{
    public int BaseParam {get;set;}

    public SpecificProductA ProductA {get;set;}

    public SpecificProductB ProductB {get;set;}
}

public class SpecificProductA
{
    public int ParamA {get;set;}
}

public class SpecificProductB
{
    public int ParamB {get;set;}
}

Решение 3. Универсальные методы

Я также могу сделать интерфейс IConsumer универсальным следующим образом:

    public interface IConsumer<TProduct> where TProduct: Product
    {
        void Consume(Product product);
    }

    public class ConcreteConsumerA : IConsumer<SpecificProductA>
    {
        public void Consume(SpecificProductA productA)
        {
            /* I now have access to ParamA of SpecificProductA */
        }
    }

    public class ConcreteConsumerB : IConsumer<SpecificProductB>
    {
        public void Consume(SpecificProductB productB)
        {
            /* I now have access to ParamA of SpecificProductB */
        }
    }

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

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


person Kzrystof    schedule 21.03.2018    source источник
comment
Могут ли параметры также храниться в общедоступном ReadOnlyDictionary<string, int>, где Key — это текущее имя свойства? Таким образом, существующие свойства могут быть прочитаны из ReadOnlyDictionary, но также все свойства могут быть прочитаны, даже из базового класса, через ReadOnlyDictionary.   -  person mjwills    schedule 22.03.2018
comment
@mjwills Это просто другой способ удалить всю статическую типизацию из решения, как если бы вы привели все.   -  person Servy    schedule 22.03.2018
comment
Исправьте @Servy. Программное обеспечение — это компромиссы. Я не говорю, что это великолепный компромисс (поскольку я недостаточно знаю более тонкие детали контекста), но это вариант. Не стесняйтесь опубликовать другой вариант, если он у вас есть.   -  person mjwills    schedule 22.03.2018
comment
Универсальный IConsumer — это решение. Если вы хотите избежать распространения, единственный правильный способ - использовать   -  person Evgeny Gorbovoy    schedule 22.03.2018
comment
@mjwills В контексте ситуации с конфигурацией это определенно можно сделать. Однако в других сценариях, где это объект другого типа, скажем, Продукт, это будет выглядеть странно...   -  person Kzrystof    schedule 22.03.2018
comment
Я согласен @Kzrystof. учитывая, что ваш базовый класс назывался Configuration, казалось разумным предположить, что это была ваша проблемная область. Это действительно продукты вместо этого? Или вы ищете решение, которое справляется с обоими?   -  person mjwills    schedule 22.03.2018
comment
@mjwills На самом деле я дважды столкнулся с этой проблемой. Один с контекстом конфигурации, а другой похож на продукт. Поэтому я ищу решение обеих проблем. поправлю вопрос...   -  person Kzrystof    schedule 22.03.2018


Ответы (3)


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

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

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

person Servy    schedule 21.03.2018
comment
Возможно, здесь проблема: объекты не могут быть объединены, и я попытался сделать их виртуально подходящими, но это просто взорвалось у меня перед носом. Может дизайн надо менять... - person Kzrystof; 22.03.2018
comment
@Kzrystof Ну, вы сказали, что они могут быть объединены в своем вопросе, учитывая, что вы указали это как решение и, кроме указания на то, что оно вам не нравится, вы ничего не сказали о том, что оно не работает. - person Servy; 22.03.2018
comment
Вы правы :) Я недостаточно ясно выразился в своем вопросе. Все решения, которые я предоставил, работали, но оставили у меня неудовлетворительное впечатление. Если проблема заключается в дизайне (например, в неправильном использовании наследования или абстракций), то у меня нет проблем с его изменением. - person Kzrystof; 22.03.2018

Если вы хотите избежать общего распространения, вы можете смягчить ошибки времени выполнения варианта 1, давая потребителю способ узнать, передает ли он правильные типы:

public interface IConsumer
{ 
    bool TryConsume(Product product);
}

public class ConcreteConsumerA : IConsumer
{
    public bool TryConsume(Product product)
    {
        if (product is SpecificProductA a)
        { 
            //consume a
            return true;
        }

        return false;
    }
}
person InBetween    schedule 21.03.2018
comment
Это очень хороший вариант решения 1 :) - person Kzrystof; 22.03.2018
comment
Но что звонящий сможет с этим сделать? Это не исключение, но оно все еще не работает. И если функциональность была обязательной, а не чем-то необязательным, вызывающей стороне, вероятно, нужно будет просто сгенерировать исключение, скорее всего, они ничего другого не могут сделать. Результатом этого решения является то, что вы получили неправильный результат во время выполнения (независимо от того, является ли этот неверный результат исключением или просто неправильным поведением), когда вы могли знать об этой ошибке в своем коде при компиляции. время, не отключая систему типов. В лучшем случае это просто перемещение проблемы. - person Servy; 22.03.2018
comment
@Servy Вот именно. Вариант решения 1. - person Kzrystof; 22.03.2018
comment
@servy да, я согласен, но в некоторых сценариях вы просто не знаете тип среды выполнения, в этом весь смысл, это просто делает использование более приятным. Дженерики — это хорошо, но если у вас есть IConsumer<Product>, как это поможет? Вы все еще в том же тупике. Если вы знаете, что у вас на руках IConsumer<ProductA>, чем это отличается от знания, что у вас на руках ConcreteConsumerA, что делает весь вопрос спорным? - person InBetween; 22.03.2018
comment
@InBetween Но это не такая ситуация. ОП ясно дал это понять, сказав, что решения со статической типизацией работают очень хорошо, они просто думают, что слишком много печатать. Если статически типизированное решение невозможно (поскольку соответствующая информация неизвестна во время компиляции), то у вас нет выбора, и вопрос бессмыслен; работает только один вариант. Другие варианты специально указаны как варианты, поэтому мы знаем, что это не очень хорошая идея. - person Servy; 22.03.2018
comment
@servy хорошо, но тогда зачем вообще нужен интерфейс? Если все типы известны статически, почему мы вообще имеем дело с интерфейсом IConsumer? Что-то не сходится. - person InBetween; 22.03.2018
comment
@InBetween Существует интерфейс, потому что остальная часть системы использует IConsumer и Product. Ему не нужны подробности о том, с каким Потребителем или Продуктом он имеет дело. Умеет работать с абстрактными понятиями. - person Kzrystof; 22.03.2018

Я нашел решение, которое решает мою проблему: шаблон посетителя. Хитрость заключалась в том, чтобы найти другую абстракцию (называемую здесь ICommonInterface) между моими IConsumer и моими Product и позволить посетителям разбираться с деталями.

public interface IProductVisitor
{
      ICommonInterface Visit(SpecificProductA productA);

      ICommonInterface Visit(SpecificProductB productB);
}

/* The purpose of this abstract class is to minimize the impact of the changes if I had to support another SpecificProductC. */ 
public abstract class ProductVisitor : IProductVisitor
{
      public virtual ICommonInterface GetCommonInterface(SpecificProductA productA)
      {
          throw new NotImplementedException();
      }

      public virtual ICommonInterface GetCommonInterface(SpecificProductB productB)
      {
          throw new NotImplementedException();
      }
}

public sealed class SpecificProductAVisitor : ProductVisitor
{
      public override ICommonInterface GetCommonInterface(SpecificProductA productA)
      {
          /* This guy will deal with ParamA of SpecificProductA */
          return new ImplACommonInterface(productA);
      }
}

public sealed class SpecificProductBVisitor : ProductVisitor
{
      public override ICommonInterface GetCommonInterface(SpecificProductB productB)
      {
          /* This guy will deal with ParamB of SpecificProductB */
          return new ImplBCommonInterface(productB);
      }
}

Затем я должен разрешить новые IProductVisitor в классах Product:

public abstract class Product
{
    public int BaseParam { get; set; }

    public abstract ICommonInterface Visit(IProductVisitor productVisitor);
}

public class SpecificProductA : Product
{
    public int ParamA {get;set;}

    public override ICommonInterface Visit(IProductVisitor productVisitor)
    {
        /* Forwards the SpecificProductA to the Visitor */
        return productVisitor.GetCommonInterface(this);
    }
}

public class SpecificProductB : Product
{
    public int ParamB {get;set;}

    public override ICommonInterface Visit(IProductVisitor productVisitor)
    {
        /* Forwards the SpecificProductB to the Visitor */
        return productVisitor.GetCommonInterface(this);
    }
}

Каждая реализация IConsumer теперь может делать следующее без необходимости приводить что-либо:

    public interface IConsumer
    {
        void Consume(Product product);

        ICommonObject Visit(IProductVisitor productVisitor);
    }

    public class ConcreteConsumerA : IConsumer
    {
        public void Consume(Product product)
        {
            /* The logic that needs for ParamA of SpecificProductA is now  
 pushed into the Visitor. */
            var productAVisitor = new SpecificProductAVisitor();
            ICommonInterface commonInterfaceWithParamA = product.GetCommonInterface(productAVisitor); 
        }
    }

    public class ConcreteConsumerB : IConsumer
    {
        public void Consume(Product product)
        {
        /* The logic that needs for ParamB of SpecificProductB is now  
 pushed into the Visitor. */
            var productBVisitor = new SpecificProductBVisitor();
            ICommonInterface commonInterfaceWithParamB = product.GetCommonInterface(productBVisitor); 
        }
    }
person Kzrystof    schedule 29.03.2018