¿Cuál es la mejor práctica cuando se trata de escribir clases que puedan tener que saber sobre la interfaz de usuario? ¿Una clase que sabe dibujar no rompería algunas de las mejores prácticas ya que depende de cuál sea la interfaz de usuario (consola, GUI, etc.)?
En muchos libros de programación me he encontrado con el ejemplo de "Forma" que muestra la herencia. La forma de la clase base tiene un método draw () en el que cada forma, como un círculo y una anulación cuadrada. Esto permite el polimorfismo. ¿Pero el método draw () no depende mucho de la interfaz de usuario? Si escribimos esta clase, por ejemplo, Win Forms, no podemos reutilizarla para una aplicación de consola o una aplicación web. ¿Es esto correcto?
La razón de la pregunta es que siempre me encuentro atascado y colgado sobre cómo generalizar las clases para que sean más útiles. Esto realmente está funcionando en mi contra y me pregunto si estoy "intentando demasiado".
Shape
clase, entonces probablemente estés escribiendo la pila de gráficos en sí, no escribiendo un cliente en la pila de gráficos.Respuestas:
Eso depende de la clase y el caso de uso. Un elemento visual que sepa dibujar no es necesariamente una violación del principio de responsabilidad única.
De nuevo, no necesariamente. Si puede crear una interfaz (drawPoint, drawLine, establecer Color, etc.), puede pasar cualquier contexto para dibujar cosas en algo a la forma, por ejemplo, dentro del constructor de la forma. Esto permitiría que las formas se dibujen en una consola o en cualquier lienzo dado.
Bien, eso es cierto. Si escribe un UserControl (no una clase en general) para Windows Forms, no podrá usarlo con una consola. Pero eso no es un problema. ¿Por qué esperaría que un UserControl para Windows Forms funcione con cualquier tipo de presentación? El UserControl debe hacer una cosa y hacerlo bien. Está vinculado a una cierta forma de presentación por definición. Al final, el usuario necesita algo concreto y no una abstracción. Esto podría ser solo cierto en parte para los marcos, pero lo es para las aplicaciones de usuario final.
Sin embargo, la lógica detrás de esto debe estar desacoplada, para que pueda usarla nuevamente con otras tecnologías de presentación. Introduzca interfaces cuando sea necesario para mantener la ortogonalidad de su aplicación. La regla general es: las cosas concretas deben ser intercambiables con otras cosas concretas.
Ya sabes, los programadores extremos son aficionados a su actitud YAGNI . No intentes escribir todo genéricamente y no te esfuerces demasiado tratando de hacer que todo sea de uso general. Esto se llama sobreingeniería y eventualmente conducirá a un código totalmente complicado. Asigne a cada componente exactamente una tarea y asegúrese de que lo haga bien. Ponga abstracciones donde sea necesario, donde espere que las cosas cambien (por ejemplo, interfaz para dibujar el contexto, como se indicó anteriormente).
En general, al escribir aplicaciones comerciales, siempre debe intentar desacoplar las cosas. MVC y MVVM son excelentes para desacoplar la lógica de la presentación, por lo que puede reutilizarla para una presentación web o una aplicación de consola. Tenga en cuenta que al final, algunas cosas tienen que ser concretas. Sus usuarios no pueden trabajar con una abstracción, necesitan algo concreto. Las abstracciones son solo ayudantes para usted, el programador, para mantener el código extensible y mantenible. Debe pensar dónde necesita que su código sea flexible. Finalmente, todas las abstracciones tienen que dar a luz a algo concreto.
Editar: si desea leer más sobre la arquitectura y las técnicas de diseño que pueden proporcionar las mejores prácticas, le sugiero que lea la respuesta de @Catchops y lea sobre las prácticas de SOLID en la wikipedia.
Además, para empezar, siempre recomiendo el siguiente libro: Head First Design Patterns . Le ayudará a comprender las técnicas de abstracción / prácticas de diseño OOP, más que el libro GoF (que es excelente, simplemente no es adecuado para principiantes).
fuente
Definitivamente tienes razón. Y no, estás "intentando muy bien" :)
Lea sobre el principio de responsabilidad única
Su funcionamiento interno de la clase y la forma en que esta información debe presentarse al usuario son dos responsabilidades.
No tengas miedo de desacoplar las clases. Raramente el problema es demasiada abstracción y desacoplamiento :)
Dos patrones muy relevantes son Model-view-controller para aplicaciones web y Model View ViewModel para Silverlight / WPF.
fuente
Uso mucho MVVM y, en mi opinión, una clase de objeto comercial nunca debería necesitar saber nada sobre la interfaz de usuario. Claro que es posible que necesiten conocer el
SelectedItem
, oIsChecked
, oIsVisible
, etc., pero esos valores no necesitan vincularse a ninguna IU particular y pueden ser propiedades genéricas en la clase.Si necesita hacer algo a la interfaz en el código subyacente, como configurar Focus, ejecutar una animación, manejar teclas de acceso rápido, etc., el código debe ser parte del código subyacente de la interfaz de usuario, no sus clases de lógica de negocios.
Entonces diría que no dejes de tratar de separar tu interfaz de usuario y tus clases. Cuanto más desacoplados estén, más fáciles serán de mantener y probar.
fuente
Hay una serie de patrones de diseño probados y verdaderos que se han desarrollado a lo largo de los años para abordar exactamente de lo que está hablando. Otras respuestas a su pregunta se han referido al Principio de responsabilidad única , que es absolutamente válido, y lo que parece estar impulsando su pregunta. Ese principio simplemente establece que una clase necesita hacer una cosa BIEN. En otras palabras, aumentar la Cohesión y reducir el Acoplamiento, que es de lo que se trata un buen diseño orientado a objetos: ¿una clase hace bien una cosa y no depende mucho de otras?
Bueno ... tienes razón al observar que si quieres dibujar un círculo en un iPhone, será diferente a dibujar uno en una PC con Windows. DEBE tener (en este caso) una clase concreta que dibuje uno bien en el iPhone y otra que dibuje uno bien en una PC. Aquí es donde se encuentra la herencia de OO básica que todos esos ejemplos de formas se descomponen. Simplemente no puede hacerlo solo con herencia.
Ahí es donde entran las interfaces, como dice el libro Gang of Four (DEBE LEER), siempre favorezca la implementación sobre la herencia. En otras palabras, use interfaces para armar una arquitectura que pueda realizar varias funciones de muchas maneras sin depender de dependencias codificadas.
He visto referencias a los principios SÓLIDOS . Esos son geniales. La 'S' es el principio de responsabilidad única. PERO, la 'D' significa Inversión de dependencia. El patrón de Inversión de Control (Inyección de Dependencia) puede usarse aquí. Es muy poderoso y puede usarse para responder la pregunta de cómo diseñar un sistema que pueda dibujar un círculo para un iPhone y uno para la PC.
Es posible crear una arquitectura que contenga reglas comerciales comunes y acceso a datos, pero que tenga varias implementaciones de interfaces de usuario utilizando estas construcciones. Sin embargo, realmente ayuda haber estado en un equipo que lo implementó y verlo en acción para comprenderlo realmente.
Esta es solo una respuesta rápida de alto nivel a una pregunta que merece una respuesta más detallada. Te animo a que profundices en estos patrones. Se puede encontrar una implementación más concreta de estos patrones como nombres bien conocidos de MVC y MVVM.
¡Buena suerte!
fuente
En este caso, aún puede usar MVC / MVVM e inyectar diferentes implementaciones de UI usando una interfaz común:
De esta forma, podrá reutilizar la lógica de su Controlador y Modelo mientras puede agregar nuevos tipos de GUI.
fuente
Hay diferentes patrones para hacer eso: MVP, MVC, MVVM, etc.
Un buen artículo de Martin Fowler (gran nombre) para leer es GUI Architectures: http://www.martinfowler.com/eaaDev/uiArchs.html
MVP aún no se ha mencionado, pero definitivamente merece ser citado: échale un vistazo.
Es el patrón sugerido por los desarrolladores de Google Web Toolkit para usar, es realmente genial.
Puede encontrar código real, ejemplos reales y justificación de por qué este enfoque es útil aquí:
http://code.google.com/webtoolkit/articles/mvp-architecture.html
http://code.google.com/webtoolkit/articles/mvp-architecture-2.html
¡Una de las ventajas de seguir este enfoque u otros similares que no se ha enfatizado lo suficiente aquí es la capacidad de prueba! ¡En muchos casos diría que es la principal ventaja!
fuente
Este es uno de los lugares donde OOP no puede hacer un buen trabajo en la abstracción. El polimorfismo OOP utiliza el despacho dinámico sobre una sola variable ('esto'). Si el polimorfismo estaba enraizado en Shape, entonces no puede enviar polimórficamente aliado en el renderizador (consola, GUI, etc.).
Considere un sistema de programación que podría distribuir en dos o más variables:
y también suponga que el sistema podría darle una forma de expresar poly_draw para varias combinaciones de tipos de formas y tipos de renderizador. Entonces sería fácil llegar a una clasificación de formas y renderizadores, ¿verdad? El verificador de tipo de alguna manera lo ayudaría a descubrir si había combinaciones de forma y renderizador que quizás no haya implementado.
La mayoría de los lenguajes OOP no admiten nada como el anterior (algunos lo hacen, pero no son convencionales). Te sugiero que eches un vistazo al patrón Visitor para una solución alternativa.
fuente
Arriba me suena correcto. Según tengo entendido, uno puede decir que significa un acoplamiento relativamente estrecho entre el Controlador y la Vista en términos del patrón de diseño MVC. Esto también significa que para cambiar entre desktop-console-webapp, uno tendrá que cambiar tanto Controller como View como un par ; solo el modelo permanece sin cambios.
Bueno, mi opinión actual anterior es que este acoplamiento de View-Controller del que estamos hablando está bien y aún más, es bastante moderno en diseño moderno.
Sin embargo, hace un año o dos también me sentí inseguro al respecto. He cambiado de opinión después de estudiar discusiones en el foro de Sun sobre patrones y diseño OO.
Si está interesado, pruebe este foro usted mismo: ahora ha migrado a Oracle ( enlace ). Si llegas allí, intenta hacer ping al tipo Saish ; en aquel entonces, sus explicaciones sobre estos asuntos difíciles me resultaron muy útiles. Sin embargo, no puedo decir si todavía participa, yo mismo no he estado allí durante bastante tiempo.
fuente
Desde un punto de vista pragmático, algún código en su sistema necesita saber cómo dibujar algo así como
Rectangle
si ese es un requisito del usuario final. Y eso se reducirá en algún momento a hacer cosas de muy bajo nivel como rasterizar píxeles o mostrar algo en una consola.La pregunta para mí desde el punto de vista del acoplamiento es quién / qué debería depender de este tipo de información, y en qué grado de detalle (qué tan abstracto, por ejemplo)
Resumen de capacidades de dibujo / renderizado
Porque si el código de dibujo de nivel superior solo depende de algo muy abstracto, esa abstracción podría funcionar (mediante la sustitución de implementaciones concretas) en todas las plataformas a las que pretende dirigirse. Como ejemplo artificial, alguna
IDrawer
interfaz muy abstracta podría ser implementada tanto en la consola como en las API de la GUI para hacer cosas como formas de trazado (la implementación de la consola podría tratar la consola como una "imagen" de 80xN con arte ASCII). Por supuesto, ese es un ejemplo artificial, ya que generalmente eso no es lo que quiere hacer es tratar una salida de consola como un búfer de imagen / marco; por lo general, la mayoría de las necesidades del usuario final requieren más interacciones basadas en texto en las consolas.Otra consideración es ¿qué tan fácil es diseñar una abstracción estable? Porque podría ser fácil si todo lo que está apuntando son API GUI modernas para abstraer las capacidades básicas de dibujo de formas como trazar líneas, rectángulos, trazados, texto, cosas de este tipo (solo rasterización 2D simple de un conjunto limitado de primitivas) , con una interfaz abstracta que puede implementarse fácilmente para ellos a través de varios subtipos con poco costo. Si puede diseñar tal abstracción de manera efectiva e implementarla en todas las plataformas de destino, entonces diría que es un mal mucho menor, si es que es un mal, para una forma o control de GUI o lo que sea para saber cómo dibujarse usando tal abstracción.
Pero supongamos que está tratando de abstraer los detalles sangrientos que varían entre una Playstation Portable, un iPhone, una XBox One y una poderosa PC para juegos, mientras que sus necesidades son utilizar las técnicas de sombreado / renderizado 3D en tiempo real más avanzadas en cada una . En ese caso, tratar de encontrar una interfaz abstracta para abstraer los detalles de renderizado cuando las capacidades de hardware subyacentes y las API varían enormemente es casi seguro que resultará en un enorme tiempo de diseño y rediseño, una alta probabilidad de cambios recurrentes en el diseño con imprevistos descubrimientos, y del mismo modo una solución de denominador común más bajo que no explota la singularidad y el poder del hardware subyacente.
Hacer que las dependencias fluyan hacia diseños estables y "fáciles"
En mi campo estoy en ese último escenario. Apuntamos a una gran cantidad de hardware diferente con API y capacidades subyacentes radicalmente diferentes, y tratar de encontrar una abstracción de renderizado / dibujo para gobernarlos a todos es casi imposible (podríamos ser mundialmente famosos simplemente haciendo eso de manera efectiva, ya que sería un juego cambiador en la industria). Así que lo último que quiero en mi caso es como el analógico
Shape
oModel
oParticle Emitter
que sabe cómo dibujar en sí, incluso si se está expresando que el dibujo en el nivel más alto y su forma más abstracta posible ...... porque esas abstracciones son demasiado difíciles de diseñar correctamente, y cuando un diseño es difícil de corregir, y todo depende de ello, esa es una receta para los cambios de diseño centrales más costosos que ondulan y rompen todo dependiendo de él. Entonces, lo último que desea es que las dependencias en sus sistemas fluyan hacia diseños abstractos demasiado difíciles de corregir (demasiado difíciles de estabilizar sin cambios intrusivos).
Difícil depende de fácil, no fácil depende de difícil
Entonces, lo que hacemos es hacer que las dependencias fluyan hacia cosas que son fáciles de diseñar. Es mucho más fácil diseñar un "Modelo" abstracto que solo se centre en almacenar cosas como polígonos y materiales y obtener ese diseño correcto que diseñar un "Renderer" abstracto que pueda implementarse efectivamente (a través de subtipos de concreto sustituibles) para dar servicio al dibujo solicita uniformemente hardware tan dispares como una PSP desde una PC.
Entonces invertimos las dependencias lejos de las cosas que son difíciles de diseñar. En lugar de hacer que los modelos abstractos sepan cómo dibujarse a sí mismos a un diseño de renderizador abstracto del que todos dependen (y romper sus implementaciones si ese diseño cambia), en su lugar tenemos un renderizador abstracto que sabe cómo dibujar cada objeto abstracto en nuestra escena ( modelos, emisores de partículas, etc.), y así podemos implementar un subtipo de renderizador OpenGL para PC como
RendererGl
, otro para PSP comoRendererPsp
, otro para teléfonos móviles, etc. En ese caso, las dependencias fluyen hacia diseños estables, fáciles de corregir, desde el renderizador hasta varios tipos de entidades (modelos, partículas, texturas, etc.) en nuestra escena, no al revés.Si te encuentras tratando de abstraer algo en un nivel central de tu base de código y es demasiado difícil de diseñar, en lugar de golpear obstinadamente las cabezas contra las paredes y hacer cambios intrusivos constantemente cada mes / año que requiere actualizar 8,000 archivos fuente porque es rompiendo todo lo que depende de ello, mi sugerencia número uno es considerar invertir las dependencias. Vea si puede escribir el código de manera tal que lo que es tan difícil de diseñar dependa de todo lo que sea más fácil de diseñar, no tener las cosas que son más fáciles de diseñar dependiendo de lo que es tan difícil de diseñar. Tenga en cuenta que estoy hablando de diseños (específicamente diseños de interfaz) y no implementaciones: a veces las cosas son fáciles de diseñar y difíciles de implementar, y a veces las cosas son difíciles de diseñar pero fáciles de implementar. Las dependencias fluyen hacia los diseños, por lo que el enfoque solo debe centrarse en cuán difícil es diseñar aquí para determinar la dirección en la que fluyen las dependencias.
Principio de responsabilidad única
Para mí, SRP no es tan interesante aquí generalmente (aunque depende del contexto). Quiero decir que hay un acto de equilibrio de la cuerda floja en el diseño de cosas que son claras en su propósito y mantenibles, pero sus
Shape
objetos pueden tener que exponer información más detallada si no saben cómo dibujar, por ejemplo, y puede que no haya muchas cosas significativas para hacer con una forma en un contexto de uso particular que construirla y dibujarla. Hay compensaciones con casi todo, y no está relacionado con SRP que puede hacer que las cosas sean conscientes de cómo dibujarse capaces de convertirse en una pesadilla de mantenimiento en mi experiencia en ciertos contextos.Tiene mucho más que ver con el acoplamiento y la dirección en la que fluyen las dependencias en su sistema. Si está intentando portar una interfaz de renderización abstracta de la que todo depende (porque la están usando para dibujar) a una nueva API / hardware objetivo y se da cuenta de que tiene que cambiar considerablemente su diseño para que funcione allí efectivamente, entonces Es un cambio muy costoso de realizar que requiere reemplazar las implementaciones de todo lo que sabe dibujar en su sistema. Y ese es el problema de mantenimiento más práctico que encuentro con cosas conscientes de cómo dibujarse si eso se traduce en un montón de dependencias que fluyen hacia abstracciones que son demasiado difíciles de diseñar correctamente por adelantado.
Orgullo desarrollador
Menciono este punto porque, en mi experiencia, este es a menudo el mayor obstáculo para fluir la dirección de las dependencias hacia cosas más fáciles de diseñar. Es muy fácil para los desarrolladores ser un poco ambiciosos aquí y decir: "Voy a diseñar la abstracción de representación multiplataforma para gobernarlos a todos, voy a resolver lo que otros desarrolladores pasan meses en portar, y voy a obtener bien y funcionará como magia en cada plataforma que admitamos y utilizará las técnicas de renderizado más avanzadas en cada una; ya lo imaginé en mi cabeza ".En ese caso, se resisten a la solución práctica que consiste en evitar hacer eso y simplemente cambian la dirección de las dependencias y traducen los cambios de diseño central que pueden ser enormemente costosos y recurrentes en cambios recurrentes simplemente baratos y locales para la implementación. Debe haber algún tipo de instinto de "bandera blanca" en los desarrolladores para darse por vencido cuando algo es demasiado difícil de diseñar a un nivel tan abstracto y reconsiderar toda su estrategia, de lo contrario, se enfrentarán a un gran dolor y pena. Sugeriría transferir tales ambiciones y espíritu de lucha a implementaciones de vanguardia de una cosa más fácil de diseñar que llevar tales ambiciones conquistadoras del mundo al nivel de diseño de interfaz.
fuente
OpenGLBillboard
, ¿harías unOpenGLRenderer
que sepa dibujar cualquier tipo deIBillBoard
? ¿Pero lo haría delegando la lógica aIBillBoard
, o tendría grandes interruptores oIBillBoard
tipos con condicionales? Eso es lo que me resulta difícil de entender, ¡porque eso no parece sostenible en absoluto!RendererPsp
que conoce las abstracciones de alto nivel de tu escena de juego, puede hacer toda la magia y los volteretas que necesita para hacer esas cosas de una manera que se vea convincente en la PSP ...BillBoard
sería muy difícil hacer objetos de bajo nivel, como depender de ellos? ¿Mientras que a yaIRenderer
es de alto nivel y podría depender de estas preocupaciones con muchos menos problemas?