Largas cadenas de delegación en C++


Esto es definitivamente subjetivo, pero me gustaría tratar de evitarlo se vuelve argumentativo. Creo que podría ser una pregunta interesante si la gente lo trata apropiadamente.

En mis varios proyectos recientes solía implementar arquitecturas donde las largas cadenas de delegación son algo común.

Las cadenas de delegación duales se pueden encontrar muy a menudo:

bool Exists = Env->FileSystem->FileExists( "foo.txt" );

Y la delegación triple no es rara en absoluto:

Env->Renderer->GetCanvas()->TextStr( ... );

Existen cadenas de delegación de orden superior pero son realmente escasos.

En los ejemplos mencionados anteriormente no se realizan comprobaciones NULAS de tiempo de ejecución, ya que los objetos utilizados siempre están allí y son vitales para el funcionamiento del programa y construido explícitamente cuando se inicia la ejecución. Básicamente solía dividir una cadena de delegación en estos casos:

1) Reutilizo el objeto obtenido a través de una cadena de delegación:

{ // make C invisible to the parent scope
   clCanvas* C = Env->Renderer->GetCanvas();
   C->TextStr( ... );
   C->TextStr( ... );
   C->TextStr( ... );
}

2) Un objeto intermedio en algún lugar en el medio de la cadena de delegación debe ser comprobado para NULL antes de uso. Eg.

clCanvas* C = Env->Renderer->GetCanvas();

if ( C ) C->TextStr( ... );

Solía luchar contra el caso (2) proporcionando objetos proxy para que un método pueda ser invocado en un objeto no NULO que conduce a un resultado empty.

Mis preguntas son:

  1. ¿Es uno de los casos (1) o (2) un patrón o un antipatrón?
  2. ¿Hay una mejor manera de lidiar con largas cadenas de delegación en C++?

Aquí hay algunos pros y contras que consideré al hacer mi elección:

Pros:

  • es muy descriptivo: está claro de 1 línea de código de dónde vino el objeto
  • las largas cadenas de delegación se ven bien

Contras:

  • la depuración interactiva es complicada ya que es difícil inspeccionar más de un objeto temporal en la cadena de delegación

Me gustaría conocer otros pros y contras de las largas cadenas de delegación. Por favor, presente su razonamiento y basado en votos sobre lo bien argumentado que es la opinión y no lo bien que está de acuerdo con ella.

Author: Sergey K., 2012-07-24

8 answers

Yo no iría tan lejos para llamar a cualquiera de los dos un anti-patrón. Sin embargo, la primera tiene la desventaja de que su variable C es visible incluso después de que es lógicamente relevante (alcance demasiado gratuito).

Puedes evitar esto usando esta sintaxis:

if (clCanvas* C = Env->Renderer->GetCanvas()) {
  C->TextStr( ... );
  /* some more things with C */
}

Esto está permitido en C++ (mientras que no está en C) y le permite mantener el ámbito adecuado (C es de ámbito como si estuviera dentro del bloque del condicional) y comprobar si hay NULL.

Afirmar que algo no es NULO, es por todos significa mejor que ser asesinado por un SegFault. Así que no recomendaría simplemente saltarse estas comprobaciones, a menos que esté 100% seguro de que ese puntero nunca puede ser NULO.


Además, podrías encapsular tus cheques en una función extra free, si te sientes particularmente dandy:

template <typename T>
T notNULL(T value) {
  assert(value);
  return value;
}

// e.g.
notNULL(notNULL(Env)->Renderer->GetCanvas())->TextStr();
 14
Author: bitmask,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-07-24 13:22:52

En mi experiencia, cadenas como esa a menudo contienen captadores que son menos que triviales, lo que conduce a ineficiencias. Creo que (1) es un enfoque razonable. El uso de objetos proxy parece una exageración. Preferiría ver un bloqueo en un puntero NULO en lugar de usar objetos proxy.

 6
Author: wilx,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-07-24 13:11:26

