Шаблон C # Singleton над унаследованными классами - PullRequest
4 голосов
/ 17 декабря 2010

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

В проекте, над которым я работаю, мне нужно использовать алгоритмы работы с порциями данных, назовем их DataCache. Иногда эти алгоритмы возвращают результаты, которые сами должны быть кэшированы, и поэтому я разработал схему.

У меня есть базовый класс Algorithm, который выглядит так

abstract class Algorithm<T>
    {
        protected abstract T ExecuteAlgorithmLogic(DataCache dataCache);
        private readonly Dictionary<DataCache, WeakReference> _resultsWeak = new Dictionary<DataCache, WeakReference>();        
        private readonly Dictionary<DataCache, T> _resultsStrong = new Dictionary<DataCache, T>();

        public T ComputeResult(DataCache dataCache, bool save = false)
        {
            if (_resultsStrong.ContainsKey(dataCache))
                return _resultsStrong[dataCache];

            if (_resultsWeak.ContainsKey(dataCache))
            {
                var temp = _resultsWeak[dataCache].Target;
                if (temp != null) return (T) temp;
            }

            var result = ExecuteAlgorithmLogic(dataCache);
            _resultsWeak[dataCache] = new WeakReference(result, true);
            if (save) _resultsStrong[dataCache] = result;

            return result;
        }
}

Если вы позвоните ComputeResult() и предоставите DataCache, вы можете при желании выбрать кеширование результата. Кроме того, если вам повезет, результат все еще может быть, если ГК еще не собрал его. Размер каждого DataCache составляет сотни мегабайт, и, прежде чем вы спросите, есть около 10 массивов в каждом, которые содержат основные типы, такие как int и float.

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

class ActualAgorithm : Algorithm<SomeType>
{
    protected override SomeType ExecuteAlgorithmLogic(DataCache dataCache)
    {
       //Elves be here
    }
}

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

class ActualAgorithm : Algorithm<SomeType>
{
    protected override SomeType ExecuteAlgorithmLogic(DataCache dataCache)
    {
       //Elves and dragons be here
    }
    protected ActualAgorithm(){ }
    private static ActualAgorithm _instance;
    public static ActualAgorithm Instance
    {
        get
        {
            _instance = _instance ?? new ActualAgorithm();
            return _instance;
        }
    }
}

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

Ответы [ 6 ]

2 голосов
/ 17 декабря 2010

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

// Where to find cached data
interface DataRepository {      
  void cacheData(Key k, Data d);
  Data retrieveData(Key k, Data d);
};

// If by any chance we need an algorithm somewhere
interface AlgorithmRepository {
  Algorithm getAlgorithm(Key k);
}

// The algorithm that process data
interface Algorithm {
  void processData(Data in, Data out);
}

С учетом этих интерфейсов мы можем определить некоторую базовую реализацию для хранилища алгоритмов:

class BaseAlgorithmRepository {
  // The algorithm dictionnary
  Map<Key, Algorithm> algorithms;

  // On init, we'll build our repository using this function
  void setAlgorithmForKey(Key k, Algorithm a) {
    algorithms.put(k, a);
  }

  // ... implement the other function of the interface
}

Тогда мы также можем реализовать что-то для DataRepository

class DataRepository {
  AlgorithmRepository algorithmRepository;
  Map<Key, Data> cache;

  void cacheData(Key k, Data d) {
    cache.put(k, d);
  }

  Data retrieveData(Key k, Data in) {
    Data d = cache.get(k);
    if (d==null) {
      // Data not found in the cache, then we try to produce it ourself
      Data d = new Data();
      Algorithm a = algorithmRepository.getAlgorithm(k);
      a.processData(in, d);

      // This is optional, you could simply throw an exception to say that the
      // data has not been cached and thus, the algorithm succession did not 
      // produce the necessary data. So instead of the above, you could simply:
      // throw new DataNotCached(k);
      // and thus halt the whole processing
    }
    return d;
  }
}

Наконец, мы можем реализовать алгоритмы:

abstract class BaseAlgorithm {
  DataRepository repository;
}

class SampleNoCacheAlgorithm extends BaseAlgorithm {
  void processData(Data in, Data out) {
    // do something with in to compute out
  }
}

class SampleCacheProducerAlgorithm extends BaseAlgorithm {
  static Key KEY = "SampleCacheProducerAlgorithm.myKey";

  void processData(Data in, Data out) {
    // do something with in to compute out
    // then call repository.cacheData(KEY, out);
  }
}

class SampleCacheConsumerAlgorithm extends BaseAlgorithm {
  void processData(Data in, Data out) {
    // Data tmp = repository.retrieveData(SampleCacheProducerAlgorithm.KEY, in);
    // do something with in and tmp to compute out
  }
}

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

class AlgorithmChain extends BaseAlgorithm {
  List<Algorithms> chain;

  void processData(Data in, Data out) {
    Data currentIn = in;
    foreach (Algorithm a : chain) {
      Data currentOut = new Data();
      a.processData(currentIn, currentOut);
      currentIn = currentOut;
    }
    out = currentOut;
  }
}

В дополнение к этому я бы добавил DataPool, который позволит вам повторно использовать существующие, но неиспользуемые объекты Data, чтобы избежать выделения большого количества памяти при каждом создании новых Data ().

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

2 голосов
/ 17 декабря 2010

То, что я имел в виду под своим комментарием, было примерно таким:

abstract class BaseClass<K,T> where T : BaseClass<K,T>, new()
{
    private static T _instance;
    public static T Instance
    {
        get
        {
            _instance = _instance ?? new T();
            return _instance;
        }
    }
}

