Перегрузка и наследование методов — как обратиться к правильному методу?

#c# #overloading #factory

#c# #перегрузка #фабрика

Вопрос:

У меня есть следующая фабрика:

 public class ChildFactory : IChildFactory {
    public Child? GetChild(ChildType childType)
    {
        switch (childType)
        {
            case ChildType.Boy:
                return new BoyChild() { };
            case ChildType.Girl:
                return new GirlChild() { };
            case ChildType.Undecided:
                return new UndecidedChild() { };
            default:
                return null;
         }
    }
}
 

Всякий раз, когда я прохожу через a ChildType , чтобы сгенерировать a Child , я получаю обратно правильный экземпляр дочернего элемента:

Child? child = _childFactory.GetChild((ChildType)Enum.Parse(typeof(ChildType), childDto.Type));

Похоже, на данный момент child имеет правильный тип (скажем BoyChild так). Теперь я хочу динамически проверять каждый из дочерних типов отдельно, используя перегрузку метода. Вот интерфейс:

 public interface IChildValidator
    {
        public ValidationResult Validate(BoyChild child);
        public ValidationResult Validate(GirlChild child);
        public ValidationResult Validate(UndecidedChild policy);
    }
 

Но всякий раз, когда я пытаюсь использовать его в описанном выше методе, я получаю сообщение об ошибке CS1503: Cannot convert from 'Child' to 'BoyChild' .

Я думаю, я должен как-то объявить child по-другому, сказав: «Это может быть любой из дочерних типов», но я не знаю, как это сделать.

Заранее спасибо.

Комментарии:

1. Я не знаю всего вашего кода, но я предполагаю, что вам может быть лучше, если вы поместите логику проверки внутри дочерних классов: public abstract class Child { public abstract ValidationResult Validate(); } это поможет вам, например, по принципу open-close.

2. Спасибо. На самом деле я реализовал его так, как вы предложили, но мне было неудобно иметь бизнес-логику внутри модели.

3. @Кто-нибудь, не могли бы вы уточнить, как такая реализация (имеющая логику проверки как часть модели) способствует открытости-закрытости кода?

4. @noamyg Вы могли бы добавить новый тип Child без необходимости обновлять свой IChildValidator .

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

Ответ №1:

Разрешение перегрузки происходит во время компиляции. Компилятор C # использует статический тип аргумента, чтобы найти наилучшую перегрузку. Поскольку вы вызываете метод с аргументом типа Child , компилятор не находит соответствующую перегрузку. Ни одна из трех перегрузок не имеет параметра типа Child . Нет неявного преобразования из Child в BoyChild , GirlChild или UndecidedChild . Вы можете назначить производный класс базовому классу, но не наоборот.

Путь состоит в том, чтобы использовать полиморфию, то есть добавить абстрактный Validate метод в (абстрактный) Child класс (если вы не можете предоставить стандартную реализацию в базовом классе). Затем производные классы должны переопределить этот метод и предоставить реализацию. Во время выполнения динамическая отправка вызовет правильную реализацию.

Ответ №2:

В зависимости от уровня разделения, который вы ищете, вы можете попробовать что-то подобное, чтобы сохранить логику проверки вне вашей дочерней модели (это всего лишь пример, есть несколько способов настроить это, например, способ настройки средства проверки) :

 public class ChildFactory : IChildFactory
{
    public Child? GetChild(ChildType childType)
    {
        switch (childType)
        {
            case ChildType.Boy:
                return new BoyChild(new BoyValidator()) { };
            case ChildType.Girl:
                return new GirlChild(new GirlValidator()) { };
            case ChildType.Undecided:
                return new UndecidedChild(new UndecidedValidator()) { };
            default:
                return null;
        }
    }
}


public interface IChildValidator
{
    ValidationResult Validate(Child child);
}

public interface IChildValidatable
{
    ValidationResult Validate();
}


public class BoyValidator: IChildValidator
{
    ...
}


public class Child: IChildValidatable
{
    public Child(IChildValidator validator)
    {
        Validator = validator;
    }

    protected IChildValidator Validator { get; }


    public ValidationResult Validate() => Validator(this);
}
 

Вы можете улучшить предыдущий пример, если вам нравятся «готовые к переопределению» классы / функции с определенными классами вместо базового класса. Иногда это полезно для переопределения проверки, так как вы все равно можете использовать дочерний элемент. Опять же, это всего лишь легкий пример, Validate(Child child) функция может выполнять некоторые общие проверки и дополнительную проверку класса перед передачей в TypedValidate.

 public abstract class ChildValidator<T>: IChildValidator where T: Child
{
    public ValidationResult Validate(Child child) => TypedValidate(child as T)

    protected abstract ValidationResult TypedValidate(T child);
}
 

и

 public class BoyValidator: ChildValidator<BoyValidator>
{
    protected override ValidationResult TypedValidate(BoyValidator child)
    {
        ...
    }
}
 

Ответ №3:

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

 public abstract class ChildValidator
{
    public ValidationResult Validate(Child child)
    {
       switch(child)
       {
         case BoyChild boyChild:
            return Validate(boyChild);
         case GirlChild girlChild:
            return Validate(girlChild);
         case UndecidedChild undecided:
            return Validate(undecided);
         default:
            throw new Exception(child.GetType()   " is not supported");
       }
    }

    protected abstract ValidationResult Validate(BoyChild child);
    protected abstract ValidationResult Validate(GirlChild child);
    protected abstract ValidationResult Validate(UndecidedChild policy);
}
 

Но я не рекомендую это решение. Если вы проверяете тип переменной, это часто (не всегда) признак того, что вы делаете что-то неправильно. Вместо этого я рекомендую поместить логику проверки внутри вашего дочернего класса:

 public abstract class Child
{
    public abstract ValidationResult Validate();
}
 

Это имеет несколько преимуществ: если вы добавляете новый дочерний подкласс, вам не нужно забывать также добавлять этот класс в свой ChildValidator и все его подклассы. Это упрощает расширение поведения вашего кода, который также известен как принцип «открыто-закрыто». Более того, я предполагаю, что ваша проверка проверяет некоторые свойства вашего дочернего элемента. Если вы добавите свойство, вам, вероятно, также придется изменить метод проверки. При втором подходе эти изменения будут в одном месте.

Комментарии:

1. Если noamyg не нужно отделять логику проверки от модели, я полностью согласен с вами.