Почему нельзя заменить производные классы вместо базовых классов в элементах интерфейса? - PullRequest
0 голосов
/ 16 мая 2018

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

public interface BaseType {}

public class FirstDerivedClass : BaseType {}

public class SecondDerivedClass : BaseType {}


public interface SomeInterface
{
    void Method(BaseType type);
}

public class Implementation : SomeInterface
{
    public void Method(FirstDerivedClass type) {}
}

'Implementation' does not implement interface member 'SomeInterface.Method(BaseType)'

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

Например, Selenium WebDriver имеет несколько драйверов браузера.Существует абстрактный класс DriverOptions с производными классами (ChromeOptions, FirefoxOptions и т.

Ответы [ 4 ]

0 голосов
/ 17 мая 2018

Объяснение

Почему это невозможно?Я попал в реальную ситуацию, в которой это было бы полезно.

Полезно или нет, это нелогично.Вы частично нарушаете договор, предусмотренный интерфейсом.

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

public class Food {}
public class Meat :  Food {}
public class Poultry : Food {}
public class Tofu :  Food {}

Ничего изпока что обычный.

Допустим, у меня есть ресторан под названием Chez Flater.Я хочу нанять шеф-поваров, поэтому составляю контракт на то, что, как я ожидаю, будут делать мои шеф-повара, когда они работают:

public interface IChezFlaterChef
{
    void Cook(Food food);
}

Обратите внимание, что определяет этот интерфейс.Чтобы заключить контракт в слова:

Если вы хотите быть IChezFlaterChef, вы должны уметь готовить все, что является едой .

public class VeganChef : IChezFlaterChef
{
    public void Cook(Tofu tofu) {}
}

Теперь вы должны увидеть, почему этот класс нарушает контракт.Он хочет быть IChezFlaterChef, но на самом деле он не выполняет всю работу, которая мне нужна от моих поваров.VeganChef хочет только готовить Tofu.Я бы не стал его нанимать.

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

Предполагается, что следующий код всегда будет работать:

IEnumerable<Food> myFood = GetSomeFoods();

foreach(var food in myFoods)
{
    var availableChef = FindAvailableChef();

    chef.Cook(food);
}

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

Этопроблема.Мой ресторан не будет работать эффективно, потому что теперь мне приходится иметь дело с шеф-поваром, который отказывается работать (= выбрасывает исключение), и мне придется искать второго шеф-повара.
Мне специально требовались всемои повара могли готовить любую еду , потому что я не хотел, чтобы это произошло .Либо я не должен ожидать этого от моих поваров;или я не должен был нанять веганского шеф-повара.Компилятор указывает, что я не должен нанимать шеф-повара:

«Реализация» не реализует элемент интерфейса «SomeInterface.Method (BaseType)»

Эквивалентнона

[VeganChef] не выполняет ожидаемое [приготовление какой-либо пищи].

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

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

Если я напишу этот код:

public string GetEmployeeName(Employee e)
{
    return e.Name;
}

Это основано на логике, что каждый Сотрудник гарантированно имеет свойство Name.

Но ваш VeganChef не может готовить каждую пищу.Поскольку мне требуется, чтобы мои IChezFlaterChef s могли готовить любую еду, это логически означает, что VeganChef нельзя нанять как IChezFlaterChef.


Решение

Ваш текущийКонтракт с IChezFlaterChef просто неверен:

Если вы хотите быть IChezFlaterChef, вы должны иметь возможность готовить все, что является пищей .

То, что вы хотите, более корректно выражается в виде:

Если вы хотите быть IChezFlaterChef для определенного типа пищи , вы должны иметь возможность готовить этого конкретного типа пищи.

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

Когда шеф-повар хочет быть IChezFlaterChef, очевиднымПервым вопросом тогда становится «для какой еды?».Эта информация предоставляется универсальным типом :

public interface IChezFlaterChef<TFood> where T : Food
{
    void Cook(TFood food);
}

Уведомление where T : Food.Это гарантирует, что вы не можете делать такие вещи, как создание IChezFlaterChef<DateTime>, что не имеет смысла.

public class VeganChef : IChezFlaterChef<Tofu>
{
    public void Cook(Tofu tofu) {}
}

И теперь этот класс действителен, потому что он выполняет контракт, определенный IChezFlaterChef<TFood>.

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

public class MeatAndPoultryChef : IChezFlaterChef<Meat>, IChezFlaterChef<Poultry>
{
    //Needed for IChezFlaterChef<Meat>
    public void Cook(Meat meat) {}

    //Needed for IChezFlaterChef<Poultry>
    public void Cook(Poultry poultry) {}
}
0 голосов
/ 16 мая 2018

Конечная цель - получить правильную фабрику для любой данной конфигурации.

Без генериков

interface IFactory
{
    bool CanYouMakeIt(IConfig config);
    IMadeThis MakeIt(IConfig config);
}

Вы разрешаете всем фабрикам реализовывать интерфейс, а затем можете хранить их в List<IFactory>. Просто запросите их все с данным экземпляром конфигурации и спросите их, могут ли они это сделать. Вы также можете опустить CanYouMakeIt () и позволить MakeIt просто возвращать ноль, если это невозможно для этой фабрики.

class SampleFactory : IFactory
{
    public bool CanYouMakeIt(IConfig config) => config.GetType() == typeof(SampleConfig);
    public IMadeThis MakeIt(IConfig config) => new SampleMadeThis(config);
}

Использование обобщений

Примечание: IFactory можно опустить, но это делает его удобным для хранения в более узком списке.

interface IFactory {}
interface IFactory<T> :IFactory where T : IConfig
{
    IMadeThis MakeIt(T config);
}

class SampleFactory : IFactory<SampleConfig>
{
    public IMadeThis MakeIt(SampleConfig config) => new SampleMadeThis(config);
}

Теперь вызов - это сложная часть. Инфраструктура DI, такая как Autofac, может упростить этот путь, но давайте посмотрим, как нам это сделать.

Здесь я не проверяю ошибки ...

class TheUltimateFactory
{
    private readonly IDictionary<Type,IFactory> _factories;

    public TheUltimateFactory(IFactory[] factories)
    {
        _factories = factories.ToDictionary(f => GetFactoryType(f.GetType()), f => f);
    }

    private Type GetFactoryType(Type type)
    {
        var genericInterface = type.GetInterfaces().First(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IFactory<>));
        return genericInterface.GetGenericArguments()[0];
    }

    public IMadeThis MakeIt(IConfig config)
    {
        var configType = config.GetType();
        var method = typeof(IFactory<>).MakeGenericType(configType).GetMethod("MakeIt");
        return (IMadeThis)method.Invoke(_factories[configType], new object[]{config});
    }
}

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

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

