c++ - ventajas - vista logica 4+1 ejemplo
Arquitectura de una aplicación que abre múltiples documentos(proyectos). (4)
Estoy trabajando en una aplicación CAD basada en Qt y estoy tratando de averiguar la arquitectura de la aplicación. La aplicación puede cargar múltiples proyectos con planos, secciones, etc., y mostrar estos dibujos en vistas dedicadas. Hay por proyecto y configuraciones globales.
La aplicación está representada por el objeto global, derivado de QApplication
:
class CADApplication Q_DECL_FINAL: public QApplication {
Q_OBJECT
public:
explicit CADApplication(int &args, char **argv);
virtual ~CADApplication();
...
ProjectManager* projectManager() const;
ConfigManager* configManager() const;
UndoManager* undoManager() const;
protected:
const QScopedPointer<ProjectManager> m_projectManager;
const QScopedPointer<ConfigManager> m_configManager;
...
};
Los "administradores" se crean en el CADApplication
la aplicación CADApplication
. Son responsables de la funcionalidad relacionada con los proyectos cargados ( ProjectManager
), las opciones de configuración global ( ConfigManager
), etc.
También hay vistas de proyectos, cuadros de diálogo de opciones de configuración y otros objetos que pueden necesitar acceso a los "administradores".
Para obtener el proyecto actual, el SettingsDialog
necesita:
#include "CADApplication.h"
#include "ProjectManager.h"
...
SettingsDialog::SettingsDialog(QWidget *parent)
: QDialog(parent)
{
...
Project* project = qApp->projectManager()->currentProject();
...
}
Lo que me gusta de todo el enfoque es que sigue el paradigma RAII. Los "administradores" se crean y destruyen en la instanciación / destrucción de la aplicación.
Lo que no me gusta es que no sea propenso a referencias circulares, y que necesito incluir "CADApplication.h" de cada archivo fuente, donde se requiere la instancia de cualquiera de los "administradores". Es como si el objeto CADApplication
se usara como una especie de "titular" global de estos "administradores".
He hecho algunas investigaciones. Parece que también hay varios otros enfoques que implican el uso de singletons. OpenToonz utiliza el singleton de TProjectManager
:
class DVAPI TProjectManager {
...
public:
static TProjectManager *instance();
...
};
TProjectManager* TProjectManager::instance() {
static TProjectManager _instance;
return &_instance;
}
En cada archivo, donde necesitan acceder al administrador de proyectos:
#include "toonz/tproject.h"
...
TProjectManager *pm = TProjectManager::instance();
TProjectP sceneProject = pm->loadSceneProject(filePath);
Según su experiencia, ¿cuál de estos enfoques debería seguir para buscar una buena arquitectura y hacer que las aplicaciones sean propensas a errores y simplificar las pruebas unitarias? Tal vez hay otros paradigmas?
Posiblemente sea más sencillo seguir el ejemplo de Qt, modificado para los tiempos modernos. Qt utiliza una macro global para referirse a la instancia, por ejemplo, en qapplication.h
:
#define qApp (static_cast<QApplication *>(QCoreApplication::instance()))
En su caso, sabemos que el tipo de aplicación global singleton es CADApplication
. Ya que qApp
está ahí para bien o para mal, no hay nada de malo en aprovecharlo: no se agrega a la contaminación del espacio de nombres global. Así:
// cadapplication.h
...
#if defined(qApp)
#undef qApp
#endif
#define qApp (static_cast<CADApplication*>(QCoreApplication::instance()))
Entonces, por ejemplo, pMgr
convierte en:
#define pMgr (qApp->projectManager())
Sin embargo, consideraría que la falta de espacios de nombres y el pMgr
global y macros similares son un mal olor de código. En lugar de una macro, tenga una función en línea en el espacio de nombres:
// cadapplication.h
...
namespace CAD {
class Application : public QApplication {
ProjectManager m_projectManager; /* holding the value has less overhead */
public:
inline ProjectManager* projectManager() const { return &m_projectManager; }
...
};
inline ProjectManager* pMgr() {
return static_cast<CAD::Application*>(QCoreApplication::instance())->projectManager();
}
}
#if defined(qApp)
#undef qApp
#endif
#define qApp (static_cast<CAD::Application*>(QCoreApplication::instance()))
Entonces:
#include "projectmanager.h"
...
CAD::pMgr()->doSomething();
/* or */
using namespace CAD;
pMgr()->doSomething();
Si le disgusta particularmente que el uso de pMgr
tenga que ser una llamada de función, puede convertirlo en una instancia de reenviador global, no introducirá ningún gasto general.
// cadapplication.h
namespace CAD {
...
namespace detail {
struct ProjectManagerFwd {
inline ProjectManager* operator->() const {
return qApp->projectManager();
}
inline ProjectManager& operator*() const {
return *(qApp->projectManager());
}
};
}
extern detail::ProjectManagerFwd pMgr;
}
// cadapplication.cpp
...
detail::ProjectManagerFwd pMgr;
...
Entonces:
#include "cadapplication.h"
...
CAD::pMgr->doSomething();
/* or */
using namespace CAD;
pMgr->doSomething();
En ningún caso es necesario un encabezado especial.
Incluso si no usa espacios de nombres (¡¿por qué ?!), los globales deben estar en un espacio de nombres (ya sea la pMgr
función o la pMgr
instancia).
Trabajo en VFX, que es un poco diferente de CAD pero no muy diferente al menos para el modelado. Allí me ha resultado muy útil revolucionar el diseño de la aplicación para modelar alrededor del patrón de comando. Por supuesto, no necesariamente tiene que hacer alguna interfaz ICommand
para eso. Puede usar std::function
y lambdas, por ejemplo.
Eliminación / reducción de dependencias centrales a instancias de un solo objeto
Sin embargo, hago las cosas de forma un poco diferente para el estado de la aplicación en que prefiero un paradigma más "pull" en lugar de un "push" para el tipo de cosas que estás haciendo (trataré de explicar esto mejor a continuación en términos de lo que quiero decir con "empujar / tirar" *) para que en lugar de un bote de cosas en "el mundo" acceda a estas pocas instancias del administrador central y les diga qué hacer, los pocos gerentes centrales "accedan al mundo" (no directamente) y averiguar qué hacer.
- Disculpas por adelantado. No soy una persona muy precisa desde el punto de vista técnico y el inglés tampoco es mi idioma nativo (de Japón). Si puede soportar mis malos intentos de describir cosas, creo que habrá información útil para algunas personas. Al menos me fue útil de una manera que tuve que aprender de la manera más difícil.
Lo que propongo no solo erradicará los singletons, sino que erradicará el fan-in hacia las instancias de objetos de la aplicación central en general, lo que puede hacer que las cosas sean mucho más fáciles de razonar, hacer que el hilo sea seguro, la prueba unitaria, etc. Incluso si usa la dependencia La inyección sobre singletons, tales objetos de aplicación general y el estado compartido, en gran medida dependientes, pueden hacer que muchas cosas sean más difíciles, desde el razonamiento sobre los efectos secundarios hasta la seguridad de subprocesos de su código base.
Lo que quiero decir es que en lugar de, digamos, cada comando que se escribe en un administrador de deshacer que requiera que todo lo que puede escribir el estado de deshacer se asocie a este sistema de deshacer, invierto el flujo de comunicación fuera de la instancia del objeto central. En lugar de esto (avanza hacia la instancia de un solo objeto):
Le sugiero que desvíe / invierta la comunicación (despliegue desde una instancia central a muchas instancias):
Y al sistema de deshacer ya no se le dice a los demás qué hacer. Averigua qué hacer al acceder a la escena (específicamente los componentes de la transacción). En el nivel conceptual amplio, lo considero en términos de "empujar / tirar" (aunque me han acusado de ser un poco confuso con esta terminología, pero no he encontrado una mejor manera de describirlo o pensar en ello). curiosamente, fue un colega quien originalmente describió esto como "tirar" los datos en lugar de "empujarlos" en respuesta a mis malos intentos de describir cómo funcionaba un sistema para el equipo y su descripción fue tan intuitiva para mí que se ha atascado conmigo desde entonces. En términos de dependencias entre las instancias de objetos (no los tipos de objetos), es un tipo de reemplazo de entrada de ventilador con salida de ventilador.
Minimizar el conocimiento
Cuando haces eso, ya no tienes que depender de este tipo de cosas de forma centralizada por tantas cosas, y ya no tienes que incluir su archivo de encabezado por todos lados.
Todavía hay un componente de transacción para el cual todo lo que se puede deshacer tiene que incluir el archivo de encabezado, pero es mucho, mucho más simple que el sistema de deshacer en toda regla (en realidad es solo datos), y no se crea una instancia de una sola vez para toda la aplicación (se obtiene instanciado localmente para cada cosa que necesita registrar acciones que no se pueden hacer).
El permite que "el mundo" trabaje con menos conocimiento. El mundo no tiene que saber acerca de los administradores de deshacer en toda regla, y los administradores de deshacer no tienen que saber sobre el mundo entero. Ambos ahora solo tienen que saber acerca de estos componentes de transacciones súper simples.
Deshacer sistemas
Específicamente para los sistemas de deshacer, logro este tipo de "inversión de comunicación" haciendo que cada objeto de escena ("Cosa" o "Entidad") escriba en su propio componente de transacción local. El sistema de deshacer luego, periódicamente (por ejemplo, después de ejecutar un comando) atraviesa la escena y reúne todos los componentes de transacción de cada objeto en la escena y la consolida como una sola entrada que el usuario no puede deshacer, así (pseudocódigo):
void UndoSystem::process(Scene& scene)
{
// Gather the transaction components in the scene
// to a single undo entry.
local undo_entry = {}
// Our central system loop. We loop through the scene
// and gather the information for the undo system to
// to work instead of having everything in the scene
// talk to the undo system. Most of the system can
// be completely oblivious that this undo system even
// exists.
for each transaction in scene.get<TransactionComponent>():
{
undo_entry.push_back(transaction);
// Clear the transaction component since we''ve recorded
// it to the undo entry so that entities can work with
// a fresh (empty) transaction.
transaction.clear();
}
// Record the undo entry to the undo system''s history
// as a consolidated user-undoable action. I used ''this''
// just to emphasize the ''history'' is a member of our
// undo system.
this->history.push_back(undo_entry);
}
Esto tiene la ventaja adicional de que cada entidad en la escena puede escribir en su propio componente de transacción local asociado en su propio hilo separado sin tener que, por ejemplo, lidiar con bloqueos en rutas de ejecución críticas que intentan escribir directamente en un sistema central de deshacer ejemplo.
También puede tener más de una instancia de sistema de deshacer utilizando este enfoque. Por ejemplo, podría ser un poco confuso si un usuario está trabajando en "Escena B" o "Proyecto B" y pulsa Deshacer solo para deshacer un cambio en "Escena A / Proyecto A" en el que no están trabajando inmediatamente. Si no pueden ver lo que está sucediendo, incluso podrían deshacer inadvertidamente los cambios que querían mantener. Sería como si presionara Deshacer en Visual Studio mientras trabajaba en Foo.cpp
y deshaciera accidentalmente los cambios en Bar.cpp
. Como resultado, a menudo desea más de una instancia de deshacer sistema / administrador en un software que permita múltiples documentos / proyectos / escenas. No deben ser necesariamente singletons y mucho menos objetos de toda la aplicación, y en su lugar a menudo deben ser objetos de documento / proyecto / escena local. Este enfoque le permitirá hacerlo fácilmente y también cambiará de opinión más adelante, ya que minimiza la cantidad de dependencias en el sistema a su administrador de deshacer (quizás solo un solo comando o dos en su sistema necesitan depender de él *).
- Incluso puede desacoplar sus comandos de deshacer / rehacer del sistema de deshacer haciendo que, por ejemplo, empuje un
UserUndoEvent
oUserRedoEvent
a una cola central a la que puedan acceder tanto los comandos como los sistemas de deshacer. De nuevo, esto los hace dependientes de estos eventos y de la cola de eventos, pero el tipo de evento podría ser mucho más simple (podría ser un entero que almacena un valor para una constante nombrada predefinida). Es el mismo tipo de estrategia aplicada aún más.
Evitar la entrada de fan a instancias de objetos centrales
Esta es mi estrategia preferida cada vez que encuentro un caso en el que quiere que muchas, muchas cosas dependan de una instancia de objeto central con un gran fan-in. Y eso puede ayudar a todo tipo de cosas a reducir los tiempos de compilación, permitiendo que ocurran cambios sin reconstruir todo, multihilo más eficiente, permitiendo cambios de diseño sin tener que volver a escribir un barco cargado de código, etc.
Y eso también puede aliviar la tentación de usar singletons. Para mí, si la inyección de dependencia es un PITA real y hay una fuerte tentación de usar singletons, lo veo como un signo para considerar un enfoque diferente del diseño hasta que encuentre uno en el que la inyección de dependencia ya no sea un PITA, que generalmente se logra a través de una gran cantidad de desacoplamiento, específicamente de un tipo que evita la entrada de un barco cargado de diferentes lugares en el código.
La búsqueda no es el resultado de tratar celosamente de evitar los singletons, sino incluso de manera pragmática, los tipos de diseños que nos tientan a usarlos son a menudo difíciles de escalar sin que su complejidad se vuelva abrumadora en algún momento, ya que implica una gran cantidad de dependencias hacia el centro. los objetos (y sus estados) en todas partes, y esas dependencias e interacciones se vuelven realmente difíciles de razonar en términos de lo que realmente está sucediendo después de que su base de código alcance una escala suficientemente grande. La inyección de dependencia por sí sola no resuelve ese problema necesariamente, ya que aún podemos terminar con una gran cantidad de dependencias a objetos de toda la aplicación inyectados por todo el lugar. En su lugar, lo que encontré para mitigar significativamente este problema es buscar diseños que hagan que la inyección de dependencia ya no resulte dolorosa, lo que significa eliminar la mayor parte de esas dependencias a instancias de objetos de toda la aplicación.
Todavía tengo una cosa central, muy generalizada y simple de la que dependen muchas cosas en mi caso, y lo que propongo necesitará al menos una estructura de datos generalizada que esté bastante centralizada. Yo uso un sistema de componente de entidad como este:
... o esto:
Por lo tanto, cada sistema en mi caso depende de la instancia de la "base de datos" de ECS central y no he podido evitar la acumulación de información. Sin embargo, eso es bastante fácil de inyectar ya que solo hay unas pocas docenas de sistemas pesados que necesitan una inyección de dependencia como un sistema de modelado, un sistema de física, un sistema de renderización, un sistema GUI. Los comandos también se ejecutan con la "base de datos" de ECS que se les pasa de manera uniforme por un parámetro para que puedan acceder a lo que sea necesario en la escena sin tener que acceder a un singleton.
Diseños a gran escala
Por encima de todas las demás cosas, al explorar varias metodologías de diseño, patrones y métricas de SE, he encontrado que la forma más efectiva de permitir que los sistemas se amplíen en complejidad es aislar las secciones importantes de "su propio mundo", confiando en la cantidad mínima de informacion al trabajo. El "desacoplamiento completo" de un tipo que minimiza no solo la información concreta requerida para que algo funcione, sino que también minimiza la información abstracta, es, ante todo, la forma más efectiva que he encontrado para permitir que los sistemas se amplíen sin abrumar. Los desarrolladores los mantienen. Para mí es una gran señal de advertencia cuando tienes, digamos, un desarrollador que sabe que las NURB salen a la superficie y pueden crear demostraciones impresionantes relacionadas con ellas, pero está detectando errores en tu sistema en su implementación de loft, no porque la implementación de las NURB sea incorrecta. , pero porque está tratando con los estados centrales de la aplicación de forma incorrecta.
El "desacoplamiento completo" divide cada sección del código base, como un sistema de renderizado, en su propio mundo aislado. Apenas necesita información del mundo exterior para trabajar. Como resultado, el desarrollador de renderizado puede hacer lo suyo sin saber mucho sobre lo que está sucediendo en otros lugares. La alternativa cuando no tiene este tipo de "mundos aislados" es un sistema cuya complejidad centralizada se derrama en cada rincón del software, y el resultado es este "todo orgánico", que es muy difícil de razonar y requiere. cada desarrollador para saber algo sobre casi todo.
El pensamiento algo (posiblemente) inusual que tengo es que las abstracciones a menudo se consideran la forma clave de desacoplar sistemas y objetos, pero una abstracción solo reduce la cantidad de información concreta requerida para que algo funcione. Es posible que aún encontremos un sistema que se convierta en este "todo orgánico" al tener una gran cantidad de información abstracta que todo necesita para funcionar, y que a veces puede ser tan difícil de razonar como la alternativa concreta, incluso si deja más espacio para respirar. cambios Para realmente permitir que un sistema se amplíe sin abrumar a nuestros cerebros, la mejor manera que encontré beneficiosa es no hacer que los sistemas dependan de una gran cantidad de información abstracta, sino hacer que dependan de la menor cantidad de información posible de cualquier forma. mundo exterior.
Puede ser un ejercicio útil simplemente sentarse con una parte de un sistema y preguntar: "¿Cuánta información del mundo exterior debo comprender para implementar / cambiar esta cosa?"
En una base de código anterior en la que trabajé (no de mi diseño), el sistema de física anterior tenía que conocer las jerarquías de escenas, el sistema de propiedades, las interfaces de transformadores abstractos, las pilas de transformación, los fotogramas clave, los sobres, los sistemas de deshacer, la evaluación de gráficos nodales, las selecciones, la selección Modos, propiedades de todo el sistema (configuraciones, es decir), diferentes tipos de abstracciones de geometría, emisores de partículas, etc., y solo para comenzar a aplicar la gravedad a algo en la escena. Estas fueron todas dependencias abstractas, pero quizás a más de 50 diferentes interfaces abstractas no triviales en el sistema para comprender desde el mundo exterior antes de que podamos comenzar a comprender qué está haciendo el sistema de física.
Y, naturalmente, un sistema de física tiene una dependencia conceptual de algunos de estos conceptos, pero no de todos, y los que están allí no deberían tener que exponer demasiada información para que un sistema de física haga su trabajo. Ese sistema era muy difícil de mantener y, a menudo, requería que un desarrollador pasara un par de años en entrenamiento y estudio antes de que pudiera comenzar a aportar algo realmente sustancial, mientras que incluso los veteranos experimentados tenían dificultades para averiguar qué estaba pasando porque la cantidad Para acceder a la información central y modificarla, era necesario que estuvieran al tanto de lo que el sistema de física estaba haciendo hasta cierto punto, incluso si estaban trabajando en cosas que no tenían ninguna relación.
Así que creo que vale la pena dar un paso atrás y pensar en estas cosas en una forma muy humana: "¿Cuánto necesito saber para implementar / cambiar esta cosa en comparación con cuánto debo saber?" tipo de manera e intente minimizar la información si supera con creces el segundo pensamiento, y una de las formas de hacerlo es usar esta estrategia propuesta para evitar la acumulación de información en las instancias de objetos de la aplicación central. En general estoy obsesionado con los aspectos humanos, pero nunca he sido tan bueno en los aspectos técnicos. Tal vez debería haber entrado en psicología, aunque no ayuda que esté loco. :-RE
"Gerentes" vs. "Sistemas"
Ahora soy una persona terrible cuando se trata de una terminología técnica adecuada (de alguna manera, a pesar de haber diseñado una serie de arquitecturas, sigo siendo terrible para comunicar las cosas de una manera técnicamente precisa). Pero he notado una tendencia a que los "administradores" se usen a menudo con una mentalidad de "empuje" casi unánimemente por los desarrolladores que crean cualquier cosa a la que se refieren como tal. Solicitamos / impulsamos que los cambios se realicen de forma centralizada al decirles a estos "gerentes" qué hacer.
En su lugar, me ha resultado útil favorecer lo que quiero llamar "sistemas", ya que lo he tomado de arquitecturas populares en el desarrollo de juegos. Los sistemas "tiran" como yo quiero llamarlo. Nadie les dice específicamente qué hacer. En su lugar, lo resuelven por su cuenta accediendo al mundo, extrayendo datos para averiguar qué hacer y hacerlo. El mundo no accede a ellos. Recorren las cosas y hacen algo. Usted puede aplicar esta estrategia a muchos de sus "gerentes" para que el mundo entero no esté tratando de hablar con ellos.
Este tipo de mentalidad de "sistema" (disculpas si mi terminología es tan pobre y mis distinciones tan arbitrarias) me fue muy útil para liberarme de muchos "gerentes". Ahora solo tengo uno de esos "administradores" centrales, que es la base de datos de ECS, pero es solo una estructura de datos para el almacenamiento. Los sistemas simplemente los utilizan para recuperar componentes y entidades para procesar en sus bucles.
Orden de destruccion
Otra cosa que aborda este tipo de estrategia es el orden de destrucción, que puede ser bastante complicado para los sistemas complejos. Incluso cuando tiene un "objeto de aplicación" central que almacena a todos estos administradores y, por lo tanto, controla su orden de inicialización y destrucción, a veces puede ser fácil, especialmente en una arquitectura de complementos, encontrar algún caso oscuro donde, por ejemplo, un complemento haya registrado algo en el Sistema que, al cerrarse, quiere acceder a algo central después de que ya ha sido destruido. Esto tiende a suceder fácilmente si tiene capas sobre capas de abstracciones y eventos que ocurren durante el cierre.
Si bien los ejemplos reales que he visto son mucho más sutiles y variados que esto, un ejemplo muy básico que acabo de hacer para que podamos mantener el ejemplo en torno a los sistemas de deshacer es como un objeto en la escena que desea escribir un evento de deshacer cuando se destruye para que el usuario pueda "restaurarla" al deshacerla. Mientras tanto, ese tipo de objeto se registra a través de un complemento. Cuando se descarga ese complemento, todas las instancias que aún quedan del objeto se destruyen. Entonces podríamos encontrarnos con ese problema de orden de destrucción si el administrador de complementos descarga complementos después de que el sistema de deshacer ya haya sido destruido.
Puede que no sea un problema para algunas personas, pero mi equipo solía luchar mucho con esto en una antigua base de código con fallos de apagado oscuros (y las razones a menudo eran mucho más complejas que el ejemplo anterior), y esto hace que no sea un problema. -edición porque, por ejemplo, nada intentará hacer nada con el sistema de deshacer en el cierre, ya que nada se refiere al sistema de deshacer.
Sistemas de entidad-componente
Lo que he mostrado en algunos diagramas son sistemas de componente de entidad (el código de deshacer del sistema también implica uno) y eso podría ser una exageración absoluta en su caso. En mi caso, estoy lidiando con un software que es bastante grande con una arquitectura de complementos, scripts incrustados, programación visual para cosas como sistemas de partículas y sombreadores, etc.
Sin embargo, esta estrategia general de "inversión" para evitar depender de instancias del objeto central se puede aplicar sin tener que recurrir a un sistema de componentes de entidad en toda regla. Es una estrategia muy generalizada que puedes aplicar de muchas maneras diferentes.
Como ejemplo muy simple, puede crear una abstracción central para objetos de escena como ISceneObject
(o incluso un ABC) con un método de transaction
virtual. Luego, su sistema de deshacer puede recorrer los punteros de base polimórficos a ISceneObject*
en su escena y llamar a ese método de transaction
virtual para recuperar un objeto de transacción si hay uno disponible.
Y, por supuesto, puede aplicar esto más allá de los sistemas de deshacer. Puede aplicar esto a su administrador de configuración y así sucesivamente, aunque podría necesitar algo como un administrador de proyectos si esa es su "escena". Básicamente, necesita una cosa central para acceder a todos los objetos de interés de "escena" o "proyecto" en su software para aplicar esta estrategia.
Con singletons
No uso singletons en todos estos días (encontré diseños en los que ya ni siquiera es tentador usarlos), pero si realmente sientes la necesidad, entonces tendía a encontrarlo útil en los códigos anteriores para al menos ocultar los detalles sangrientos de Accediendo a ellos y minimizando dependencias innecesarias en tiempo de compilación.
Por ejemplo, en lugar de acceder al administrador de deshacer a través de la aplicación CADApplication
, puede hacer lo siguiente:
// In header:
// Returns the undo manager:
UndoManager& undoManager();
// Inside source file:
#include "CADApplication.h"
UndoManager& undoManager()
{
return *CADApplication::instance()->undoManager();
}
Esto puede parecer una diferencia superflua, pero puede al menos reducir ligeramente la cantidad de dependencias en tiempo de compilación que tiene, ya que una parte de su base de código podría necesitar acceso a su administrador de deshacer, pero no al objeto de aplicación en toda regla, por ejemplo, es más fácil de comenzar apagado dependiendo de muy poca información y amplíe si es necesario para comenzar dependiendo de demasiada información y reducir en retrospectiva.
Si cambia de opinión más adelante y desea alejarse de los singletons e inyectar las cosas adecuadas con el administrador de deshacer, por ejemplo, las inclusiones en el encabezado también hacen que sea más fácil descubrir qué necesita qué en lugar de todo, incluida la aplicación CADApplication.h
y accediendo a lo que necesite de eso.
Examen de la unidad
Según su experiencia, ¿cuál de estos enfoques debería seguir para buscar una buena arquitectura y hacer que las aplicaciones sean propensas a errores y simplificar las pruebas unitarias?
Para las pruebas unitarias en general, depender ampliamente de las instancias de objetos de un tipo no trivial (singletons o no) puede ser bastante difícil de manejar ya que es difícil razonar acerca de sus estados y si ha agotado completamente los casos límite para un determinado Interfaz que está probando (ya que el comportamiento de un objeto puede variar si los objetos centrales dependen del cambio de estado).
Si llama a una función y le pasa una amplia gama de parámetros y proporciona la salida correcta, es difícil estar seguro de que seguirá siendo así si sus singletons cambian de estado. Específicamente el problema es que si tenemos una función como esta:
func(a, b)
Entonces esto podría ser fácil de probar a fondo porque podemos razonar sobre qué combos de a
y b
conducen a diferentes casos para que func
. Si se convierte en un método, entonces eso introduce un parámetro invisible (self / this):
// * denotes invisible parameter
method(*self, a, b)
... pero eso aún puede ser bastante fácil de razonar si nuestro objeto no contiene demasiado estado que podría afectar el method
(sin embargo, incluso un objeto que no depende de otros puede ser difícil de probar si lo tiene, por ejemplo, 25 eclécticos). variables miembro). Y es un parámetro invisible pero no exactamente oculto. Se vuelve descaradamente obvio cuando se llama a foo.method(...)
que foo
es una entrada a la función. Pero si empezamos a inyectar muchos objetos complejos o si dependemos de globals / singletons, entonces podríamos tener todo tipo de parámetros genuinamente ocultos que ni siquiera sabríamos que están ahí a menos que rastreemos la implementación de la función / método:
method(*self, a, b, *c, *d, *e, *f, *g, *h, *i, *j)
... y eso puede ser realmente difícil de probar y razonar para ver si hemos agotado por completo todas las posibles combinaciones de entrada / estado que llevan a casos únicos a manejar. Es otra razón por la que recomendaría este enfoque de convertir a sus "administradores" en "sistemas" y evitar el uso de objetos en instancias de un solo objeto si su aplicación alcanza una escala suficientemente grande.
Ahora, el ECS que mostré arriba, que tiene un "Administrador de ECS" central y generalizado (aunque inyectado, no un singleton) puede parecer que no sería diferente, pero la diferencia es que el ECS no tiene un comportamiento complejo propio. . Es bastante trivial al menos en el sentido de no tener ningún caso de borde oscuro. Es una "base de datos generalizada". Almacena datos en forma de componentes que los sistemas leen como entrada y usan para generar algo.
Los sistemas solo lo utilizan para recuperar ciertos tipos de componentes y entidades que los contienen. Como resultado, generalmente no hay casos de borde oscuro con los que lidiar, por lo que a menudo puede probar todo simplemente generando los datos para los componentes que un sistema necesita procesar (por ejemplo, componentes de transacción si está probando su sistema de deshacer) para ver si funciona correctamente Sorprendentemente, he encontrado que el ECS es más fácil de probar sobre una base de código anterior (originalmente tuve dudas sobre el intento de usar ECS en un dominio de efectos visuales que, por lo que sé entre todos los grandes competidores, nunca se había intentado antes). La base de código anterior usaba algunos singletons pero en su mayoría todavía usaba DI para todo lo demás. Sin embargo, el ECS fue aún más fácil de probar, ya que lo único que debe hacer para probar la corrección de un sistema es crear algunos datos de componentes del tipo que le interesan como entrada (por ejemplo, componentes de audio para un sistema de audio) y asegurarse de que proporciona la Salida correcta después de procesarlos.
La organización de estilo de sistema de un ECS y la forma en que acceden a los componentes a través de la base de datos lo hacen explícitamente claro y descaradamente qué tipo de componentes procesan, por lo que es bastante fácil obtener una cobertura completa sin tropezar con algún caso de borde oscuro. La parte clave es que es claramente obvio a pesar de las dependencias de esta base de datos central. Es cuando no es tan obvio que se convierte en un punto de inflexión.
Pros y contras
Ahora en cuanto a si mis sugerencias son adecuadas para su caso, no puedo decir. Este enfoque que estoy sugiriendo definitivamente involucra un poco más de trabajo por adelantado, pero realmente puede valer la pena después de alcanzar una cierta escala. Pero podría ser una estrategia alternativa a considerar si desea desacoplar a estos gerentes del mundo entero mientras camina en el equilibrio de la cuerda floja al diseñar una arquitectura. Descubrí que fue de gran ayuda para el tipo de base de código y dominio en el que trabajo. Intentaré algunos pros y contras, aunque creo que incluso los pros y los contras nunca están totalmente divorciados de la subjetividad, pero trataré de ser tan objetivo como yo. puede:
Pros:
- Reduce el acoplamiento y minimiza la información que todo lo que tiene que tener sobre todo lo demás.
- Hace que los sistemas a gran escala sean más fáciles de razonar. Por ejemplo, resulta realmente fácil razonar cuándo y dónde se registran las operaciones de deshacer de la aplicación central cuando eso ocurre en un lugar central en un "sistema" de "deshacer" de componentes de transacción en la escena en lugar de tener cientos de lugares en el código base tratando de decir. el deshacer "manager" lo que hay que grabar. Los cambios en el estado de la aplicación son más fáciles de entender y los efectos secundarios se vuelven mucho más centralizados. Para mí, una prueba de fuego para la capacidad que tenemos para comprender un código base a gran escala no es solo comprender qué efectos secundarios generales deben ocurrir, sino cuándo y dónde ocurren. La parte "qué" puede ser mucho más fácil de comprender que "cuándo" y "dónde", y estar confundido acerca de "cuándo" y "dónde" es a menudo una receta para los efectos secundarios no deseados cuando los desarrolladores intentan realizar cambios. el "cuándo / dónde" es mucho más obvio, incluso si el "qué" sigue siendo el mismo.
- A menudo hace que sea más fácil lograr la seguridad del hilo sin sacrificar la eficiencia del hilo.
- Facilita el trabajo en equipo con menos escalones debido al desacoplamiento.
- Evita aquellos problemas con el orden de dependencia para la inicialización y destrucción.
- Simplifica las pruebas unitarias al evitar muchas dependencias en el estado de un objeto central, lo que podría llevar a casos poco claros que pueden pasarse por alto en las pruebas.
- Te da más espacio para realizar cambios de diseño sin interrupciones en cascada. Por ejemplo, imagine lo doloroso que sería cambiar su administrador de deshacer en uno almacenado localmente por proyecto si acumulara una base de código con un millón de líneas de código dependiendo de la central que se proporciona a través del singleton.
Contras:
- Definitivamente toma un poco más de trabajo por adelantado. Es una mentalidad de "inversión" para reducir los costos de mantenimiento futuros, pero el intercambio será costos más altos por adelantado (aunque no mucho más alto si me lo preguntas).
- Puede ser completamente excesivo para pequeñas aplicaciones. No me molestaría con esto por algo realmente pequeño. Para aplicaciones de escala suficientemente pequeña, realmente creo que hay un argumento pragmático a favor de singletons e incluso variables globales simples a veces, ya que esas aplicaciones pequeñas a menudo se benefician de la creación de su propio "entorno" global, como poder dibujar a la reproduzca desde cualquier lugar o reproduzca audio desde cualquier lugar para un pequeño videojuego que solo usa una "pantalla" (ventana) del mismo modo que podemos enviar a la consola desde cualquier lugar. Simplemente tiende a ser problemático a medida que comienza a moverse hacia aplicaciones de mediana y gran escala que desean quedarse por un tiempo y pasar por muchas actualizaciones, ya que es posible que deseen reemplazar las capacidades de reproducción o audio, pueden llegar a ser tan grandes que es es difícil decir dónde reside el error si ves artefactos de dibujo extraños, etc.
Dado un conjunto de clases, muy usado desde cualquier lugar en el código de la aplicación, recurriría a una solución de proxy a singleton. Por ejemplo, al ser uno de su administrador llamado Manager, le daría una clase de implementación privada y lo convertiría en un singleton:
class Status{ /* ... */ };
class ManagerPrivate
{
public:
static ManagerPrivate & instance();
void doThis();
void doThat();
void doSomethingElse();
// etc ...
Status status() const;
private:
Status _status;
};
Un proxy para este administrador, podría ser así:
class Manager
{
public:
Status doSomething()
{
ManagerPrivate::instance().doThis();
ManagerPrivate::instance().doThat();
return ManagerPrivate::instance().status();
}
//...
};
Así que tenemos un singleton con estado envuelto en un proxy sin estado, y aún podemos confiar en la herencia y tener una jerarquía de gerentes, todos envolviendo el mismo singleton:
class BaseManager
{
public:
virtual ~BaseManager() = default;
virtual Status doSomething() = 0;
};
class ManagerA : public BaseManager
{
public:
Status doSomething()
{
ManagerPrivate::instance().doThis();
ManagerPrivate::instance().doThat();
return ManagerPrivate::instance().status();
}
};
class ManagerB : public BaseManager
{
public:
Status doSomething()
{
ManagerPrivate::instance().doSomethingElse();
return ManagerPrivate::instance().status();
}
};
o una clase de fachada única que envuelve más de un singleton, y así sucesivamente.
De esta manera, siempre que se necesite un administrador, el usuario puede incluir su encabezado y usar nuevas instancias donde lo desee:
void someFunction()
{
//...
Status theManagerStatus = ManagerX().doSomething();
//...
}
La inversión de control sigue siendo una característica factible:
BaseManager * theManagerToUse()
{
if(configuration == A)
{
return new ManagerA();
}
else if(configuration == B)
{
return new ManagerB();
}
// etc ...
}
Realmente no veo un beneficio al ser un boy scout, con demasiada frecuencia las personas se lanzan a las prácticas recomendadas que no tienen ningún mérito y solo introducen la caldera y los gastos generales. Claro, algunas personas pueden considerarlo genial, pero en realidad es un esfuerzo inútil y en realidad hace que el código sea menos fácil de mantener.
Tener a los gerentes como miembros de la aplicación como sugiere Kuba Ober es perfectamente adecuado. Y dado que con Qt tiene una sola aplicación a la que puede acceder en cualquier momento, las implementaciones como singletons independientes son completamente redundantes. También significa que no tiene que lidiar con ninguna construcción manual, eliminación o inicialización. Simplemente funciona.
Sin embargo, lo que me interesa es ¿está seguro de que desea que el administrador de deshacer se implemente en el nivel de la aplicación? Eso no parece ser una buena idea, y será problemático en muchos casos. Por ejemplo, si está trabajando simultáneamente en varios proyectos, no podrá volver a un proyecto sin afectar al otro. Además, ¿qué sucede con los comandos de un proyecto que cierra pero continúa trabajando en otro?
En mi opinión, cada proyecto debe tener su propio administrador de comandos.
Más o menos lo mismo se aplica a la configuración del proyecto, aunque es más una diferencia conceptual que práctica. No invoca un cuadro de diálogo de configuración "maestro" y lo dirige al proyecto actual, invoca un cuadro de diálogo de configuración para un proyecto en particular, independientemente de cuál sea. De esta manera, puede tener dos diálogos de configuración uno al lado del otro, por ejemplo, para comparar configuraciones. Siempre que su implementación de "vista múltiple" sea lo suficientemente flexible como para permitir eso.