smart - Cadenas largas de delegación en C++
smart contracts ethereum (8)
En mi experiencia, las cadenas como esa a menudo contienen captadores que son menos que triviales, lo que lleva a ineficiencias. Creo que (1) es un enfoque razonable. Usar objetos proxy parece una exageración. Preferiría ver un bloqueo en un puntero NULL en lugar de usar un objeto proxy.
Esto es definitivamente subjetivo, pero me gustaría tratar de evitar que se vuelva argumentativo. Creo que podría ser una pregunta interesante si la gente lo trata adecuadamente.
En mis varios proyectos recientes solía implementar arquitecturas donde las cadenas largas de delegación son una cosa común.
Cadenas de doble delegación pueden encontrarse muy a menudo:
bool Exists = Env->FileSystem->FileExists( "foo.txt" );
Y la triple delegación no es rara en absoluto:
Env->Renderer->GetCanvas()->TextStr( ... );
Existen cadenas de delegación de orden superior pero son realmente escasas.
En los ejemplos mencionados anteriormente, no se realizan comprobaciones de tiempo de ejecución NULL ya que los objetos utilizados siempre están ahí y son vitales para el funcionamiento del programa y se construyen 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 revisarse en NULL antes de usarlo. P.ej.
clCanvas* C = Env->Renderer->GetCanvas();
if ( C ) C->TextStr( ... );
Solía luchar contra el caso (2) proporcionando objetos proxy para que se pueda invocar un método en un objeto que no sea NULL, lo que lleva a un resultado empty
.
Mis preguntas son:
- ¿Es alguno de los casos (1) o (2) un patrón o un antipatrón?
- ¿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 una línea de código de dónde vino el objeto
- largas cadenas de delegación se ven bien
Contras:
- la depuración interactiva se realiza debido a que es difícil inspeccionar más de un objeto temporal en la cadena de delegación
Me gustaría saber otros pros y contras de las largas cadenas de delegación. Por favor, presente su razonamiento y voto basándose en qué tan bien argumentada es la opinión y no en qué tan bien está de acuerdo con ella.
Las largas cadenas de delegaciones son para mí un olor a diseño.
Lo que me dice una cadena de delegación es que una pieza de código tiene un acceso profundo a una pieza de código no relacionada, lo que me hace pensar en un alto acoplamiento , que va en contra de los principios de diseño de SOLID .
El principal problema que tengo con esto es la mantenibilidad. Si está llegando a dos niveles de profundidad, son dos piezas de código independientes que podrían evolucionar por sí solas y romperse debajo de usted. Esto se agrava rápidamente cuando tiene funciones dentro de la cadena, ya que pueden contener cadenas propias, por ejemplo, Renderer->GetCanvas()
podría estar eligiendo el lienzo basándose en información de otra jerarquía de objetos y es difícil hacer cumplir un código Ruta que no termina llegando a lo profundo de los objetos durante el tiempo de vida del código base.
La mejor manera sería crear una arquitectura que obedezca los principios de SOLID y utilice técnicas como la inyección de dependencia y la 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 a pruebas automatizadas y por unidad.
Sólo mis 2 centavos.
No iría tan lejos para llamar a un anti-patrón. Sin embargo, el primero 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 alcance adecuado ( C
tiene el alcance como si estuviera dentro del bloque del condicional) y verifique si hay NULL.
Afirmar que algo no es NULL es mejor que ser asesinado por un SegFault. Por lo tanto, no recomendaría simplemente saltarse estas comprobaciones, a menos que esté 100% seguro de que ese puntero nunca puede ser NULL.
Además, puede encapsular sus cheques en una función gratuita adicional, si se siente particularmente elegante:
template <typename T>
T notNULL(T value) {
assert(value);
return value;
}
// e.g.
notNULL(notNULL(Env)->Renderer->GetCanvas())->TextStr();
Para bool Exists = Env->FileSystem->FileExists( "foo.txt" );
Prefiero ir a un desglose aún más detallado de su cadena, así que en mi mundo ideal, están 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:
- legibilidad : mi atención se pierde hasta que tengo que leer hasta el final de la línea en caso de que
bool Exists = Env->FileSystem->FileExists( "foo.txt" );
Es demasiado largo para mí. - validez : no importa si mencionó que son los objetos, si su compañía mañana contrata a un nuevo programador y él comienza a escribir el código, pasado mañana los objetos podrían no estar allí. Estas largas colas son bastante hostiles, la gente nueva podría asustarse con ellos y hará algo interesante, como optimizarlos ... lo que llevará más tiempo al programador con más experiencia para solucionarlo.
- 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 cadenas, es bastante difícil averiguar cuál es el objeto culpable. Cuanto más detallado sea el desglose, más fácil será encontrar la ubicación del error.
- velocidad : 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 una función getter "adecuada" para ella. No sé si su código es de producción o no, pero parece que pierde la función getter "adecuada", en cambio parece que usa solo el atributo.
Pregunta interesante, creo que esto está abierto a 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 harán comenzar con interfaces, cadenas de herencia y / o relaciones de contención que resultarán en que use el encadenamiento para llamar cosas en cierta medida. Sin embargo, los patrones no intentan resolver un problema de programación como este: el encadenamiento es solo un efecto secundario de que resuelvan los problemas funcionales en cuestión. Entonces, realmente no lo consideraría un patrón.
Igualmente, los antipatrones son enfoques que (en mi opinión) actúan en contra del 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 que un singleton es un anti-patrón porque (a menudo, no siempre) da como resultado un código similar a una tela de araña debido al hecho de que crea un global, y cuando tiene muchos, su diseño se deteriora rápidamente.
Entonces, una vez más, su problema de encadenamiento no indica necesariamente un diseño bueno o malo, no está relacionado con los objetivos funcionales de los patrones o los inconvenientes de los patrones. Algunos diseños solo tienen muchos objetos anidados incluso cuando están bien diseñados.
Qué hacer al respecto:
Definitivamente, las cadenas largas de delegación pueden ser un problema en el trasero después de un tiempo, y mientras su diseño exija que los punteros en esas cadenas no se reasignen, creo que guardar un puntero temporal al punto de la cadena que le interesa Está completamente bien (función alcance o menos preferiblemente).
Sin embargo, en lo personal, estoy en contra de guardar un puntero permanente en una parte de la cadena como miembro de una clase, como he visto que termina en personas que tienen 30 punteros a sub objetos almacenados permanentemente, y se pierde toda idea de cómo son los objetos. Presentado en el patrón o arquitectura con el que estás trabajando.
Otro pensamiento: no estoy seguro de si esto me gusta 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 los problemas sobre si o no su puntero cambia bajo las cubiertas, o si tiene o no nulos. Puede ser bueno envolver toda esa lógica una vez, poner un comentario agradable en la parte superior de la función que indique de qué parte de la cadena obtiene el puntero y luego simplemente use el resultado de la función directamente en su código en lugar de usar su delegación Cadena cada vez.
Actuación
Mi última nota sería que este enfoque envolvente en función, así como el enfoque de su cadena de delegación, sufren de inconvenientes de rendimiento. Guardar un puntero temporal le permite evitar las dos referencias extra potencialmente muchas veces si está utilizando estos objetos en un bucle. Igualmente, el almacenamiento del puntero de la llamada de función evitará la sobrecarga de una llamada de función adicional en cada ciclo de bucle.
Si es posible usaría referencias en lugar de punteros. Así que los delegados tienen la garantía de devolver objetos válidos o lanzar excepciones.
clCanvas & C = Env.Renderer().GetCanvas();
Para los objetos que no pueden existir, proporcionaré métodos adicionales como has, is, etc.
if ( Env.HasRenderer() ) clCanvas* C = Env.Renderer().GetCanvas();
Si puedes garantizar que todos los objetos existen, realmente no veo un problema en lo que estás haciendo. Como han mencionado otros, incluso si usted piensa 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. Cuando usa el operador ->, un puntero inteligente generalmente se lanzará si el puntero es NULL. Así que evitas un SegFault. No solo eso, si utiliza punteros inteligentes, puede guardar copias y los objetos no desaparecen bajo sus pies. Debe restablecer explícitamente cada puntero inteligente antes de que el puntero pase a NULL.
Dicho esto, no evitaría que el operador -> lance de vez en cuando.
De lo contrario, preferiría 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 está haciendo el objeto A es probablemente algo que el objeto B debería estar haciendo. Entonces, A puede garantizar que tiene un puntero a B en todo momento (porque mantiene un puntero compartido a B y, por lo tanto, no puede pasar a NULL) y, por lo tanto, siempre puede llamar a una función en B para realizar 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!
Una cadena de delegación tan larga no debería ocurrir si sigues la Ley de Demeter . A menudo he discutido con algunos de sus partidarios de que ellos se aferraron a ellos con demasiada conciencia, pero si llega al punto de preguntarse cuál es la mejor manera de manejar largas cadenas de delegación, probablemente debería cumplir un poco más con sus recomendaciones.