Рефакторинг Одиночного Злоупотребления - PullRequest
45 голосов
/ 28 мая 2010

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

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

Мне нужна помощь в переосмыслении этого. Я знаю о Внедрении Зависимости, и это, как правило, стратегия, которая используется для снятия проклятия Синглтона. Однако у меня есть несколько конкретных вопросов, связанных с этим рефакторингом, и все о передовых методах для этого.

  1. Насколько допустимо использование статических переменных для инкапсуляции информации о конфигурации? У меня есть мозговой блок по использованию статики, и я думаю, что это связано с ранним уроком ОО в колледже, где профессор сказал, что статика плохая. Но должен ли я перенастраивать класс каждый раз, когда получаю к нему доступ? При доступе к оборудованию можно оставить статический указатель на необходимые адреса и переменные или я должен постоянно выполнять операции Open() и Close()?

  2. Прямо сейчас у меня есть один метод, действующий в качестве контроллера. В частности, я постоянно запрашиваю данные у нескольких внешних инструментов (через драйверы оборудования). Должен ли этот тип контроллера быть подходящим, или я должен создавать отдельные потоки для каждого инструмента при запуске программы? Если последнее, как мне сделать этот объект ориентированным? Должен ли я создавать классы с именами InstrumentAListener и InstrumentBListener? Или есть какой-то стандартный подход к этому?

  3. Есть ли лучший способ выполнить глобальную настройку? Прямо сейчас я просто разбросал Configuration.Instance.Foo по всему коду. Почти каждый класс использует его, поэтому, возможно, имеет смысл сохранять его как синглтон. Есть мысли?

  4. Многие из моих классов - это такие вещи, как SerialPortWriter или DataFileWriter, которые должны сидеть и ждать, пока эти данные будут переданы. Так как они активны все время, как мне организовать их, чтобы прослушивать события, генерируемые при поступлении данных?

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

Ответы [ 7 ]

13 голосов
/ 28 мая 2010

Хорошо, вот мой лучший шанс атаковать этот вопрос:

(1) Статика

Проблема с static, которая может у вас возникать, заключается в том, что это означает разные вещи в .NET и, например, C ++. Статика в основном означает, что она доступна для самого класса. Что касается Accetability ID, то это говорит о том, что это нечто большее, чем то, что вы будете использовать для выполнения операций, не относящихся к экземпляру, над классом. Или просто общие вещи вроде Math.Abs(...). То, что вы должны использовать для глобальной конфигурации, вероятно, является статически доступным свойством для хранения текущей / активной конфигурации. Также возможно некоторые статические классы для загрузки / сохранения настроек конфигурации, однако конфигурация должна быть Object , чтобы ее можно было обрабатывать и т. Д. открытый класс MyConfiguration { public const string DefaultConfigPath = "./config.xml";

  protected static MyConfiguration _current;
  public static MyConfiguration Current
  {
    get
    {
      if (_current == null)
        Load(DefaultConfigPath);
      return _current;
    }
  }

  public static MyConfiguration Load(string path)
  {
    // Do your loading here
    _current = loadedConfig;
    return loadedConfig; 
  }

  // Static save function

  //*********** Non-Static Members *********//

  public string MyVariable { get; set; }
  // etc..
}

(2) Контроллер / Аппаратное обеспечение

Возможно, вам следует рассмотреть реактивный подход, IObserver<> или IObservable<>, это часть Reactive Framework (Rx) .

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

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

(3) Глобальная конфигурация

Возможно, я затронул этот вопрос в пункте № 1, но в целом именно к этому мы и стремимся. Если вы набираете слишком много текста, вы всегда можете вытащить его оттуда, предполагая, что .Instance - это объект.

MyConfiguration cfg = MyConfiguration.Current
cfg.Foo // etc...

(4) Прослушивание данных

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

4 голосов
/ 28 мая 2010

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

Это не "исправление", а улучшение, оно делает многие объекты, которые являются одиночками, немного более нормальными и проверяемыми. например ... (полностью надуманный пример)

HardwareRegistry.SerialPorts.Serial1.Send("blah");

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

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