0 голосов
/ 17 мая 2018

Чтение вопроса

Почему нельзя заменить производные классы вместо базовых классов в элементах интерфейса?

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

Согласно интерфейсу вашего контракта interface SomeInterface Каждый класс, который реализует этот интерфейс, должен иметь метод Method(BaseType type), который может принимать каждый объект тех классов, который реализует BaseType.

означает, что объект всех тех классов, которые реализуют BaseType, может быть передан в Method().

но в вашем производном классе class Implementation : SomeInterface, когда он попытался переопределить этот метод, он может принять только объект типа FirstDerivedClass.

значение,

если кто-то создает другой класс, скажем Demo, который реализует BaseType, то теперь, согласно методу интерфейса, объект этого класса тоже может быть передан в Method(BaseType type), но как у вашего производного класса Implementation действительно есть метод, который принимает только * Только 1028 *, он не примет объект Demo.

один такой класс (такой же как Demo) уже существует только в вашем коде, SecondDerivedClass

Так что это нарушает договор.

В вашем производном классе Implementation также должен быть метод, который может принимать каждый объект тех классов, который реализует BaseType.

Способ сделать это, просто принять BaseType Типизированный экземпляр в методе Implementation.

public void Method(Base type) {} вместо public void Method(FirstDerivedClass type) {}

Здесь в первой строке этого метода вы можете привести тип от Base до FirstDerivedClass на FirstDerivedClass obj = (FirstDerivedClass)type.

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

0 голосов
/ 16 мая 2018

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

    // Third party code - Selenium.
public abstract class DriverOptions { }
public class ChromeOptions : DriverOptions { }
public class FirefoxOptions : DriverOptions { }


// My code
public interface IWebDriverFactory<in T> where T:DriverOptions
{
    IWebDriver GetWebDriver(T options);
}

public class ChromeWebDriverFactory : IWebDriverFactory<ChromeOptions>
{
    public IWebDriver GetWebDriver(ChromeOptions options)
    {
        // implementation details for initalising Chrome and use options from ChromeOptions
    }
}

public class FirefoxWebDriverFactory : IWebDriverFactory<FirefoxOptions>
{
    public IWebDriver GetWebDriver(FirefoxOptions options)
    {
        // implementation details for initalising Firefox and use options from FirefoxOptions
    }
}

как предложил @TSmith -> использование: var a = new FirefoxWebDriverFactory();

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...