Esta larga cadena de delegación no debería suceder si se sigue la Ley de Deméter. A menudo he argumentado con algunos de sus defensores que se aferran a ella demasiado concienzudamente, pero si llegas al punto de preguntarte cómo manejar mejor las largas cadenas de delegación, probablemente deberías ser un poco más obediente con sus recomendaciones.

 6
Author: AProgrammer,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-08-25 17:39:22

Pregunta interesante, creo que esto está abierto a la interpretación, pero:

Mis Dos Centavos

Los patrones de diseño son solo soluciones reutilizables a problemas comunes que son lo suficientemente genéricos como para ser aplicados ampliamente en la programación orientada a objetos (generalmente). Muchos patrones comunes lo iniciarán con interfaces, cadenas de herencia y/o relaciones de contención que resultarán en que use el encadenamiento para llamar a las cosas hasta cierto punto. Los patrones no están tratando de resolver un problema de programación como este, sin embargo, el encadenamiento es solo un efecto secundario de ellos resolviendo los problemas funcionales a mano. Así que, realmente no lo consideraría un patrón.

Igualmente, los anti-patrones son enfoques que (en mi mente) contrarrestan el propósito de los patrones de diseño. Por ejemplo, los patrones de diseño tienen que ver con la estructura y la adaptabilidad de su código. La gente considera un singleton un anti-patrón, ya que (a menudo, no siempre) resulta en la tela de araña como el código debido al hecho de que inherentemente crea un global, y cuando tienes muchos, tu diseño se deteriora rápidamente.

Así que, nuevamente, su problema de encadenamiento no necesariamente indica un diseño bueno o malo, no está relacionado con los objetivos funcionales de los patrones o los inconvenientes de los anti - patrones. Algunos diseños solo tienen muchos objetos anidados, incluso cuando están bien diseñados.


¿Qué hacer al respecto:

Las largas cadenas de delegación definitivamente pueden ser un dolor en el trasero después de un tiempo, y siempre y cuando su el diseño dicta que los punteros en esas cadenas no se reasignarán, creo que guardar un puntero temporal al punto de la cadena que le interesa está completamente bien (ámbito de función o menos preferiblemente).

Personalmente, sin embargo, estoy en contra de guardar un puntero permanente a una parte de la cadena como miembro de la clase, ya que he visto que terminan en personas que tienen 30 punteros a sub objetos almacenados permanentemente, y se pierde toda concepción de cómo se presentan los objetos en el patrón o arquitectura con la que estás trabajando.

Otro pensamiento - No estoy seguro de si me gusta esto o no, pero he visto a algunas personas crear una función privada (para su cordura) que navega por la cadena para que pueda recordar eso y no tratar con problemas sobre si su puntero cambia o no bajo las cubiertas, o si tiene o no nulos. Puede ser bueno envolver toda esa lógica una vez, poner un buen comentario en la parte superior de la función indicando de qué parte de la cadena obtiene el puntero, y luego simplemente use la función resultado directamente en su código en lugar de usar su cadena de delegación cada vez.

Rendimiento

Mi última nota sería que este enfoque de wrap-in-function, así como su enfoque de cadena de delegación, sufren inconvenientes de rendimiento. Guardar un puntero temporal le permite evitar las dos desreferencias adicionales potencialmente muchas veces si está utilizando estos objetos en un bucle. Igualmente, almacenar el puntero de la llamada a la función evitará la sobrecarga de una función extra llamar a cada ciclo de bucle.

 4
Author: John Humphreys - w00te,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-07-24 13:24:58

Para bool Exists = Env->FileSystem->FileExists( "foo.txt" ); Prefiero un desglose aún más detallado de su cadena, por lo que en mi mundo ideal, hay las siguientes líneas de código:

Environment* env = GetEnv();
FileSystem* fs = env->FileSystem;
bool exists = fs->FileExists( "foo.txt" );