Статический: -

Множество исключений из правил здесь, но в целом избегайте этого, но это полезно для выполнения одиночных операций и создания методов, которые выполняют «общие» вычисления вне контекста объекта. (как Math.Min)

Мониторинг данных: -

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

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

Конфигурация: -

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

Последовательные порты: -

Я делаю кучу работы с ними, у меня есть последовательное соединение, которое генерирует поток символов, которые он публикует как событие. Тогда у меня есть кое-что, что интерпретирует поток протокола в значимые команды. Мои классы протокола работают с универсальным «IConnection», которому наследует SerialConnection ..... У меня также есть TcpConnections, MockConnections и т. Д., Чтобы можно было вводить тестовые данные или передавать последовательные порты с одного компьютера на другой и т. Д. Итак Классы протокола просто интерпретируют поток, имеют машину состояний и отправляют команды. Протокол предварительно сконфигурирован с подключением. Различные протоколы регистрируются в протоколе, поэтому, когда он имеет значимые данные, они будут запущены и сделают свое дело. Все это строится из конфигурации в начале или перестраивается на лету, если что-то меняется.

1 голос
/ 28 мая 2010
  1. Вы (ОП), кажется, озабочены ОО-дизайном, ну, я скажу это так, думая о статических переменных.Основная концепция - инкапсуляция и повторное использование;что-то, что вам не нужно беспокоиться о повторном использовании, но вы почти всегда хотите инкапсуляцию.Если это статическая переменная, она не инкапсулирована, не так ли?Подумайте, кому нужен доступ к нему, почему и как далеко вы можете скрыть его от клиентского кода.Хорошие дизайны часто могут изменить их внутренности без особой поломки для клиентов, вот о чем вы хотите подумать .Я согласен со Скоттом Мейерсом (Effective C ++) во многих вещах.ООП выходит далеко за рамки ключевого слова класса.Если вы никогда не слышали об этом, посмотрите свойства: да, они могут быть статическими, и C # имеет очень хороший способ их использования.В отличие от буквально с использованием статической переменной .Как я и намекнул в начале этого списка: подумайте о том, как не выстрелить себе в ногу позже , так как класс меняется со временем, что многие программисты не могут сделать при разработке классов.

  2. Взгляните на ту платформу Rx, о которой кто-то упоминал.Используемая модель потоков для такой ситуации, как вы описали, не может быть легко решена без дополнительных подробностей о сценарии использования IMHO.Убедитесь, что вы знаете, что вы делаете с потоками.Многие люди не могут придумать, как спасти свою жизнь;это не так сложно, быть уверенным в том, что можно быть безопасным при повторном использовании кода.Помните, что контроллеры часто должны быть отделены от объектов, которыми они управляют (например, не тот же класс);если вы этого не знаете, найдите книгу о MVC и купите банду из четырех человек.

  3. Зависит от того, что вам нужно.Для многих приложений класс, который почти полностью заполнен статическими данными, достаточно хорош;как синглтон бесплатно.Это можно сделать очень хорошо.Иногда вы предпочитаете иметь несколько экземпляров или играть с инъекцией, что делает его более сложным.

  4. Я предлагаю темы и события.Простота создания кода, управляемого событиями, на самом деле является одной из самых приятных вещей в C # IMHO.

Хм, убивая одиночек ...

По моему опыту, многоеиз более распространенных применений, в которые молодые программисты помещают синглтоны, - это всего лишь пустая трата ключевого слова класса.А именно то, что они имели в виду, когда модуль с состоянием находится в классе highlander ;и есть несколько плохих одноэлементных реализаций, чтобы соответствовать.Я не знаю, было ли это из-за того, что они не смогли узнать , что они делают, или имели только Java в колледже.Вернувшись на землю C, это называется использованием данных в области видимости файла и раскрытием API.В C # (и Java) вы как бы связаны с тем, чтобы быть классом больше, чем многие языки.ООП! = Ключевое слово класса;хорошо выучите lhs.

