Есть ли лучший способ подделать интерфейсы статического класса? - PullRequest
1 голос
/ 16 октября 2011

Имеет ли этот шаблон дизайна много смысла? Первоначально у меня был один статический класс, который возвращал HashFunction для каждого реализованного алгоритма.

public delegate int HashFunction(int seed, params int[] keys);

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

public interface IHashAlgorithm
{
    HashFunction CalculateHash { get; }
    NoiseFunction CalculateNoise { get; }
    int Maximum { get; }
    int Minimum { get; }
}

Внутренний класс реализует необходимый интерфейс:

public delegate double NoiseFunction(int seed, params int[] keys);

internal sealed class HashAlgorithm : IHashAlgorithm
{
    public HashAlgorithm(HashFunction function, int min, int max)
    {
        CalculateHash = function;
        Minimum = min;
        Maximum = max;
    }

    public HashFunction CalculateHash { get; private set; }

    public NoiseFunction CalculateNoise
    {
        get { return Noise; }
    }

    public int Maximum { get; private set; }
    public int Minimum { get; private set; }

    private double Noise(int seed, params int[] keys)
    {
        return ((double)CalculateHash(seed, keys) - Minimum)/
            ((double)Maximum - Minimum + 1);
    }
}

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

public static class Hashing
{
    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)
    {
        //...
    }
}

Я бы предпочел реализовать IHashAlgorithm для статических классов для каждого алгоритма:

public static class MurmurHash2 : IHashAlgorithm
{
    public static int Hash(int seed, params int[] keys) {...}

    //...
}

К сожалению, C # не позволяет этого, так что это моя попытка обойти это.

1 Ответ

2 голосов
/ 22 февраля 2012

Нет способа подделать статические интерфейсы классов, и много раз, когда я думал, что мне нужен один, мне действительно нужны обычные интерфейсы экземпляров. Вы не можете передать «экземпляр» статического класса в C #, нет способа дать функции «статический» интерфейс или даже статический «класс» для использования статических методов из него. Когда вы вызываете статический метод, он всегда является явным, и вы «жестко связываете» свой метод со статическим классом, который вы вызываете, и это не очень хорошая вещь.

Изменчивость, основанная на статических методах, трудно провести модульное тестирование. Классы, зависящие от такой изменчивости, менее гибкие. Представьте, что какая-то функция явно использует один из ваших алгоритмов из вашего статического класса. Такая функция явно связывает себя с этим конкретным алгоритмом.

public class SomeBusinessLogic
{
   public Result HandleDocument(IDocument doc)
   {
       // some transformations...

       int hash = Hashing.ReSharperHash.CalculateHash(seed, doc.Properties);

       // some other code ...
   }
}

Ну, что с этим не так?

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

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

  3. Когда вы проводите модульное тестирование, вы как бы тестируете как логику обработки документов, так и логику хеширования (которая, как предполагается, уже тестировалась его собственными модульными тестами). Если ваши тесты сравнивают вывод 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 ...
   }
}

Это решает проблемы, описанные выше:

  1. Класс явно объявляет, что это зависит от хеширования. Тот, кто предоставляет конкретный алгоритм хеширования, знает, чего ожидать от производительности, ресурсов, соединений, исключений и т. Д.

  2. Вы можете настроить, какой алгоритм хеширования используется для каждого экземпляра бизнес-логики или для каждого документа.

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

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

P.S. Есть также аспект "что является твоим доменом". Если вы пишете какое-то бизнес-приложение и называете static Math.Sqrt(...) здесь и там - это нормально, поскольку альтернативных вариантов поведения нет. Но если вы пишете какую-то математическую библиотеку и у вас есть несколько различных реализаций квадратного корня с разными алгоритмами или точностью, вы, вероятно, захотите заключить их в интерфейс и передать как экземпляры интерфейса, чтобы иметь возможность расширяться.

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