Mas o método draw () não depende muito da interface do usuário?
De um ponto de vista pragmático, algum código em seu sistema precisa saber como desenhar algo como um Rectangle
se esse for um requisito para o usuário final. Em algum momento, isso se resumirá em fazer coisas realmente de baixo nível, como rasterizar pixels ou exibir algo em um console.
A questão para mim do ponto de vista do acoplamento é quem / o que deve depender desse tipo de informação e com que grau de detalhe (quão abstrato, por exemplo)?
Abstraindo as capacidades de desenho / renderização
Como se o código de desenho de nível superior depender apenas de algo muito abstrato, essa abstração poderá funcionar (através da substituição de implementações concretas) em todas as plataformas que você pretende direcionar. Como exemplo artificial, alguma IDrawer
interface muito abstrata pode ser implementada nas APIs do console e da GUI para fazer coisas como formas de plotagem (a implementação do console pode tratar o console como uma "imagem" de 80xN com arte ASCII). Claro que esse é um exemplo artificial, já que geralmente não é o que você quer fazer é tratar uma saída do console como um buffer de imagem / quadro; normalmente, a maioria das necessidades do usuário final exige mais interações baseadas em texto nos consoles.
Outra consideração é como é fácil projetar uma abstração estável? Porque pode ser fácil se tudo o que você está alvejando são as modernas APIs da GUI para abstrair os recursos básicos de desenho de formas, como traçar linhas, retângulos, caminhos, texto, coisas desse tipo (apenas rasterização 2D simples de um conjunto limitado de primitivas) , com uma interface abstrata que pode ser facilmente implementada para todos eles através de vários subtipos com pouco custo. Se você pode projetar essa abstração de maneira eficaz e implementá-la em todas as plataformas de destino, eu diria que é um mal muito menor, mesmo que seja um mal, para uma forma ou controle de GUI ou qualquer outra coisa que saiba como se desenhar usando esse abstração.
Mas digamos que você esteja tentando abstrair os detalhes sangrentos que variam entre um Playstation Portable, iPhone, um XBox One e um poderoso PC para jogos, enquanto suas necessidades são utilizar as mais avançadas técnicas de renderização / sombreamento 3D em tempo real de cada uma. . Nesse caso, tentar criar uma interface abstrata para abstrair os detalhes de renderização quando os recursos e APIs de hardware subjacentes variarem muito, quase certamente resultará em um tempo enorme de design e redesenho, uma alta probabilidade de mudanças de design recorrentes com imprevistos. descobertas e da mesma forma uma solução de denominador comum mais baixo que falha ao explorar a exclusividade e o poder completos do hardware subjacente.
Fazendo com que as dependências fluam para projetos estáveis e "fáceis"
No meu campo, estou nesse último cenário. Temos como alvo muitos hardwares diferentes, com recursos e APIs subjacentes radicalmente diferentes, e tentar criar uma abstração de renderização / desenho para governá-los é algo sem esperança (podemos tornar-nos mundialmente famosos apenas fazendo isso com eficácia, pois seria um jogo trocador na indústria). Assim, a última coisa que eu quero no meu caso é como a analógica Shape
ou Model
ou Particle Emitter
que sabe como desenhar a si próprio, mesmo se ele está expressando que o desenho no mais alto nível e forma mais abstrato possível ...
... porque essas abstrações são muito difíceis de projetar corretamente, e quando um projeto é difícil de corrigir, e tudo depende disso, essa é uma receita para as alterações de projeto central mais caras que ondulam e quebram tudo, dependendo dele. Portanto, a última coisa que você deseja é que as dependências em seus sistemas fluam para projetos abstratos muito difíceis de serem corrigidos (muito difíceis de estabilizar sem alterações intrusivas).
Difícil Depende de Fácil, Não Fácil Depende de Difícil
Então, o que fazemos é fazer com que as dependências fluam em direção a coisas fáceis de projetar. É muito mais fácil projetar um "Modelo" abstrato, focado apenas no armazenamento de coisas como polígonos e materiais e obter esse design correto do que projetar um "Renderer" abstrato que possa ser efetivamente implementado (por meio de subtipos de concreto substituíveis) para servir ao desenho solicita uniformemente um hardware tão díspar quanto um PSP a partir de um PC.
Portanto, invertemos as dependências das coisas difíceis de projetar. Em vez de fazer com que os modelos abstratos saibam desenhar-se para um design de renderizador abstrato do qual todos eles dependem (e quebre suas implementações se esse design mudar), temos um renderizador abstrato que sabe como desenhar todos os objetos abstratos em nossa cena ( modelos, emissores de partículas, etc.) e, portanto, podemos implementar um subtipo de renderizador OpenGL para PCs RendererGl
, outro para PSPs RendererPsp
, outro para celulares etc. Nesse caso, as dependências estão fluindo para projetos estáveis, fáceis de corrigir, do renderizador a vários tipos de entidades (modelos, partículas, texturas etc.) em nossa cena, e não o contrário.
- Estou usando "estabilidade / instabilidade" em um sentido ligeiramente diferente da métrica de acoplamentos aferentes / eferentes do tio Bob, que está medindo mais a dificuldade da mudança, tanto quanto eu possa entender. Estou falando mais sobre "probabilidade de exigir mudanças", embora sua métrica de estabilidade seja útil lá. Quando a "probabilidade de mudança" é proporcional à "facilidade de mudança" (por exemplo: as coisas com maior probabilidade de exigir mudanças têm a mais alta instabilidade e acoplamentos aferentes da métrica do tio Bob), essas alterações prováveis são baratas e não intrusivas. , exigindo apenas a substituição de uma implementação sem tocar em nenhum design central.
Se você está tentando abstrair algo em um nível central da sua base de código e é muito difícil projetar, em vez de bater de cabeça teimosamente contra paredes e fazer constantemente alterações intrusivas a cada mês / ano, que exigem a atualização de 8.000 arquivos de origem, porque é quebrando tudo o que depende, minha sugestão número um é considerar a inversão das dependências. Veja se você pode escrever o código de uma maneira que a coisa que é tão difícil de projetar depende de tudo o que é mais fácil de projetar, sem ter as coisas que são mais fáceis de projetar, dependendo da coisa que é tão difícil de projetar. Observe que estou falando de designs (especificamente designs de interfaces) e não de implementações: às vezes, as coisas são fáceis de projetar e difíceis de implementar, e às vezes as coisas são difíceis de projetar, mas fáceis de implementar. As dependências fluem para os projetos; portanto, o foco deve ser apenas o quão difícil é projetar aqui para determinar a direção em que as dependências fluem.
Princípio da responsabilidade única
Para mim, o SRP não é tão interessante aqui normalmente (embora dependendo do contexto). Quero dizer, há um ato de equilibrar a corda bamba ao projetar coisas que são claras de propósito e de manutenção, mas seus Shape
objetos podem precisar expor informações mais detalhadas se não souberem se desenhar, por exemplo, e talvez não haja muitas coisas significativas para faça com uma forma em um contexto de uso específico do que construa e desenhe. Há trocas com quase tudo, e não está relacionado ao SRP que pode tornar as coisas conscientes de como se tornar capaz de se tornar um pesadelo de manutenção na minha experiência em determinados contextos.
Tem muito mais a ver com acoplamento e a direção na qual as dependências fluem no seu sistema. Se você estiver tentando portar uma interface de renderização abstrata da qual tudo depende (porque eles a estão usando para se desenhar) para uma nova API / hardware de destino e perceber que você precisa alterar consideravelmente o design para fazê-la funcionar efetivamente lá, é uma alteração muito cara a ser feita, que requer a substituição de implementações de tudo em seu sistema que sabe como se desenhar. E esse é o problema de manutenção mais prático que encontro com as coisas que sabem como se desenhar, se isso se traduz em um monte de dependências fluindo em direção a abstrações que são muito difíceis de projetar corretamente com antecedência.
Orgulho do desenvolvedor
Mencionei esse ponto porque, na minha experiência, esse costuma ser o maior obstáculo ao direcionamento das dependências para coisas mais fáceis de projetar. É muito fácil para os desenvolvedores ficarem um pouco ambiciosos aqui e dizer: "Vou projetar a abstração de renderização de plataforma cruzada para governar todos eles, vou resolver o que outros desenvolvedores passam meses em portar e vou conseguir certo e funcionará como mágica em todas as plataformas que suportamos e utilizamos as técnicas de renderização de ponta em cada uma; eu já imaginava isso na minha cabeça ".Nesse caso, eles resistem à solução prática, que é evitar fazê-lo, apenas mudar a direção das dependências e traduzir o que pode ser enormemente caro e recorrente nas alterações de design central para alterações simplesmente baratas e locais na implementação. É preciso haver algum tipo de instinto de "bandeira branca" nos desenvolvedores para desistir quando algo é muito difícil de projetar para um nível tão abstrato e reconsiderar toda a sua estratégia; caso contrário, eles sofrerão muito. Eu sugeriria transferir tais ambições e espírito de luta para implementações de última geração, de uma coisa mais fácil de projetar do que levar essas ambições de conquista do mundo para o nível de design de interface.