Что делать, когда одна из реализаций требует немного больше данных - PullRequest
0 голосов
/ 04 сентября 2018

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

class Configuration
{
    public int Min { get; set; } 
    public int Max { get; set; }     
}

interface IService
{
    int Calculate(int userId, Configuration configuration)
}

У меня есть, скажем, 5 классов, которые реализуют этот интерфейс, и они работают хорошо. Однажды я должен реализовать 6-й сервис, но этот немного отличается. Чтобы выполнить свою работу, новая служба нуждается в конфигурации, подобной этой:

class ExtendedConfiguration : Configuration
{
    public string Filter { get; set; }     
}

Мой новый сервис может выглядеть так:

class NewService : IService
{
    public int Calculate(int userId, Configuration configuration)
    {
        var extendedConfig = configuration as ExtendedConfiguration;
        //Calculating and returning result using extendedConfig...
    }
}

Кажется, все в порядке, служба будет работать. НО, мне не нравится тот факт, что для сигнатуры метода Calculate требуется объект Configuration, в то время как в действительности требуется ExtendedConfiguration - в противном случае он не сможет выполнить вычисление (и сгенерирует исключение).

Есть ли лучший способ написания этого кода?

Ответы [ 4 ]

0 голосов
/ 04 сентября 2018

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

Вместо предоставления конфигурации службе, предоставьте поставщика конфигурации: (используя C # 7 ValueTuples )

public enum ValueType
{
    ReturnedConfigured,
    NotConfiguredReturnedDefault,
    InvalidConfigurationReturnedDefault
}

public interface IConfigurationProvider
{
    (T result, ValueType resultType) GetSetting<T>(string serviceName, string settingKey, T defaultValue);
}

public interface IService
{
    int Calculate(int userId, IConfigurationProvider configurationProvider);
}

Вы можете использовать это следующим образом:

public class NewService : IService
{
    public int Calculate(int userId, IConfigurationProvider configurationProvider)
    {
        (int min, _) = configurationProvider.GetSetting(nameof(NewService), "Min", -1);
        (int max, _) = configurationProvider.GetSetting(nameof(NewService), "Max", Int32.MaxValue);
        (string filter, ValueType filterConfigResponse) = configurationProvider.GetSetting(nameof(NewService), "Filter", string.Empty);
        if (filterConfigResponse!=ValueType.ReturnedConfigured)
        {
            throw new ArgumentException("Oh no! Where's my filter?", nameof(configurationProvider));
        }
        Console.WriteLine($"{nameof(NewService)},min={min}, max={max}, filter={filter}");
        return 0;
    }
}

Вот пример IConfigurationProvider, который вы можете внедрить в модульный тест

public class FakeConfigurationProvider : IConfigurationProvider
{
    public (T result, ValueType resultType) GetSetting<T>(string serviceName, string settingKey, T defaultValue)
    {
        switch (settingKey)
        {
            case "Min":
                {
                    return (result: (T)Convert.ChangeType(1, typeof(T)), resultType: ValueType.ReturnedConfigured);
                }
            case "Max":
                {
                    return (result: (T)Convert.ChangeType(42, typeof(T)), resultType: ValueType.ReturnedConfigured);
                }
            case "Filter":
                {
                    return (result: (T)Convert.ChangeType("Hello World", typeof(T)), resultType: ValueType.ReturnedConfigured);
                }
            default:
                {
                    return (result: defaultValue, resultType: ValueType.NotConfiguredReturnedDefault);
                }
        }
    }
}

Отсюда довольно просто представить себе других провайдеров конфигурации для извлечения настроек из app.Config или таблицы базы данных или Uri или любого другого способа хранения.

0 голосов
/ 04 сентября 2018

Я не считаю это решение ужасным; Алгоритмы, которые пытаются обрабатывать объекты в иерархии наследования единообразно, имеют недостаток, заключающийся в том, что в какой-то момент вам нужно будет установить или проверить определенные признаки объектов во время выполнения . Причина именно в том, что вы испытываете: редко, когда одно решение подходит всем, если вы попадаете на крайние случаи.

В качестве альтернативы проверке типа можно сделать Configuration, ну, настраиваемым. Типичные подходы

  • Наследование: Configuration определяет дополнительные виртуальные методы (например, предварительная обработка, дополнительная обработка или пост-обработка ;-)), которые пусты, но могут быть заполнены жизнью в производных классах. Вызывающий код не должен знать точный тип, он просто тупо вызывает эти функции в правильном порядке, и все, что этот конкретный класс определяет для них, выполняется.

  • Инъекция: поведение объектов Configure настраивается во время выполнения путем установки членов, которые «знают, как делать». Расчет выполняется с использованием информации или действий, определенных в этих «агентах».

0 голосов
/ 04 сентября 2018

На самом деле проблем нет. Как упоминалось ранее, кастинг не удался.

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

class NewService : IService
{
    public int Calculate(int userId, Configuration configuration)
    {
        var extendedConfig = new ExtendedConfiguration {
            Max = configuration.Max,
            Min=configuration.Min
        };


        return e.Max - e.Min;
    }
}
0 голосов
/ 04 сентября 2018

, что для сигнатуры метода Calculate требуется объект конфигурации, тогда как в действительности требуется ExtendedConfiguration

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

interface IService<T> where T : Configuration
{
    int Calculate(int userId, T configuration)
}

Тогда ваши старые сервисы все еще могут выглядеть так:

class OldService : IService<Configuration>
{
    public int Calculate(int userId, Configuration configuration)
    {       
        return (configuration.Min + configuration.Max) * 2;
    }
}

и в NewService вы можете указать, что входной параметр должен иметь тип ExtendedConfiguration:

class NewService : IService<ExtendedConfiguration>
{
    public int Calculate(int userId, ExtendedConfiguration configuration)
    {
        string accessHereTheExtendedVersion = configuration.Filter;
        return (configuration.Min + configuration.Max) / 2;
    }
}

Это не совсем ответ на ваш точный вопрос:

Есть ли лучший способ написания этого кода?

Но это другой подход к решению этой проблемы. Соответствует ли это вашему контексту и ситуации, вы должны проверить это.

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