¿Pero el método draw () no depende mucho de la interfaz de usuario?
Desde un punto de vista pragmático, algún código en su sistema necesita saber cómo dibujar algo así como Rectanglesi 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 IDrawerinterfaz 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 Shapeo Modelo Particle Emitterque 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 como RendererPsp, 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.

- Estoy usando "estabilidad / inestabilidad" en un sentido ligeramente diferente de la métrica de acoplamientos aferentes / eferentes del tío Bob, que está midiendo más la dificultad del cambio hasta donde puedo entender. Estoy hablando más sobre la "probabilidad de requerir un cambio", aunque su métrica de estabilidad es útil allí. Cuando la "probabilidad de cambio" es proporcional a la "facilidad de cambio" (por ejemplo: las cosas que más probablemente requieren cambios tienen la mayor inestabilidad y los acoplamientos aferentes de la métrica del tío Bob), entonces cualquier cambio probable es barato y no intrusivo. , que solo requiere reemplazar una implementación sin tocar ningún diseño central.
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 Shapeobjetos 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.