Прилично написанный класс может использовать статические данные для эффективной реализации синглтона и заставить компилятор выполнять всю работу по поддержанию его единства или единства, которого вы когда-либо добьетесь от всего, НЕ заменяйте синглтоны наследованием, если вы серьезно не знаете, какого черта вы делаете.Плохо выполненное наследование таких вещей приводит к более хрупкому коду, который знает о многом.Занятия должны быть тупыми, данные умными.Это звучит глупо, если вы не посмотрите на утверждение глубоко .Использование наследования IMHO для такой вещи, как правило, плохо (тм), языки имеют понятие модулей / пакетов по определенной причине.

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

Вот одно предложение для XXX способа начать с: (визуализация) записи (^ Hing) класса составного контроллера, который работает как менеджер над объектами, на которые он ссылается. Эти объекты были вашими синглетонами, их не контролер, а экземпляры этих классов. Это не лучший дизайн для многих приложений (особенно это может быть проблемой в многопоточных IMHO), но он, как правило, решает то, что заставляет большинство молодых людей тянуться к одиночке, и он будет подходящим для широкого спектра программы. Это как шаблон проектирования CS 102. Забудьте о синглтоне, который вы выучили в CS 607.

Этот управляющий класс, возможно, «Application», был бы более подходящим;), в основном решает вашу потребность в синглетах и ​​для хранения конфигурации. Как это сделать возвышенно OO-способом (если вы do понимаете ООП) и не стреляйте себе в ногу (опять же), это упражнение для вашего собственного образования.

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

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

1 голос
/ 28 мая 2010

Поскольку вы знаете о Dependency Injection, рассматривали ли вы возможность использования контейнера IoC для управления временем жизни? См. мой ответ на вопрос о статических классах.

0 голосов
/ 28 мая 2010

Отличный вопрос. Несколько быстрых мыслей от меня ...

static в C # следует использовать только для данных, которые точно одинаковы для всех экземпляров данного класса , Так как вы в настоящее время застряли в аду Синглтона, у вас все равно есть только один экземпляр всего, но как только вы выйдете из этого, это общее правило (по крайней мере, для меня). Если вы начнете создавать потоки в своих классах, вы можете отказаться от статического использования, потому что тогда у вас есть потенциальные проблемы с параллелизмом, но это можно решить позже.

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

Удачи.

0 голосов
/ 28 мая 2010

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

(1) Сгруппируйте всю «глобальную» информацию в один класс. Давайте назовем это Configuration.

(2) Для всех классов, которые использовали эти статические объекты, измените их на (в конечном итоге) наследование от нового абстрактного базового класса, который выглядит примерно так:

abstract class MyBaseClass {
    protected Configuration config; // You can also wrap it in a property
    public MyBaseClass(Configuration config) {
        this.config = config;
    }
}

(3) Измените все конструкторы классов, производных от MyBaseClass соответственно. Затем просто создайте один экземпляр Configuration при запуске и передавайте его везде.

Минусы:

  • Вам нужно провести рефакторинг многих ваших конструкторов и каждого места, где они называются
  • Это не сработает, если вы не извлекаете классы верхнего уровня из Object. Ну, вы можете добавить поле config к производному классу, оно менее элегантно.

Плюсы

  • Не так много усилий, чтобы просто изменить наследование и конструкторы, и bang - вы можете переключать все Configuration.Instance с помощью config.
  • Вы полностью избавляетесь от статических переменных; так что никаких проблем сейчас нет, если, например, ваше приложение внезапно превращается в библиотеку, и кто-то пытается вызвать несколько методов одновременно или как угодно.
0 голосов
/ 28 мая 2010

Я ограничиваю себя максимум двумя синглетами в приложении / процессе. Один обычно называется SysConfig и содержит вещи, которые в противном случае могли бы оказаться глобальными переменными или другими искаженными понятиями. У меня нет названия для второго, так как до сих пор я никогда не достигал своего предела. : -)

Статические переменные-члены имеют свое применение, но я рассматриваю их так же, как и проктологов. Спасатель, когда вам нужен один, но шансы должны быть «миллион к одному» (ссылка на Seinfeld), что вы не можете найти лучший способ решения проблемы.

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

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