Ocultación de información v.s. Seguridad de tipo estático

7

Estoy trabajando en un proyecto con otros y tenemos una discusión sobre la ocultación de la información y la seguridad de tipo estático. Nuestro escenario se describe a continuación.

Idioma : C ++ 11

Escenario : queremos crear una estructura similar a un árbol. Cada nodo de árbol tiene su propia clase, que son todos los subtipos de la clase base NodeType . Cada subtipo tiene su propia regla para vincular otros nodos. Por ejemplo, NodeTypeA , NodeTypeB y NodeTypeC son subclases de NodeType , y

  • NodeTypeA solo puede tener NodeTypeB como su primer hijo, NodeTypeA como su segundo hijo.
  • NodeTypeB solo puede tener NodeTypeB como su childron. (Puede tener cualquier cantidad de childron)
  • NodeTypeC solo puede tener NodeTypeA como su primer hijo, NodeTypeB como su segundo hijo.

El ejemplo puede tener un problema de recursión, pero para ilustrar está bien.

Ahora hay una clase Factory para crear cada nodo:

  • createNodeTypeA
  • createNodeTypeB
  • createNodeTypeC

Hay una clase, Builder , que quiere convertir la entrada de texto del usuario al árbol.

Builder no necesita conocer información sobre el tipo de cada nodo. Solo lleva los punteros obtenidos de Factory y los pasa a otro método de Factory . En la perspectiva de Builder , todo tipo de nodo puede ser la clase base NodeType .

Dilema :

Si valoramos Ocultar información más, el método de Factory debería ser:

  • NodeType *createNodeTypeA(NodeType *first, NodeType *second)
  • NodeType *createNodeTypeB(std::vector<NodeType *> children)
  • NodeType *createNodeTypeC(NodeType *first, NodeType *second)

y para proporcionar corrección, debemos proporcionar una verificación en tiempo de ejecución de cada parámetro en el método de Factory.

Si valoramos Seguridad de tipo estático más, el método de Factory debería ser:

  • NodeTypeA *createNodeTypeA(NodeTypeB *first, NodeTypeA *second)
  • NodeTypeB *createNodeTypeB(std::vector<NodeTypeB *> childron)
  • NodeTypeC *createNodeTypeC(NodeTypeA *first, NodeTypeB *second)

y Builder deben conocer los subtipos de cada nodo. Lo que ganamos es seguridad de tipo.

La siguiente figura ilustra las dos formas:

Discusión:

FavorecerOcultarinformación:

  • Lainformaciónqueseocultaesimportante.LosdesarrolladoresdeBuilderpuedentrabajarsinelconocimientodelossubtiposdenodos.
  • Podemoscompensarlapérdidadeseguridaddetipoestáticomediantelarevisióndecódigo,lapruebaylaherramientadeanálisisautomático.Seesperaqueunequipocalificadoloscompletebastantebien.

FavorecerSeguridaddetipoestático:

Pregunta :

¿Cuál puede ser una buena práctica? Valoramos más el impacto a largo plazo.

    
pregunta Leo Jacob 13.10.2018 - 22:46

5 respuestas

4
NodeType *createNodeTypeA(NodeType *first, NodeType *second)
NodeType *createNodeTypeB(std::vector<NodeType *> children)
NodeType *createNodeTypeC(NodeType *first, NodeType *second)

Esto no "oculta la información", el Constructor aún sabe a qué fábrica llama y a qué datos debe pasarle. Por lo tanto, el uso de tipo dinámico no parece dar ningún beneficio en este caso

¿Qué se podría hacer para que el Generador no sea un tipo de nodo, puedes usar una sola fábrica?

NodeType *createNode(std::vector<NodeType*> children)

Pero esto no permite especificar el tipo de nodo. Este suele ser el caso de la construcción y, en general, plantea una pregunta: en principio, es posible eliminar la información de tipo de nodo del Generador. Tal vez el desacoplamiento debería estar más bien en el lado de preparación / preprocesamiento.

    
respondido por el max630 14.10.2018 - 08:25
4
  

Favorecer la ocultación de información:

     
  • La información que se oculta es importante. Los desarrolladores de Builder pueden trabajar   sin conocimiento de subtipos de nodos.
  •   

