Нет способа подделать статические интерфейсы классов, и много раз, когда я думал, что мне нужен один, мне действительно нужны обычные интерфейсы экземпляров. Вы не можете передать «экземпляр» статического класса в C #, нет способа дать функции «статический» интерфейс или даже статический «класс» для использования статических методов из него. Когда вы вызываете статический метод, он всегда является явным, и вы «жестко связываете» свой метод со статическим классом, который вы вызываете, и это не очень хорошая вещь.
Изменчивость, основанная на статических методах, трудно провести модульное тестирование. Классы, зависящие от такой изменчивости, менее гибкие. Представьте, что какая-то функция явно использует один из ваших алгоритмов из вашего статического класса. Такая функция явно связывает себя с этим конкретным алгоритмом.
public class SomeBusinessLogic
{
public Result HandleDocument(IDocument doc)
{
// some transformations...
int hash = Hashing.ReSharperHash.CalculateHash(seed, doc.Properties);
// some other code ...
}
}
Ну, что с этим не так?
Класс никогда явно не объявляет, что это зависит от хеширования. Вы должны знать его реализацию, чтобы рассуждать об этом. В этом случае это может быть не очень важно, но что, если один из алгоритмов хеширования работает очень медленно? Или если ему нужны внешние файлы на диске? Или если он подключается к какой-либо внешней службе хеширования? Может произойти непредвиденный сбой при вызове функции HandleDocument
.
Если вы хотите использовать какой-то другой алгоритм хеширования для конкретного документа, вы не сможете сделать это без изменения кода.
Когда вы проводите модульное тестирование, вы как бы тестируете как логику обработки документов, так и логику хеширования (которая, как предполагается, уже тестировалась его собственными модульными тестами). Если ваши тесты сравнивают вывод Result
с некоторым значением из ресурса, и он содержит хеш-значение, то все модульные тесты для этой функции будут прерваны, если вы измените его на другой хеш-алгоритм.
Какой способ лучше? Извлеките интерфейс, абстрагирующий вашу функцию хеширования, и явно запрашивайте его всякий раз, когда вам нужно будет выполнить хеширование. Таким образом, вы можете сохранять свои алгоритмы реализованными в виде неких состояний, пока они не сохраняют состояние, но клиентский код не будет связан с особенностями хеширования. И кто знает, вы можете однажды обнаружить, что вам нужен какой-то параметризованный алгоритм хеширования, и вы можете просто создавать новый экземпляр алгоритма каждый раз, когда он вам нужен.
Я использую ваш интерфейс с немного измененным стилем:
public interface IHashAlgorithm
{
int CalculateHash(int seed, params int[] keys);
int CalculateNoise(int seed, params int[] keys);
int Maximum { get; }
int Minimum { get; }
}
public static class StatelessHashAlgorithms
{
private static readonly IHashAlgorithm MurmurHash2Instance =
new HashAlgorithm(MurmurHash2Hash, 0, int.MaxValue);
private static readonly IHashAlgorithm ReSharperInstance =
new HashAlgorithm(ReSharperHash, int.MinValue, int.MaxValue);
public static IHashAlgorithm MurmurHash2
{
get { return MurmurHash2Instance; }
}
public static IHashAlgorithm ReSharper
{
get { return ReSharperInstance; }
}
private static int MurmurHash2Hash(int seed, params int[] keys)
{
//...
}
private static int ReSharperHash(int seed, params int[] keys)
{
//...
}
}
public class SomeCustomHashing : IHashAlgorithm
{
public SomeCustomHashing(parameters)
{
//parameters define how hashing behaves
}
// ... implement IHashAlgorithm here
}
Весь клиентский код должен явно запрашивать интерфейс хеширования, когда он ему нужен, он называется Dependency Injection и может быть выполнен на уровне класса или на уровне метода. Затем вызывающий или создатель класса будет отвечать за предоставление алгоритма хеширования.
public class SomeBusinessLogic
{
// injection in constructor
public SomeBusinessLogic(IHashingAlgorithm hashing)
{
// put hashing in a field of the class
}
// OR injection in method itself, if hashing is only used in this method
public Result HandleDocument(IDocument doc, IHashingAlgorithm hashing)
{
// some transformations...
int hash = hashing.CalculateHash(seed, doc.Properties);
// some other code ...
}
}
Это решает проблемы, описанные выше:
Класс явно объявляет, что это зависит от хеширования. Тот, кто предоставляет конкретный алгоритм хеширования, знает, чего ожидать от производительности, ресурсов, соединений, исключений и т. Д.
Вы можете настроить, какой алгоритм хеширования используется для каждого экземпляра бизнес-логики или для каждого документа.
Когда вы тестируете модуль, вы можете обеспечить фиктивную реализацию хеширования, например, всегда возвращая 0 (а также проверяя, что метод передает ожидаемые значения алгоритму хеширования). Это вы отдельно проверяли хеширование и отдельно проверяли обработку документов.
Итак, суть в том, что если у вас есть некоторые различия в поведении - используйте стандартные интерфейсы экземпляров. Код за ними может быть статическим или нестатичным, это не имеет значения. Важно то, что места, где используется переменное поведение, останутся гибкими, расширяемыми и тестируемыми на уровне модулей.
P.S. Есть также аспект "что является твоим доменом". Если вы пишете какое-то бизнес-приложение и называете static Math.Sqrt(...)
здесь и там - это нормально, поскольку альтернативных вариантов поведения нет. Но если вы пишете какую-то математическую библиотеку и у вас есть несколько различных реализаций квадратного корня с разными алгоритмами или точностью, вы, вероятно, захотите заключить их в интерфейс и передать как экземпляры интерфейса, чтобы иметь возможность расширяться.