Унификация проверки типов

284
28 августа 2017, 05:48

Есть следующий код:

public abstract class PointBase
{
}
public class ProductPoint : PointBase
{
}
public class EmptyPoint : PointBase
{
}
...
public void SomeMethod(IEnumerable<PointBase> points)
{
    points.ForEach(DoSome);
}
public void DoSome(PointBase point)
{
    if (point is ProductPoint)
    {
        DoSomeForProductPoint((ProductPoint)point);
        return;
    }
    if (point is EmptyPoint)
    {
        DoSomeForEmptyPoint((EmptyPoint)point);
        return;
    }
    throw new Exception("An unknown implementation.");
}

Мест с таким type checking'ом несколько.

Что хочется?

  1. Чтобы при добавлении нового наследника от PointBase среда разработки сама мне говорила какие места с type checking'ом мне нужно дополнить чтобы они работали с новым наследником.

  2. Чтобы нельзя было скомпилировать код (ну или в крайнем случае прогнать удачно тесты) без дополнения всех мест с type checking'ом для нового наследника.

Решения в лоб:

  1. Какой-то волшебный метод в базовом классе с примерно следующим содержанием:

    public static void DoSomeForDerived(
        PointBase p,
        Action<ProductPoint> a1,
        Action<EmptyPoint> a2)
    {
        if (point is ProductPoint)
        {
            a1((ProductPoint)point);
            return;
        }
        if (point is EmptyPoint)
        {
            a2((EmptyPoint)point);
            return;
        }
        throw new Exception("An unknown implementation.");
    }

И собственно вызывать этот метод каждый раз когда мне нужен какой-то type checking прокидывая в него экшены которые нужно выполнять для каждого типа наследников. С таким решением при добавлении нового наследника, например какого-нибудь "CodePoint : PointBase", и добавлением нового экшена в сигнатуру метода компилятор при следующем билде скажет мне что все прежние использования этого метода содержат неполный набор аргуметов. И я, разработчик, буду вынужден тем самым заимплементить логику работы с новым наследником во всех местах.

  1. Рефлексия при тестировании. Все методы которые в аргументах содержат какой-либо упоминание о PointBase тестировать отталкиваясь от текущего количества наследников в сборке вытаскивая их с помошью рефлексии. Тут мне конечно необходимо чтобы у всех наследников был конструктор без параметров чтобы я мог создать их экземпляры и понапихать туда для теста. Без наличия такого конструктора задача сильно усложняется и перестает иметь всякий смысл из за затрат по времени.

Ни один из вариантов мне не нравится. Буду рад увидеть любые предложения.

Answer 1

Помимо того, у вас в коде присутствует проверка типа (а это само по-себе проблема и вы её описываете) потенциально метод DoSome нарушает SRP. Я бы отделил операции DoSomeForEmptyPoint и DoSomeForProductPoint в отдельное место и сделал бы вызовы непосредственно из классов ProductPoint и EmptyPoint воспользовавшись паттерном визитер, например:

public interface IPointVisitor
{
    void Visit(ProductPoint productPoint);
    void Visit(EmptyPoint emptyPoint);
}
class PointVisitor : IPointVisitor
{
    public void Visit(ProductPoint productPoint)
    {
        // DoSomeForProductPoint 
    }
    public void Visit(EmptyPoint emptyPoint)
    {
        // DoSomeForEmptyPoint 
    }
}
public abstract class PointBase 
{ 
    public abstract void Accept(IPointVisitor visitor);
}
public class ProductPoint : PointBase
{
    public override void Accept(IPointVisitor visitor)
    {
        visitor.Visit(this);
    }
}
public class EmptyPoint : PointBase
{
    public override void Accept(IPointVisitor visitor)
    {
        visitor.Visit(this);
    }
}
public class Program
{
    public static void Main()
    {
        IPointVisitor visitor = new PointVisitor();
        ProductPoint point = new ProductPoint();
        point.Accept(visitor);
    }
}

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

Answer 2

По моему, вы пытаетесь заново изобрести полиморфизм.

public abstract class PointBase
{
    public virtual void DoSome()
    {
        throw new NotImplementedException();
        //Если метод не реализован в наследнике будет исключение
    }
}
public class ProductPoint : PointBase
{
     public override void DoSome()
     {
         //что то делаем...
     }
}
public class EmptyPoint : PointBase
{
     public override void DoSome()
     {
         //что то делаем...
     }
}
public void SomeMethod(IEnumerable<PointBase> points)
{
     foreach (point in points)
     {
          point.DoSome();
     }
}

З.Ы. Если родительский класс определить как:

public abstract class PointBase
{
    public abstract void DoSome();
}

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

READ ALSO
Выделение текста в textbox методом Select

Выделение текста в textbox методом Select

Имеется форма с панелью и richtextbox'ом

241
&ldquo;Cannot convert null to &rdquo;int&ldquo; because it is a non-nullable value type&rdquo;

“Cannot convert null to ”int“ because it is a non-nullable value type”

Вызов метода должен отправить такой ответ:

312
Создание анимации загрузки WPF

Создание анимации загрузки WPF

Задача: сделать крутящийся кружок, пока приложение будет делать свои делаДля этой задачи пробовал использовать gif, но есть разные проблемы,...

645