Esto es incorrecto, porque los argumentos pasados a los métodos de fábrica deben ser de cierto tipo (tiempo de ejecución). Por lo tanto, su programa debe ser consciente de los subtipos al construir el árbol, independientemente de si las relaciones se verifican en el momento de la compilación o en el tiempo de ejecución.

  
  • Podemos compensar la pérdida de   Seguridad de tipo estático mediante revisión de código, pruebas y análisis automático.   herramienta. Se espera que un equipo calificado los complete bastante bien.
  •   

O puedes ahorrarte mucho trabajo dejando que el compilador haga su trabajo.

El problema real parece ser que su interfaz está mal diseñada. Los diferentes subtipos requieren el conocimiento mutuo, por lo que no son independientes ni intercambiables. Pregúntese cuánta extensibilidad realmente ofrece su diseño y cuánto necesita realmente (si no puede encontrar un caso de uso que pueda ocurrir de manera realista en un futuro cercano, no lo necesita).

Sospecho que la causa de este fallo de diseño es el hecho de que está representando de forma intrusiva la estructura de datos de su árbol, es decir, que almacena la relación entre los nodos como datos de los nodos mismos. La única distinción entre los subtipos que usted menciona son las diferentes relaciones que tienen con otros nodos. Por lo tanto, si separa estas relaciones de la representación de sus nodos, puede terminar sin la necesidad de subtipos de nodos o, al menos, ser capaz de proporcionar una mejor abstracción.

La dificultad de tal cambio es que la funcionalidad que utiliza las relaciones entre nodos debe reformularse como un algoritmo de gráfico (búsqueda). Esto a menudo requiere un cambio de mentalidad. Usar la Biblioteca de gráficos de Boost le ayudará con esto y le proporcionará una estructura de datos y los algoritmos de una manera que facilita la separación de la representación de árbol, la representación de datos de nodo / borde y los algoritmos.

    
respondido por el D Drmmr 14.10.2018 - 15:16
1

Creo que se habría apreciado un poco más de contexto :).

Déjame reformular tu dilema:

  • Desea poder ver un árbol de la manera más abstracta posible. Desea poder decir: "Un árbol es solo una colección no vacía de Nodos que tienen uno y solo un padre (excepto el nodo raíz) que puede hacer lo que un árbol general puede hacer".
  • Desea poder verificar la invalidez del árbol rápidamente.

Un algoritmo que construye el árbol debe ser consciente de los diferentes tipos de nodos y construir un árbol válido. Si al algoritmo se le proporciona un flujo de nodos, debería poder indicar, después de verificar, qué nodo debe ir a dónde. Si ve una entrada no válida o se ve obligado a crear un árbol no válido, simplemente detiene e imprime el error.

Por lo tanto, defina un Tree que contenga métodos solo que tomen un NodeType como parámetro, como findChildren , getRoot , getSize , getDepth , etc. Puede llama a tu Parser (algoritmo) después de darle un método buildTree que devuelve un NodeType (que es equivalente a un árbol!). Decidiremos si un nodo es NodeTypeA , NodeTypeB y NodeTypeC al observar la carga útil de NodeType que puede contener una enumeración como TypeA , TypeB , etc. Toda la lógica de que Distingue entre los tipos de nodo que estarán en el Parser !

En conclusión, usamos algo llamado Parser (o Algoritmo) que es consciente de los diferentes tipos de nodos para separar las preocupaciones de la Tree abstracción que puede hacer cosas muy generales en el Árbol.

    
respondido por el Harsh Verma 15.10.2018 - 05:43
1

Es una comparación defectuosa

"Ocultar información" y "escribir datos estáticos" no son conceptos opuestos y no se excluyen mutuamente de manera absoluta ni relativa.

Esta es una falsa dicotomía que se aleja del verdadero problema que @DDmmr contesta: design . Luego, un comentario relacionado da una pista clave: La estructura de árbol es una sintaxis abstracta Árbol (AST)] de un script simple

Al diseño le falta una gramática para el "script simple"

  

Hay una clase, Builder, que quiere convertir la entrada de texto del usuario al árbol.

Sugiero que esta declaración de diseño combine el análisis con la construcción del objeto que conduce directamente a la pregunta del OP. El análisis requiere una estructura definida del "script simple". Esta definición se llama gramática.

Una gramática describe los bits fundamentales del "script simple", así como las combinaciones permitidas y las combinaciones de combinaciones, y así sucesivamente. Inevitablemente, ciertos bits (complejos) se asignarán directamente como Node s y subtipos de nodo (y propiedades potencialmente relacionadas).

