Подтвердить что ты не робот

Хороший дизайн OO - шаблон дизайна Singleton

Внезапно у меня немного кризис ОО. За последние пару лет я неплохо использовал объекты Singleton. Я использовал их во многих местах.

Например, при разработке Java-приложения MVC я бы создал класс Singleton 'SystemRegistry' для хранения моделей и классов классов (я работал только с простыми приложениями, и необходимость в нескольких представлениях никогда не возникала).

Когда я создаю свою модель и просматриваю объекты (которые не были одиночными, просто нормальными объектами), я бы сделал что-то вроде:

SystemRegistry.getInstance().setModel(model);

В моих классах контроллера (которые были в значительной степени обработчиками событий для разных элементов GUI), я бы получил доступ к представлению или модели следующим образом:

SystemRegistry.getInstance().getView();

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

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

Кроме того, еще один аспект того, как я проектирую классы, которые могут или не могут быть связаны с Singletons, - это использование классов "Тип менеджера". Примером является (очень простой) движок OpenGL, созданный на С++.

Основным классом был GameEngine. Это был класс over-arcing, который хранил кучу менеджеров и обрабатывал основной цикл, а что нет. Некоторые из менеджеров, хранящихся в этом классе, были такими, как: ObjectManager, RenderingManager, LightingManager, EventManager (включая ввод), HUDManager, FrameRateManager, WindowManager и т.д. Возможно, было еще несколько.

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

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

В каждой новой игре я бы создал экземпляр GameEngine как переменной класса (большая часть игровой логики хранилась в одном классе) и настраивала разных менеджеров (например, загружая окно co- ординаты или детали освещения из файла, установка FPS и т.д.). Чтобы зарегистрировать объект в ObjectManager, я бы сделал что-то вроде:

Player player = new Player();
gameEngine.getObjectManager().addObject(player);

Этот объект теперь будет сохранен в векторе класса ObjectManager и будет нарисован, когда GameEngine вызывает метод ObjectOpject() объекта ObjectManager в каждом кадре.

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

Любые комментарии к моему сообщению будут высоко оценены.

Изменить: Спасибо за ответы. Я их очень ценю. Если возможно, мне бы хотелось, чтобы кто-то мог дать мне несколько советов относительно двух сценариев проекта, опубликованных выше. Как я мог избежать использования синглтонов/менеджеров?

С первым, будет ли DI правильным ответом? Должен ли я даже дать виду доступ к модели (вероятно, это скорее ответ MVC)? Будет ли представление выгодно реализовать интерфейс (чтобы можно было подключить несколько разных представлений)?

Во втором случае, как еще можно было структурировать приложение? Является ли gripe просто использованием классов Manager в отличие от более конкретных имен? Или в некоторых случаях классы могут быть дополнительно разбиты (например, ObjectHolder, ObjectDrawer, ObjectUpdater)?

4b9b3361

Ответ 1

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

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

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

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

И если вы сами придумали оправдание, можете ли вы сегодня понять, что с ним не так? Что вы забыли принять во внимание?

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

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

Затем вам понадобится немного кода с ответственностью за рисование всех объектов, хранящихся там. Но это не должно быть тем же самым объектом, что и хранилища.

Что еще нужно для управления? Узнайте, что означает "управление" этими объектами, а затем вы знаете, какие классы вам нужны для управления. Это, скорее всего, не одна монолитная задача, а несколько разных видов обязанностей, которые следует разделить на разные классы.

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

Один последний совет:

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

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

И независимо от языка, из функционального программирования есть много полезных уроков.

И иногда простое старое процедурное программирование - это просто приятный и простой инструмент, который вам нужен.

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

Что касается вашего Edit:

С первым, будет ли DI правильным ответом? Должен ли я даже дать виду доступ к модели (вероятно, это скорее ответ MVC)? Будет ли представление выгодно реализовать интерфейс (чтобы можно было подключить несколько разных представлений)?

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

