Скрывают ли методы расширения зависимости?

Все,

Хотел высказать несколько мыслей по этому поводу. В последнее время я становлюсь все более и более приверженцем "пуристских" принципов 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? Если вы подписались на методологию IOC, избегаете ли вы использования методов расширения?


person Phil Sandler    schedule 18.08.2009    source источник
comment
@antisanity: AFAIK логически невозможно, чтобы параметр экземпляра метода расширения (в данном случае input) имел значение NULL.   -  person Phil Sandler    schedule 04.08.2011
comment
Просто ради пуристов слово «статический» в любом месте кода нарушает объектно-ориентированный дизайн и большинство принципов.   -  person Sebastien    schedule 27.04.2017


Ответы (4)


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

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

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

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

person LorenVS    schedule 18.08.2009

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

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

Другими словами: если бы TruncateToSize был настоящим членом String, вы бы даже дважды подумали, прежде чем использовать его? Вы пытаетесь высмеять целочисленную арифметику, вводя IInt32Adder и т. Д.? Конечно, нет. Это то же самое, только вы предоставляете реализацию. Модульное тестирование из TruncateToSize и не беспокойтесь об этом.

person Jon Skeet    schedule 18.08.2009
comment
Так что я не не согласен с вашим ответом - он очень прагматичный. Вы определенно можете зайти слишком далеко с IOC (или любой другой теорией дизайна). Ради аргумента: предполагая, что вы привержены IOC, у вас не возникнет проблем со вторым примером реализации (явная зависимость от статического класса)? Я никогда не читал статьи жесткого защитника IOC, в которых говорилось бы, что статические зависимости допустимы. - person Phil Sandler; 19.08.2009
comment
Что, если бы для TruncateToSize было два алгоритма? Один обменивает скорость на память, а другой обменивает память на скорость. Разве в таком случае не было бы лучше, если бы у него был интерфейс? А поскольку вы не можете предсказать потребность в них, не следует ли вам просто сделать это в первую очередь интерфейсом? - person Didier A.; 29.01.2015
comment
@didibus: Следуя этой логике до конца, каждый метод должен иметь свой собственный интерфейс. Нет, есть некоторые прогнозы, которые разумно сделать. В программном обеспечении вы должны быть прагматичными, иначе вы никогда ничего не добьетесь, и ваш код будет намного сложнее, чем нужно. - person Jon Skeet; 29.01.2015

Методы расширения, частичные классы и динамические объекты. Они мне очень нравятся, но будьте осторожны, здесь есть монстры.

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

Что касается вашего тестового сценария с фиктивными объектами / ICO / DI, действительно ли вы вложили бы в метод расширения какую-то тяжелую работу или просто какие-то простые статические вещи, которые работают в режиме функционального типа? Я обычно использую их, как и вы, в стиле функционального программирования, ввод идет, результаты выходят без всякой магии в середине, просто классы фреймворков, которые, как вы знаете, разработали и протестировали ребята из MS: P, на которые вы можете положиться на.

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

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

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

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

person WeNeedAnswers    schedule 04.08.2011

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

  1. Да, выбросьте расширение, это PITA, чтобы высмеять
  2. Используйте расширение и просто проверьте, правильно ли оно работает. т.е. передать данные в усечение и проверить, что они были усечены
  3. Если это не какая-то тривиальная вещь, и я ДОЛЖЕН насмехаться над ней, я сделаю свой класс расширения установщиком для службы, которую он использует, и установлю это в тестовом коде.

i.e.

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, чтобы избежать сеттер, используемый в производстве.

person mcintyre321    schedule 18.08.2009