Es importante destacar que todos estos bits distintos, desde los fundamentales hasta los más complejos, tienen nombres, por ejemplo, "número", "palabra clave", "dígito", "letra", "operador", etc.

Separación de inquietudes

Los "nodos con nombre" de AST son metadatos, una abstracción de objetos Node concretos que aún no se han construido. Estos metadatos son la "costura" que separa el AST de la implementación del árbol Node .

El diseño es entonces tres piezas generales: una gramática definida, un análisis y una construcción concreta del objeto Node .

Diseño de implementación de nodo

La salida del análisis es el AST. Pero el árbol eventual Node se deriva del AST, no es el AST. Pero ciertamente puedo ver la confusión. Esto se siente como una abstracción artificial pero es un punto de diseño importante.

Ahora el Constructor tomará el AST. Tenga en cuenta que las reglas de relación Node ya están integradas en la estructura de AST. Por lo tanto, el Generador construye un árbol Node basado únicamente en la estructura AST sin el conocimiento explícito de estas reglas.

Supongo que el "script simple" no es un lenguaje de scripting de propósito general completo, por lo que el análisis es realmente simple, pero no permitas que esto te lleve a violar la responsabilidad única a.k.a. Separación de preocupaciones.

Quizás el AST no necesita ser analizado completamente en elementos / nodos de partículas fundamentales del script. Tal vez hay NodeMetaData objetos, digamos, en el AST que corresponden a Node de propiedades de clase superior.

El dilema de OP está diseñado de forma remota

"Ocultar información" es inherentemente apropiado, dada la separación de preocupaciones y un Builder cuidadosamente diseñado. Los parámetros de tipo estático dan paso a un objeto AST con metadatos bien definidos.

Builder y las fábricas no toman Node ni subtipo objetos como argumentos. Builder controla el paso a través de AST y las fábricas se crean instancias y se llaman en función de los metadatos de AST. El generador ensambla el NodeTree a partir de partes creadas de fábrica NodeTree .

La construcción del árbol de nodos es potencialmente compleja, por lo que se deben estudiar patrones de diseño de construcción como "Generador" y "Fábrica". Tenga en cuenta que el patrón de fábrica varía según la complejidad.

    
respondido por el radarbob 19.10.2018 - 03:09
0

La ocultación de información es un beneficio externo. Dada la clase A, A tiene cero beneficio en ocultar su información. No ayuda a la clase a hacer su trabajo y no ayuda al desarrollador a razonar sobre el código. Ayuda a los desarrolladores externos que no tienen que estar expuestos a conocimientos inútiles. Ocultar información es un favor que su clase hace para otras clases.

La comprobación de tipos estáticos es un beneficio para su clase y los desarrolladores de la clase. Beneficia a la clase porque necesita hacer menos validación. Da la clase A y el método Foo, que toma un número entero y devuelve un decimal. El método Foo no tiene que verificar para ver si el entero nulo o una matriz u otra cosa, esa comprobación se realiza en tiempo de compilación o en tiempo de ejecución en otro lugar dentro de ese método, será un entero. Si Foo es privado devolverá un decimal. Esto es un beneficio para sus desarrolladores.

Ahora, no puedo decir si realmente necesita hacer este intercambio, pero se puede prescindir de la ocultación de la información. Los negativos son externos y, por lo tanto, no son directamente su problema.

Actualización en respuesta a un comentario: imagine que tiene una implementación de procedimiento de un algoritmo de clasificación, utiliza varias variables globales y un par de funciones para devolver resultados intermedios. Es absolutamente sólido, rápido, eficiente, con una gran O grande. No es el mejor objetivo para reescribir utilizando un diseño orientado a objetos, pero está haciendo el resto de la aplicación. Si simplemente envuelve esto en una clase, sigue siendo una implementación sólida, rápida, eficiente y sin errores. Ocultar los elementos globales (ahora los campos o propiedades de nivel de clase) y los métodos privados no lo hace más rápido ni elimina ningún error dentro de la clase. Lo que hace es evitar que alguien más necesite aprender los detalles de cómo funciona internamente para poder usarlo de manera segura. Cero beneficio para nuestra clase de clasificación hipotética, gran ganancia para nuestra aplicación.

    
respondido por el jmoreno 16.10.2018 - 01:08

Lea otras preguntas en las etiquetas