Ваш рендерер должен знать о списке игровых объектов. (Или это действительно так? Может быть, у него есть только один метод, которому должен быть предоставлен список объектов. Может быть, класс рендеринга сам по себе ему не нужен). Поэтому конструктору рендеринга следует предоставить ссылку на этот список. Это действительно все, что нужно. Когда X нуждается в доступе к Y, вы передаете ему ссылку на Y в конструкторе параметра функции. Хорошая вещь об этом подходе, по сравнению с DI, заключается в том, что вы делаете свои зависимости болезненно явными. Вы знаете, что рендеринг знает о списке визуализируемых объектов, потому что вы можете увидеть ссылку, переданную конструктору. И вам пришлось написать эту зависимость, предоставив вам прекрасную возможность остановиться и спросить "это необходимо"? И у вас есть мотивация для устранения зависимостей: это означает, что вам не нужно печатать на машинке!

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

Еще один возможный недостаток DI - простой вопрос технологии. Не все языки имеют хорошие библиотеки DI. Некоторые языки делают невозможным создание надежной и общей библиотеки DI.

Во втором случае, как еще можно было структурировать приложение? Является ли gripe просто использованием классов Manager в отличие от более конкретных имен? Или в некоторых случаях классы могут быть дополнительно разбиты (например, ObjectHolder, ObjectDrawer, ObjectUpdater)?

Я думаю, что просто переименование их - хорошее начало, да. Как я уже сказал выше, что значит "управлять" вашими объектами? Если я буду управлять этими объектами, что я должен делать? Три упомянутых вами класса звучат как хорошее разделение. Конечно, когда вы копаете в разработке этих классов, вы можете задаться вопросом, зачем вам даже нужен ObjectHolder. Разве это не то, что делают стандартные классы библиотеки/коллекции библиотеки? Вам нужен выделенный класс для "хранения объектов", или вы можете уйти просто с помощью List<GameObject>?

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

Представьте, что вы отложили код на полгода. Когда вы вернетесь к нему, вы поймете, какова цель класса "ObjectManager"? Наверное, нет, но у вас будет довольно хорошая идея, для чего предназначен "ObjectRenderer". Он отображает объекты.

Ответ 2

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

Не путайте только один экземпляр класса с шаблоном singleton. В вашем случае хорошо иметь класс менеджера, который управляет всеми вещами. Также вполне здорово, что для всего вашего приложения есть только один экземпляр. Но классы, которые используют диспетчер, не должны полагаться на существование точно одного глобального объекта (будь то через статическую переменную для класса или глобальную переменную, к которой обращаются все). Чтобы смягчить это, заставьте их потребовать ссылку на объект-менеджер при их создании. Они могут получить ссылку на один и тот же объект, им все равно. Это имеет два больших побочных эффекта. Во-первых, вы можете лучше проверить свои классы, потому что вы можете легко вводить объект mock-manager. Второе преимущество заключается в том, что, если вы решите это позже, вы все равно можете использовать более одного объекта-менеджера (например, в сценариях с несколькими потоками), не меняя клиентов.

Ответ 3

Стив Йегг прав. Google идет еще дальше: у них есть детектор Singleton, чтобы помочь их искоренить.

Я думаю, что есть несколько проблем с Singletons:

  • Сложнее проверить.
  • Устоявшиеся синглтоны должны быть осторожны с безопасностью потоков; легко стать узким местом.

Но иногда у вас действительно есть только один. Я бы просто не увлекся малым мальчиком с синдромом рисунка.

Ответ 4

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

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

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

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

Ответ 5

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

Ответ 6

Двухэтапный ответ:

1) http://misko.hevery.com/2008/08/17/singletons-are-pathological-liars/

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

2) http://misko.hevery.com/2008/08/21/where-have-all-the-singletons-gone/

Глобальному доступу противопоставляется использование инъекции зависимостей.