Y por qué? Algunas razones:

  1. legibilidad : mi atención se pierde hasta que tengo que leer hasta el final de la línea en el caso de bool Exists = Env->FileSystem->FileExists( "foo.txt" ); Es demasiado largo para mí.
  2. validez : las consideraciones que usted mencionó los objetos son, si su empresa mañana contrata a un nuevo programador y comienza a escribir código, el día después de mañana los objetos podrían no estar allí. Estas largas colas son bastante antipáticas, las personas nuevas pueden asustarse de ellas y harán algo interesante, como optimizarlas... que tomará más tiempo adicional programador experimentado para arreglar.
  3. depuración: si por casualidad (y después de haber contratado al nuevo programador) la aplicación lanza un fallo de segmentación en la larga lista de cadena es bastante difícil averiguar qué objeto era el culpable. El más detallado el desglose más fácil de encontrar la ubicación del error.
  4. speed: si necesita hacer muchas llamadas para obtener los mismos elementos de la cadena, podría ser más rápido "extraer" una variable local de la cadena en lugar de llamar a una función getter "adecuada" para ella. No se si su código es de producción o no, pero parece perder la función getter "apropiada", en su lugar parece usar solo el atributo.
 3
Author: fritzone,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-08-27 11:44:19

Las largas cadenas de delegación son un poco un olor a diseño para mí.

Lo que me dice una cadena de delegación es que una pieza de código tiene acceso profundo a una pieza de código no relacionada, lo que me hace pensar en acoplamiento alto, que va en contra de los principios de diseño SÓLIDOS.

El principal problema que tengo con esto es la mantenibilidad. Si usted está alcanzando dos niveles de profundidad, que son dos piezas independientes de código que podrían evolucionar por su cuenta y romper debajo de usted. Así de rápido compuestos cuando tiene funciones dentro de la cadena, porque pueden contener cadenas propias - por ejemplo, Renderer->GetCanvas() podría estar eligiendo el lienzo basado en información de otra jerarquía de objetos y es difícil imponer una ruta de código que no termina llegando profundamente a los objetos durante la vida útil del código base.

La mejor manera sería crear una arquitectura que obedeciera los principios SÓLIDOS y utilizara técnicas como Inyección de dependencias y Inversión de Control para garantizar que sus objetos siempre tengan acceso a lo que necesitan para realizar sus tareas. Este enfoque también se presta bien a las pruebas automatizadas y unitarias.

Solo mis 2 centavos.

 3
Author: Carl,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-08-31 00:07:41

Si es posible, usaría referencias en lugar de punteros. Por lo tanto, los delegados tienen la garantía de devolver objetos válidos o lanzar excepción.

clCanvas & C = Env.Renderer().GetCanvas();

Para objetos que no pueden existir proporcionaré métodos adicionales como has, is, etc.

if ( Env.HasRenderer() ) clCanvas* C = Env.Renderer().GetCanvas();
 2
Author: Torsten,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-07-24 21:35:54

Si puedes garantizar que todos los objetos existen, realmente no veo un problema en lo que estás haciendo. Como otros han mencionado, incluso si piensas que NULL nunca sucederá, puede suceder de todos modos.

Dicho esto, veo que usas punteros desnudos en todas partes. Lo que sugeriría es que empieces a usar punteros inteligentes en su lugar. Cuando se utiliza el operador ->, un puntero inteligente generalmente lanzará si el puntero es NULO. Así que evitas un SegFault. No solo eso, si utilizas smart punteros, puedes guardar copias y los objetos no desaparecen bajo tus pies. Debe restablecer explícitamente cada puntero inteligente antes de que el puntero pase a NULL.

Dicho esto, no evitaría que el operador -> lanzara de vez en cuando.

De lo contrario, prefiero utilizar el enfoque propuesto por AProgrammer. Si el objeto A necesita un puntero al objeto C apuntado por el objeto B, entonces el trabajo que el objeto A está haciendo es probablemente algo que el objeto B debería estar haciendo. Así que A puede garantizar que tiene un puntero a B en todo momento (porque tiene un puntero compartido a B y por lo tanto no puede ir a NULL) y por lo tanto siempre puede llamar a una función en B para hacer la acción Z en el objeto C. En la función Z, B sabe si siempre tiene un puntero a C o no. Eso es parte de la implementación de su B.

Tenga en cuenta que con C++11 tiene std::smart_ptr, ¡así que úselo!

 1
Author: Alexis Wilke,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-08-30 23:56:55