Yo uso el motor de juego cocos2d-x para crear juegos. El motor ya usa muchos singletons. Si alguien lo usó, entonces debería estar familiarizado con algunos de ellos:
Director
SimpleAudioEngine
SpriteFrameCache
TextureCache
EventDispatcher (was)
ArmatureDataManager
FileUtils
UserDefault
y muchos más con un total de aproximadamente 16 clases. Puede encontrar una lista similar en esta página: objetos Singleton en Cocos2d-html5 v3.0 Pero cuando quiero escribir en el juego necesito mucho más singletons:
PlayerData (score, lives, ...)
PlayerProgress (passed levels, stars)
LevelData (parameters per levels and level packs)
SocialConnection (Facebook and Twitter login, share, friend list, ...)
GameData (you may obtain some data from server to configure the game)
IAP (for in purchases)
Ads (for showing ads)
Analytics (for collecting some analytics)
EntityComponentSystemManager (mananges entity creation and manipulation)
Box2dManager (manages the physics world)
.....
¿Por qué creo que deberían ser solteros? Porque los necesitaré en lugares muy diferentes en mi juego, y el acceso compartido sería muy útil. En otras palabras, no sé qué crear en algún lugar y pasar punteros a toda mi arquitectura, ya que será muy difícil. Además, estas son cosas que solo necesito. En cualquier caso necesitaré varios, también puedo usar un patrón Multiton. Pero lo peor es que Singleton es el patrón más criticado debido a:
- bad testability
- no inheritance available
- no lifetime control
- no explicit dependency chain
- global access (the same as global variables, actually)
- ....
Puede encontrar algunas ideas aquí: https://stackoverflow.com/questions/137975/what-is-so-bad-about-singletons y https://stackoverflow.com/questions/4074154/when-should-the-singleton -patrón-no-ser-usado-además-de-lo-obvio
Entonces, creo que estoy haciendo algo mal. Creo que mi código huele mal . :) Me pregunto cómo los desarrolladores de juegos más experimentados resuelven este problema arquitectónico. Quiero verificar, tal vez todavía sea normal en el desarrollo del juego tener más de 30 singletons , considerados los que ya están en el motor del juego.
He pensado usar una Singleton-Facade que tendrá instancias de todas estas clases que necesito, pero cada una de ellas ya no sería singleton. Esto eliminará muchos problemas, y solo tendría un singleton que sería la Fachada misma. Pero en este caso tendré otro problema de diseño. La Fachada se convertirá en un OBJETO DE DIOS. Creo que esto también huele . Por lo tanto, no puedo encontrar una buena solución de diseño para esta situación. Por favor aconséjame.
fuente
Respuestas:
Inicializo mis servicios en mi clase de aplicación principal y luego los paso como punteros a lo que sea necesario para usarlos a través de los constructores o las funciones. Esto es útil por dos razones.
Uno, el orden de inicialización y limpieza es simple y claro. No hay forma de inicializar accidentalmente un servicio en otro lugar como puede hacerlo con un singleton.
Dos, está claro quién depende de qué. Puedo ver fácilmente qué clases necesitan qué servicios solo desde la declaración de clase.
Puede pensar que es molesto pasar todos estos objetos, pero si el sistema está bien diseñado, en realidad no está nada mal y aclara las cosas (para mí, de todos modos).
fuente
No discutiré sobre la maldad detrás de los singletons porque Internet puede hacerlo mejor que yo.
En mis juegos uso el patrón Localizador de servicios para evitar tener toneladas de Singletons / Managers.
El concepto es muy simple. Solo tiene un Singleton que actúa como la única interfaz para alcanzar lo que solía usar como Singleton. En lugar de tener varios Singleton, ahora solo tiene uno.
Llamadas como:
Se ve así, usando el patrón Localizador de servicios:
Como puede ver, cada Singleton está contenido en el Localizador de servicios.
Para tener una explicación más detallada, le sugiero que lea esta página sobre cómo usar el patrón de localización .
[ EDITAR ]: como se señala en los comentarios, el sitio gameprogrammingpatterns.com también contiene una sección muy interesante sobre Singleton .
Espero que ayude.
fuente
Leyendo todas estas respuestas, comentarios y artículos señalados, especialmente estos dos artículos brillantes,
Finalmente, llegué a la siguiente conclusión, que es una especie de respuesta a mi propia pregunta. El mejor enfoque es no ser perezoso y pasar dependencias directamente. Esto es explícito, comprobable, no hay acceso global, puede indicar un diseño incorrecto si tiene que pasar delegados muy profundos y en muchos lugares, etc. Pero en la programación, una talla no sirve para todos. Por lo tanto, todavía hay casos en los que no es útil pasarlos directamente. Por ejemplo, en caso de que creamos un marco de juego (una plantilla de código que se reutilizará como código base para desarrollar diferentes tipos de juegos) e incluiremos muchos servicios. Aquí no sabemos qué tan profundo puede pasar cada servicio y cuántos lugares será necesario. Depende de un juego en particular. Entonces, ¿qué hacemos en este tipo de casos? Utilizamos el patrón de diseño del Localizador de servicios en lugar de Singleton,
También debemos considerar que este patrón se usó en Unity, LibGDX, XNA. Esto no es una ventaja, pero es una prueba de la usabilidad del patrón. Tenga en cuenta que estos motores fueron desarrollados por muchos desarrolladores inteligentes durante mucho tiempo y durante las fases de evolución del motor todavía no encontraron una solución mejor.
En conclusión, creo que Service Locator puede ser muy útil para los escenarios descritos en mi pregunta, pero aún así debería evitarse si es posible. Como regla general, úselo si es necesario.
EDITAR: Después de un año de trabajo y utilizando DI directo y teniendo problemas con grandes constructores y muchas delegaciones, he investigado más y he encontrado un buen artículo que habla sobre los pros y los contras sobre los métodos DI y Localizador de servicios. Citando este artículo ( http://www.martinfowler.com/articles/injection.html ) de Martin Fowler que explica cuándo realmente los Localizadores de servicios son malos:
Pero de todos modos, si nos quiere DI por primera vez, debería leer este http://misko.hevery.com/2008/10/21/dependency-injection-myth-reference-passing/ artículo (señalado por @megadan) , prestando mucha atención a la Ley de Demeter (LoD) o al principio de menor conocimiento .
fuente
No es raro que partes de una base de código se consideren objetos de piedra angular o una clase básica, pero eso no justifica que su ciclo de vida se dicte como Singleton.
Los programadores a menudo confían en el patrón Singleton como un medio de conveniencia y holgazanería pura en lugar de adoptar el enfoque alternativo y ser un poco más detallado e imponer relaciones de objeto entre sí en una mansión explícita.
Entonces pregúntese cuál es más fácil de mantener.
Si eres como yo, prefiero poder abrir un archivo de encabezado y leer brevemente los argumentos del constructor y los métodos públicos para ver qué dependencias requiere un objeto en lugar de tener que inspeccionar cientos de líneas de código en un archivo de implementación.
Si ha convertido un objeto en un Singleton y luego se da cuenta de que necesita ser capaz de soportar múltiples instancias de dicho objeto, imagine los cambios radicales necesarios si esta clase fue algo que usó bastante en toda su base de código. La mejor alternativa es hacer explícitas las dependencias, incluso si el objeto solo se asigna una vez y si surge la necesidad de admitir múltiples instancias, puede hacerlo de manera segura sin tales preocupaciones.
Como otros han señalado, el uso del patrón Singleton también ofusca el acoplamiento que a menudo significa un diseño deficiente y una alta cohesión entre los módulos. Tal cohesión generalmente no es deseable y conduce a un código frágil que puede ser difícil de mantener.
fuente
Singleton es un patrón famoso, pero es bueno saber los propósitos a los que sirve, y sus ventajas y desventajas.
Realmente tiene sentido si no hay ninguna relación .
Si puede manejar su componente con un objeto totalmente diferente (sin dependencia fuerte) y espera tener el mismo comportamiento, el singleton puede ser una buena opción.
Por otro lado, si necesita una pequeña funcionalidad , que solo se usa en ciertos momentos, debería preferir pasar referencias .
Como mencionó, los singletons no tienen control de por vida. Pero la ventaja es que tienen una inicialización perezosa.
Si no llama a ese singleton en la vida de su aplicación, no se instanciará. Pero dudo que construyas singletons y no los uses.
Debe ver un singleton como un servicio en lugar de un componente .
Singletons funciona, nunca rompieron ninguna aplicación. Pero sus carencias (las cuatro que has dado) son razones por las que no harás una.
fuente
La falta de familiaridad no es la razón por la cual se deben evitar los singletons.
Hay muchas buenas razones por las que Singleton es necesario o inevitable. Los marcos de juego a menudo usan singletons porque es una consecuencia inevitable de tener un solo hardware con estado. No tiene sentido querer controlar estos hardwares con múltiples instancias de sus respectivos controladores. La superficie gráfica es un hardware externo con estado y accidentalmente inicializar una segunda copia del subsistema gráfico no es nada desastroso, ya que ahora los dos subsistemas gráficos se enfrentarán entre sí por quién dibuja y cuándo, sobrescribiéndose sin control. Del mismo modo, con el sistema de cola de eventos, se pelearán por quién obtiene los eventos del mouse de manera no determinista. Cuando se trata de un hardware externo con estado en el que solo hay uno de ellos, singleton es inevitable para evitar conflictos.
Otro lugar donde Singleton es sensible es con los administradores de caché. Los cachés son un caso especial. Realmente no deberían considerarse un Singleton, incluso cuando usan las mismas técnicas que los Singleton para mantenerse con vida y vivir para siempre. Los servicios de almacenamiento en caché son servicios transparentes, no se supone que alteren el comportamiento del programa, por lo que si reemplaza el servicio de caché con un caché nulo, el programa aún debería funcionar, excepto que solo se ejecuta más lentamente. Sin embargo, la razón principal por la que los administradores de caché son una excepción a singleton es porque no tiene sentido cerrar un servicio de caché antes de cerrar la aplicación en sí, porque eso también descartará los objetos en caché, lo que frustra el punto de tenerlo como un semifallo.
Esas son buenas razones por las cuales estos servicios son únicos. Sin embargo, ninguna de las buenas razones para que los singletons se apliquen a las clases que ha enumerado.
Esas no son razones para solteros. Esas son razones para los globales. También es una señal de mal diseño si necesita pasar muchas cosas por varios sistemas. Tener que pasar cosas indica un alto acoplamiento que puede evitarse con un buen diseño OO.
Solo por mirar la lista de clases en tu publicación que crees que deben ser Singletons, puedo decir que la mitad de ellos realmente no deberían ser Singletons y la otra mitad parece que ni siquiera deberían estar allí en primer lugar. El hecho de que necesite pasar objetos parece deberse a la falta de una encapsulación adecuada en lugar de a los casos de buen uso reales para singletons.
Veamos tus clases poco a poco:
Solo por los nombres de las clases, diría que estas clases huelen a antipatrón del modelo de dominio anémico . Los objetos anémicos generalmente generan muchos datos que deben pasarse, lo que aumenta el acoplamiento y hace que el resto del código sea engorroso. Además, la clase anémica oculta el hecho de que probablemente todavía esté pensando en el procedimiento en lugar de utilizar la orientación a objetos para encapsular los detalles.
¿Por qué estas clases deben ser singletons? Me parece que estas deberían ser clases de corta duración, que deberían mencionarse cuando sean necesarias y deconstruirse cuando el usuario finalice la compra o cuando ya no sea necesario mostrar los anuncios.
En otras palabras, ¿un constructor y una capa de servicio? ¿Por qué esta clase sin límites ni propósito claros, incluso allí en primer lugar?
¿Por qué el progreso del jugador está separado de la clase de jugador? La clase Player debe saber cómo realizar un seguimiento de su propio progreso, si desea implementar el seguimiento del progreso en una clase diferente a la clase Player para la separación de responsabilidades, entonces PlayerProgress debe estar detrás del Player.
Realmente no puedo comentar más sobre este sin saber lo que realmente hace esta clase, pero una cosa está clara es que esta clase está mal nombrada.
Estas parecen ser las únicas clases en las que Singleton puede ser razonable, porque los objetos de conexión social no son realmente sus objetos. Las conexiones sociales son solo una sombra de objetos externos que viven en un servicio remoto. En otras palabras, este es un tipo de caché. Solo asegúrese de separar claramente la parte de almacenamiento en caché con la parte de servicio del servicio externo, porque la última parte no debería ser un singleton.
Una forma de evitar pasar instancias de estas clases es mediante el uso de pasar mensajes. En lugar de realizar una llamada directa a instancias de estas clases, en su lugar, envía un mensaje dirigido al servicio Analytic y SocialConnection de forma asíncrona, y estos servicios se suscriben para recibir estos mensajes y actuar en consecuencia. Dado que la cola de eventos ya es un singleton, al trampolizar la llamada, evita la necesidad de pasar una instancia real cuando se comunica con un singleton.
fuente
Los solteros para mí SIEMPRE son malos.
En mi arquitectura, creé una colección de GameServices que administra la clase de juego. Puedo buscar agregar y quitar de esta colección según sea necesario.
Al decir que algo tiene un alcance global, básicamente estás diciendo "esta acción es Dios y no tiene maestría". En los ejemplos que proporcionas arriba, diría que la mayoría de ellos son clases auxiliares o GameServices, en cuyo caso el juego sería responsable de ellos.
Pero ese es solo mi diseño en mi motor, su situación puede ser diferente.
Poniéndolo más directamente ... El único singleton real es el juego, ya que posee cada parte del código.
Esta respuesta se basa en la naturaleza subjetiva de la pregunta y se basa únicamente en mi opinión, puede que no sea correcta para todos los desarrolladores.
Editar: según lo solicitado ...
Ok, esto es de mi cabeza ya que no tengo mi código frente a mí en este momento, pero esencialmente en la raíz de cualquier juego está la clase Game, que es verdaderamente un singleton ... ¡tiene que serlo!
Entonces agregué lo siguiente ...
Ahora también tengo una clase de sistema (estática) que contiene todos mis singletons de todo el programa (contiene muy pocas opciones de juego y configuración y eso es todo) ...
Y el uso ...
Desde cualquier lugar puedo hacer algo como
Este enfoque significa que mi juego "posee" cosas que normalmente se considerarían un "singleton" y puedo extender la clase base de GameService como quiera. Tengo interfaces como IUpdateable que mi juego busca y llama a cualquier servicio que lo implemente según sea necesario para situaciones específicas.
También tengo servicios que heredan / extienden otros servicios para escenarios de escenas más específicos.
Por ejemplo ...
Tengo un servicio de física que maneja mis simulaciones físicas, pero dependiendo de la escena, la simulación física puede necesitar un comportamiento ligeramente diferente.
fuente
var tService = Sys.Game.getService<T>(); tService.Something();
lugar de omitir lagetService
parte y acceder a ella directamente.Un ingrediente esencial de la eliminación de singleton que los oponentes de singleton a menudo no consideran es agregar lógica adicional para lidiar con el estado mucho más complicado que los objetos encapsulan cuando los singletons dan paso a los valores de instancia. De hecho, incluso definir el estado de un objeto después de reemplazar un singleton con una variable de instancia puede ser difícil, pero los programadores que reemplazan singletons con variables de instancia deben estar preparados para lidiar con él.
Compare, por ejemplo, Java
HashMap
con .NETDictionary
. Ambos son bastante similares, pero tienen una diferencia clave: JavaHashMap
está codificado para usarequals
yhashCode
(efectivamente usa un comparador singleton) mientras que .NETDictionary
puede aceptar una instancia deIEqualityComparer<T>
como parámetro de constructor. El costo en tiempo de ejecución de mantener elIEqualityComparer
es mínimo, pero la complejidad que introduce no lo es.Debido a que cada
Dictionary
instancia encapsula unIEqualityComparer
, su estado no es solo un mapeo entre las claves que realmente contiene y sus valores asociados, sino un mapeo entre los conjuntos de equivalencia definidos por las claves y el comparador y sus valores asociados. Si dos instanciasd1
yd2
deDictionary
mantener referencias a la mismaIEqualityComparer
, será posible producir una nueva instanciad3
tal que para cualquierax
,d3.ContainsKey(x)
será iguald1.ContainsKey(x) || d2.ContainsKey(x)
. Si, sin embargo,d1
yd2
puede contener referencias a diferentes comparadores de igualdad arbitrarias, habrá, en general, no hay manera de combinarlos con el fin de garantizar que condición se cumple.Agregar variables de instancia en un esfuerzo por eliminar los singletons, pero no tener en cuenta adecuadamente los posibles nuevos estados que podrían implicar esas variables de instancia puede crear un código que termina siendo más frágil de lo que hubiera sido con un singleton. Si cada instancia de objeto tiene un campo separado, pero las cosas funcionarán mal a menos que todas identifiquen la misma instancia de objeto, entonces todos los campos nuevos que han comprado son un mayor número de formas en que las cosas pueden salir mal.
fuente