class ActualClass : BaseClass<int, ActualClass>
{
    public ActualClass() {}
}

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine(ActualClass.Instance.GetType().ToString());
        Console.ReadLine();
    }
}

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

2 голосов
/ 17 декабря 2010

Ваши алгоритмы могут быть независимыми от их результатов:

class Engine<T> {
  Map<AlgorithmKey, Algorithm<T>> algorithms;
  Map<AlgorithmKey, Data> algorithmsResultCache;

  T processData(Data in);
}

interface Algorithm<T> {
  boolean doesResultNeedsToBeCached();
  T processData(Data in);
}

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

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

1 голос
/ 17 декабря 2010

Обычно, когда вы создаете класс Singleton, вы не хотите наследовать его. Когда вы делаете это, вы теряете некоторые достоинства синглтон-паттерна (и что я слышу от фанатиков-паттернов, так это то, что ангел теряет свои крылья каждый раз, когда вы делаете что-то подобное). Но давайте будем прагматичны ... иногда вы делаете то, что должны.

Несмотря на это, я не думаю, что объединение дженериков и наследования в любом случае сработает в этом случае.

Вы указали количество алгоритмов в десятках (а не в сотнях). Пока это так, я бы создал словарь с ключом System.Type и сохранил ссылки на ваши методы в качестве значений словаря. В этом случае я использовал Func<DataCache, object> в качестве сигнатуры значения словаря.

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

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

public sealed partial class Algorithm<T>
{
    private static object ExecuteForSomeType(DataCache dataCache)
    {
        return new SomeType();
    }
}

public sealed partial class Algorithm<T>
{
    private static object ExecuteForSomeOtherType(DataCache dataCache)
    {
        return new SomeOtherType();
    }
}

public sealed partial class Algorithm<T>
{
    private readonly Dictionary<System.Type, Func<DataCache, object>> _algorithms = new Dictionary<System.Type, Func<DataCache, object>>();
    private readonly Dictionary<DataCache, WeakReference> _resultsWeak = new Dictionary<DataCache, WeakReference>();
    private readonly Dictionary<DataCache, T> _resultsStrong = new Dictionary<DataCache, T>();

    private Algorithm() { }
    private static Algorithm<T> _instance;
    public static Algorithm<T> Instance
    {
        get
        {
            if (_instance == null)
            {
                _instance = new Algorithm<T>();
                _instance._algorithms.Add(typeof(SomeType), ExecuteForSomeType);
                _instance._algorithms.Add(typeof(SomeOtherType), ExecuteForSomeOtherType);
            }
            return _instance;
        }
    }

    public T ComputeResult(DataCache dataCache, bool save = false)
    {
        T returnValue = (T)(new object());
        if (_resultsStrong.ContainsKey(dataCache))
        {
            returnValue = _resultsStrong[dataCache];
            return returnValue;
        }
        if (_resultsWeak.ContainsKey(dataCache))
        {
            returnValue = (T)_resultsWeak[dataCache].Target;
            if (returnValue != null) return returnValue;
        }

        returnValue = (T)_algorithms[returnValue.GetType()](dataCache);
        _resultsWeak[dataCache] = new WeakReference(returnValue, true);
        if (save) _resultsStrong[dataCache] = returnValue;

        return returnValue;
    }
}
1 голос
/ 17 декабря 2010

Прежде всего, я бы предложил вам переименовать DataCache в нечто вроде DataInput для большей ясности, потому что его легко спутать с объектами, которые действительно действуют как кэши (_resultsWeak и _resultsStrong) для хранения результатов.

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

Вы также можете использовать AlgorithmLocator (см. Шаблон ServiceLocator ) в качестве единой точки доступа ко всем алгоритмам, чтобы избавиться от дублирования одноэлементной логики в каждом алгоритме.

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

Инкапсуляция логики кэширования в конкретном объекте, содержащемся в алгоритме (CachingStrategy?), Также была бы альтернативой его наследованию, но, возможно, немного неловко, поскольку объект кэширования будет иметьчтобы получить доступ к кешу до и после вычислений, и он должен был бы иметь возможность инициировать вычисление алгоритма и иметь руку с результатами.

[Редактировать], если вам нужно иметь один файл .cs на алгоритм,вы всегда можете сгруппировать все классы алгоритма, относящиеся к определенному T, в одном файле.

1 голос
/ 17 декабря 2010

Можете ли вы зарегистрировать свои экземпляры алгоритмов в объединенном хранилище / фабрике алгоритмов, которые будут хранить ссылки на них? Хранилище может быть одноэлементным, и, если вы дадите репозиторию возможность управления созданием алгоритма, вы сможете использовать его, чтобы гарантировать, что существует только один экземпляр каждого из них.

public class AlgorithmRepository
{
    //... use boilerplate singleton code

    public void CreateAlgorithm(Algorithms algorithm)
    {
        //... add to some internal hash or map, checking that it hasn't been created already
        //... Algorithms is just an enum telling it which to create (clunky factory
        //    implementation)
    }

    public void ComputeResult(Algorithms algorithm, DataCache datacache)
    {
        // Can lazy load algoirthms here and make CreateAlgorithm private ..
        CreateAlgorithm(algorithm);
        //... compute and return.
    }
}

При этом наличие отдельного класса (и файла cs) для каждого алгоритма имеет смысл для меня. Вы можете нарушить соглашение и иметь несколько классов алгоритмов в одном файле cs, если они легковесны, и вам легче управлять, если вы беспокоитесь о количестве файлов - есть худшие вещи делать. Я бы просто смирился с количеством файлов ...

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