Qual é a melhor prática quando se trata de escrever aulas que talvez precisem saber sobre a interface do usuário. Uma classe que sabe desenhar-se não quebraria algumas práticas recomendadas, pois depende de qual é a interface do usuário (console, GUI etc.)?
Em muitos livros de programação, me deparei com o exemplo "Shape" que mostra herança. A forma da classe base possui um método draw () que cada forma, como um círculo e um quadrado, substitui. Isso permite polimorfismo. Mas o método draw () não depende muito da interface do usuário? Se escrevermos esta classe para, por exemplo, Win Forms, não poderemos reutilizá-la para um aplicativo de console ou aplicativo da web. Isso está correto?
O motivo da pergunta é que eu sempre fico preso e desligado de como generalizar as aulas para que elas sejam mais úteis. Isso está realmente funcionando contra mim e eu estou me perguntando se estou "tentando demais".
Shape
classe, provavelmente está escrevendo a própria pilha de gráficos, não escrevendo um cliente na pilha de gráficos.Respostas:
Isso depende da classe e do caso de uso. Um elemento visual que sabe se desenhar não é necessariamente uma violação do princípio da responsabilidade única.
Novamente, não necessariamente. Se você pode criar uma interface (drawPoint, drawLine, definir Color etc.), pode praticamente transmitir qualquer contexto para desenhar coisas em algo na forma, por exemplo, dentro do construtor da forma. Isso permitiria que as formas se desenhassem em um console ou em qualquer tela fornecida.
Bem, isso é verdade. Se você escrever um UserControl (não uma classe em geral) para o Windows Forms, não poderá usá-lo com um console. Mas isso não é problema. Por que você esperaria que um UserControl for Windows Forms funcionasse com qualquer tipo de apresentação? O UserControl deve fazer uma coisa e fazê-lo bem. Está vinculado a uma certa forma de apresentação por definição. No final, o usuário precisa de algo concreto e não de uma abstração. Isso pode ser apenas parcialmente verdadeiro para estruturas, mas é para aplicativos de usuário final.
No entanto, a lógica por trás disso deve ser dissociada, para que você possa usá-lo novamente com outras tecnologias de apresentação. Introduzir interfaces sempre que necessário, para manter a ortogonalidade do seu aplicativo. A regra geral é: As coisas concretas devem ser trocáveis com outras coisas concretas.
Você sabe, programadores extremos gostam de sua atitude no YAGNI . Não tente escrever tudo de forma genérica e não tente demais fazer de tudo para fins gerais. Isso é chamado de superengenharia e, eventualmente, levará a um código totalmente complicado. Dê a cada componente exatamente uma tarefa e verifique se ela funciona bem. Coloque abstrações onde for necessário, onde você espera que as coisas mudem (por exemplo, interface para o contexto do desenho, como mencionado acima).
Em geral, ao escrever aplicativos de negócios, você deve sempre tentar desacoplar as coisas. O MVC e o MVVM são ótimos para separar a lógica da apresentação, para que você possa reutilizá-la para uma apresentação na Web ou um aplicativo de console. Lembre-se de que, no final, algumas coisas precisam ser concretas. Seus usuários não podem trabalhar com uma abstração, eles precisam de algo concreto. As abstrações são apenas auxiliares para você, programador, manter o código extensível e sustentável. Você precisa indicar onde você precisa que seu código seja flexível. Eventualmente, todas as abstrações precisam dar origem a algo concreto.
Editar: se você quiser ler mais sobre técnicas de arquitetura e design que podem fornecer práticas recomendadas, sugiro que você leia a resposta do @Catchops e leia sobre as práticas do SOLID na wikipedia.
Além disso, para iniciantes, eu sempre recomendo o seguinte livro: Head First Design Patterns . Isso o ajudará a entender as técnicas de abstração / práticas de design de POO, mais do que o livro do GoF (que é excelente, mas não é adequado para iniciantes).
fonte
Você está definitivamente certo. E não, você está "tentando muito bem" :)
Leia sobre o princípio da responsabilidade única
Seu trabalho interno com a classe e a maneira como essas informações devem ser apresentadas ao usuário são duas responsabilidades.
Não tenha medo de dissociar as aulas. Raramente o problema é muita abstração e desacoplamento :)
Dois padrões muito relevantes são o Model-view-controller para aplicativos da Web e o Model View ViewModel para Silverlight / WPF.
fonte
Eu uso muito o MVVM e, na minha opinião, uma classe de objeto de negócios nunca precisa saber nada sobre a interface do usuário. Claro que eles podem precisar conhecer o
SelectedItem
, ouIsChecked
, ouIsVisible
etc, mas esses valores não precisam se vincular a nenhuma interface do usuário específica e podem ser propriedades genéricas na classe.Se você precisar fazer algo com a interface no código por trás, como definir o Focus, executar uma animação, manipular teclas de atalho etc., o código deve fazer parte do código por trás da interface do usuário, não das classes de lógica de negócios.
Então, eu diria que não pare de tentar dividir sua interface do usuário e suas classes. Quanto mais dissociados, mais fáceis são de manter e testar.
fonte
Há vários padrões de design testados e aprovados que foram desenvolvidos ao longo dos anos para tratar exatamente do que você está falando. Outras respostas à sua pergunta se referiram ao Princípio Único de Responsabilidade - que é absolutamente válido - e ao que parece estar direcionando sua pergunta. Esse princípio simplesmente afirma que uma classe precisa fazer uma coisa BEM. Em outras palavras, aumentar a coesão e diminuir o acoplamento, que é o objetivo do bom design orientado a objetos - uma classe faz uma coisa bem e não tem muitas dependências das outras.
Bem ... você está certo ao observar que, se quiser desenhar um círculo em um iPhone, será diferente de desenhar um círculo em um PC executando o Windows. Você DEVE ter (neste caso) uma classe concreta que desenhe um poço no iPhone e outra que desenhe um poço no PC. É aqui que o legado básico da herança OO que todos esses exemplos de formas se decompõem. Você simplesmente não pode fazê-lo apenas com herança.
É aí que as interfaces entram - como afirma o livro da Gangue dos Quatro (DEVE LER) - Sempre favorecem a implementação sobre a herança. Em outras palavras, use interfaces para reunir uma arquitetura que possa executar várias funções de várias maneiras, sem depender de dependências codificadas.
Eu já vi referência aos princípios do SOLID . Essas são ótimas. O 'S' é o princípio da responsabilidade única. MAS, o 'D' significa Inversão de Dependência. O padrão Inversão de Controle (Injeção de Dependência) pode ser usado aqui. É muito poderoso e pode ser usado para responder à pergunta de como arquitetar um sistema que pode desenhar um círculo para um iPhone e outro para o PC.
É possível criar uma arquitetura que contenha regras comerciais comuns e acesso a dados, mas tenha várias implementações de interfaces com o usuário usando essas construções. Realmente ajuda, no entanto, estar em uma equipe que o implementou e vê-lo em ação para realmente entendê-lo.
Esta é apenas uma resposta rápida de alto nível a uma pergunta que merece uma resposta mais detalhada. Encorajo-vos a olhar mais para esses padrões. Uma implementação mais concreta desses padrões pode ser encontrada, assim como nomes conhecidos de MVC e MVVM.
Boa sorte!
fonte
Nesse caso, você ainda pode usar o MVC / MVVM e injetar diferentes implementações da interface do usuário usando a interface comum:
Dessa forma, você poderá reutilizar sua lógica de Controlador e Modelo enquanto poderá adicionar novos tipos de GUI.
fonte
Existem diferentes padrões para fazer isso: MVP, MVC, MVVM, etc ...
Um bom artigo de Martin Fowler (grande nome) para ler é GUI Architectures: http://www.martinfowler.com/eaaDev/uiArchs.html
O MVP ainda não foi mencionado, mas definitivamente merece ser citado: dê uma olhada nele.
É o padrão sugerido pelos desenvolvedores do Google Web Toolkit, é realmente legal.
Você pode encontrar código real, exemplos reais e justificativas sobre por que essa abordagem é útil aqui:
http://code.google.com/webtoolkit/articles/mvp-architecture.html
http://code.google.com/webtoolkit/articles/mvp-architecture-2.html
Uma das vantagens de seguir esta ou outras abordagens semelhantes que não foram enfatizadas o suficiente aqui é a testabilidade! Em muitos casos, eu diria que essa é a principal vantagem!
fonte
Este é um dos lugares onde o OOP falha em fazer um bom trabalho na abstração. O polimorfismo OOP usa despacho dinâmico sobre uma única variável ('this'). Se o polimorfismo estiver enraizado no Shape, você não poderá despachar polimorficamente no renderizador (console, GUI etc.).
Considere um sistema de programação que possa despachar em duas ou mais variáveis:
e também suponha que o sistema possa fornecer uma maneira de expressar poly_draw para várias combinações de tipos de forma e tipos de renderizador. Seria fácil criar uma classificação de formas e renderizadores, certo? O verificador de tipos de alguma forma ajudaria você a descobrir se havia combinações de forma e renderizador que você pode ter perdido na implementação.
A maioria dos idiomas OOP não suporta nada como o descrito acima (alguns o fazem, mas eles não são comuns). Eu sugiro que você dê uma olhada no padrão Visitor para uma solução alternativa.
fonte
Acima parece correto para mim. Pelo meu entendimento, pode-se dizer que significa um acoplamento relativamente apertado entre o Controller e o View em termos de padrão de design do MVC. Isso também significa que, para alternar entre o desktop-console-webapp, será necessário alternar o Controller e o View como um par - apenas o modelo permanece inalterado.
Bem, minha opinião atual acima é que esse acoplamento View-Controller de que estamos falando está OK e, mais ainda, está bastante moderno no design moderno.
Porém, há um ou dois anos, eu também me sentia insegura sobre isso. Mudei de idéia depois de estudar as discussões no fórum da Sun sobre padrões e design de OO.
Se você estiver interessado, experimente este fórum - ele foi migrado para o Oracle agora ( link ). Se você chegar lá, tente fazer o ping no cara Saish - na época, suas explicações sobre esses assuntos complicados se revelaram muito úteis para mim. Não sei dizer se ele ainda participa - eu mesmo não estou lá há um bom tempo
fonte
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
ouModel
ouParticle 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 PSPsRendererPsp
, 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.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.
fonte
OpenGLBillboard
, você faria umOpenGLRenderer
que sabe desenhar qualquer tipo deIBillBoard
? Mas faria isso delegando a lógicaIBillBoard
ou teria enormes opções ouIBillBoard
tipos com condicionais? É isso que acho difícil de entender, porque isso não parece sustentável!RendererPsp
que conhece as abstrações de alto nível da cena do jogo, ela pode fazer toda a mágica e retroceder para renderizar essas coisas de uma maneira que pareça convincente no PSP ...BillBoard
seria difícil fazer objetos de baixo nível, como uma dependência deles? Considerando que umIRenderer
já é alto e pode depender dessas preocupações com muito menos problemas?