Способы расширения скрывают зависимости?
Все,
Хотелось подумать об этом. В последнее время я все больше и больше становлюсь подписчиком "пуристических" принципов DI/IOC при проектировании / разработке. Часть этого (большая часть) состоит в том, чтобы удостовериться, что между моими классами есть небольшая связь, и что их зависимости разрешаются через конструктор (конечно, есть другие способы управления этим, но вы поймете идею).
Моя основная предпосылка заключается в том, что методы расширения нарушают принципы DI/IOC.
Я создал следующий метод расширения, который я использую, чтобы убедиться, что строки, вставленные в таблицы базы данных, обрезаны до нужного размера:
public static class StringExtensions
{
public static string TruncateToSize(this string input, int maxLength)
{
int lengthToUse = maxLength;
if (input.Length < maxLength)
{
lengthToUse = input.Length;
}
return input.Substring(0, lengthToUse);
}
}
Затем я могу вызвать мою строку из другого класса следующим образом:
string myString = "myValue.TruncateThisPartPlease.";
myString.TruncateToSize(8);
Правильный перевод этого без использования метода расширения будет:
string myString = "myValue.TruncateThisPartPlease.";
StaticStringUtil.TruncateToSize(myString, 8);
Любой класс, который использует любой из вышеприведенных примеров, не может быть протестирован независимо от класса, который содержит метод TruncateToSize (кроме TypeMock). Если бы я не использовал метод расширения и не хотел создавать статическую зависимость, это выглядело бы как:
string myString = "myValue.TruncateThisPartPlease.";
_stringUtil.TruncateToSize(myString, 8);
В последнем примере зависимость _stringUtil будет разрешена с помощью конструктора, и класс можно будет протестировать без зависимости от фактического класса метода TruncateToSize (его можно легко смоделировать).
С моей точки зрения, первые два примера основаны на статических зависимостях (одна явная, одна скрытая), а вторая инвертирует зависимость и обеспечивает меньшую связь и лучшую тестируемость.
Так противоречит ли использование методов расширения принципам DI/IOC? Если вы являетесь подписчиком методологии МОК, избегаете ли вы использовать методы расширения?
4 ответа
Я вижу, откуда вы, однако, если вы пытаетесь смоделировать функциональность метода расширения, я думаю, что вы используете их неправильно. Методы расширения должны использоваться для выполнения задачи, которая была бы просто синтаксически неудобной без них. Ваш TruncateToLength является хорошим примером.
Тестирование TruncateToLength не потребовало бы его макетирования, оно просто включало бы создание нескольких строк и проверку того, что метод действительно возвращает правильное значение.
С другой стороны, если у вас есть код на уровне данных, содержащийся в методах расширения, которые обращаются к вашему хранилищу данных, тогда да, у вас есть проблема, и тестирование станет проблемой.
Обычно я использую только методы расширения, чтобы обеспечить синтаксический сахар для небольших, простых операций.
Я думаю, что это хорошо - потому что это не так TruncateToSize
является реально заменяемым компонентом. Это метод, который когда-либо должен будет делать только одну вещь.
Вам не нужно иметь возможность макетировать все - только сервисы, которые либо нарушают юнит-тестирование (доступ к файлам и т. Д.), Либо те, которые вы хотите протестировать с точки зрения подлинных зависимостей. Если бы вы использовали его для выполнения аутентификации или чего-то в этом роде, это было бы совсем другое дело... но было бы просто выполнить прямую строковую операцию, которая не имеет абсолютно никакой конфигурируемости, других вариантов реализации и т. Д. - нет смысла рассматривать это как зависимость в обычном смысле.
Другими словами, если TruncateToSize
были подлинным членом String
Вы бы даже дважды подумали об его использовании? Вы пытаетесь смоделировать целочисленную арифметику, вводя IInt32Adder
так далее? Конечно, нет. Это то же самое, только то, что вы предоставляете реализацию. Модульное тестирование TruncateToSize
и не беспокойся об этом.
Методы расширения, частичные классы и динамические объекты. Они мне очень нравятся, но вы должны действовать осторожно, здесь есть монстры.
Я хотел бы взглянуть на динамические языки и посмотреть, как они справляются с такими проблемами на повседневной основе, это действительно поучительно. Особенно, когда им нечего помешать им делать глупости, кроме хорошего дизайна и дисциплины. Все динамично во время выполнения, единственное, что их останавливает, это компьютер, выдающий большую ошибку времени выполнения. "Duck Typing" - самая безумная вещь, которую я когда-либо видел, хороший код сводится к хорошему дизайну программы, уважению к другим в вашей команде и доверию, которое каждый участник, хотя и имеет способность делать некоторые дурацкие вещи, выбирает не потому, что хороший дизайн приводит к лучшим результатам.
Что касается вашего тестового сценария с фиктивными объектами /ICO/DI, вы бы действительно вложили какую-то тяжелую работу в метод расширения или просто некоторые статические вещи, которые работают в функциональном стиле? Я склонен использовать их, как и вы, в функциональном стиле программирования, ввод идет, результаты получаются без магии в середине, просто классы фреймворков, которые, как вы знаете, разработчики и разработчики MS разработали и протестировали:P, на которую вы можете положиться на.
Если вы делаете какие-то тяжелые вещи с использованием методов расширения, я бы снова посмотрел на дизайн вашей программы, проверил ваши проекты CRC, модели классов, варианты использования, DFD, диаграммы действий или все, что вы хотели бы использовать, и выяснил, где в этом проекте вы планировал поместить этот материал в метод расширения вместо надлежащего класса.
В конце концов, вы можете тестировать только на соответствие вашей конструкции системы, а не код за пределами вашей области. Если вы собираетесь использовать классы расширения, я бы посоветовал вместо этого взглянуть на модели Object Composition и использовать наследование только тогда, когда есть очень веская причина.
Композиция объектов всегда выигрывает у меня, так как они создают твердый код. Вы можете подключить их, вынуть их и делать с ними то, что вам нравится. Помните, что все зависит от того, используете ли вы интерфейсы или нет как часть вашего дизайна. Также, если вы используете классы Composition, дерево иерархии классов сглаживается в дискретные классы, и становится меньше мест, где ваш метод расширения будет выбран через унаследованные классы.
Если вы должны использовать класс, который действует на другой класс, как в случае с методами расширения, сначала посмотрите на шаблон посетителя и решите, является ли он лучшим маршрутом.
Это боль, потому что их трудно высмеивать. Я обычно использую одну из этих стратегий
- Да, откажитесь от расширения его PITA, чтобы макетировать
- Используйте расширение и просто проверьте, правильно ли он поступил. т.е. передать данные в усечение и проверить, что они были усечены
- Если это не какая-то тривиальная вещь, и я ДОЛЖЕН ее высмеивать, я сделаю так, чтобы у моего класса расширения был установщик для службы, которую он использует, и я установлю это в тестовом коде.
т.е.
static class TruncateExtensions{
public ITruncateService Service {private get;set;}
public string TruncateToSize(string s, int size)
{
return (Service ?? Service = new MyDefaultTranslationServiceImpl()). TruncateToSize(s, size);
}
}
Это немного страшно, потому что кто-то может установить службу, когда не должен, но я иногда немного кавалер, и если бы это было действительно важно, я мог бы сделать что-то умное с флагами #if TEST или шаблоном ServiceLocator, чтобы избежать сеттер, используемый в производстве.