Suponha que exista uma Page
classe, que represente um conjunto de instruções para um renderizador de página. E suponha que exista uma Renderer
classe que saiba como renderizar uma página na tela. É possível estruturar o código de duas maneiras diferentes:
/*
* 1) Page Uses Renderer internally,
* or receives it explicitly
*/
$page->renderMe();
$page->renderMe($renderer);
/*
* 2) Page is passed to Renderer
*/
$renderer->renderPage($page);
Quais são os prós e os contras de cada abordagem? Quando alguém será melhor? Quando o outro será melhor?
FUNDO
Para adicionar um pouco mais de fundo - estou me achando usando as duas abordagens no mesmo código. Estou usando uma biblioteca de PDF de terceiros chamada TCPDF
. Em algum lugar do meu código, tenho que ter o seguinte para que a renderização de PDF funcione:
$pdf = new TCPDF();
$html = "some text";
$pdf->writeHTML($html);
Digamos que desejo criar uma representação da página. Eu poderia criar um modelo que contém instruções para renderizar um trecho de página em PDF da seguinte maneira:
/*
* A representation of the PDF page snippet:
* a template directing how to render a specific PDF page snippet
*/
class PageSnippet
{
function runTemplate(TCPDF $pdf, array $data = null): void
{
$pdf->writeHTML($data['html']);
}
}
/* To be used like so */
$pdf = new TCPDF();
$data['html'] = "some text";
$snippet = new PageSnippet();
$snippet->runTemplate($pdf, $data);
1) Observe aqui que $snippet
é executado sozinho , como no meu primeiro exemplo de código. Ele também precisa conhecer e estar familiarizado com o $pdf
e com qualquer um $data
para que ele funcione.
Mas, eu posso criar uma PdfRenderer
classe assim:
class PdfRenderer
{
/**@var TCPDF */
protected $pdf;
function __construct(TCPDF $pdf)
{
$this->pdf = $pdf;
}
function runTemplate(PageSnippet $template, array $data = null): void
{
$template->runTemplate($this->pdf, $data);
}
}
e então meu código se volta para isso:
$renderer = new PdfRenderer(new TCPDF());
$renderer->runTemplate(new PageSnippet(), array('html' => 'some text'));
2) Aqui, $renderer
recebe o PageSnippet
e todo o $data
necessário para que ele funcione. Isso é semelhante ao meu segundo exemplo de código.
Portanto, mesmo que o renderizador receba o trecho de página, dentro do renderizador, o trecho ainda será executado automaticamente . Ou seja, ambas as abordagens estão em jogo. Não tenho certeza se você pode restringir o uso de OO a apenas um ou apenas o outro. Ambos podem ser necessários, mesmo se você mascarar um pelo outro.
fonte
Respostas:
Isso depende inteiramente do que você pensa que é OO .
Para OOP = SOLID, a operação deve fazer parte da classe se fizer parte da Responsabilidade Única da classe.
Para OO = despacho virtual / polimorfismo, a operação deve fazer parte do objeto se for despachada dinamicamente, ou seja, se for chamada por meio de uma interface.
Para OO = encapsulamento, a operação deve fazer parte da classe se ela usar o estado interno que você não deseja expor.
Para OO = "Eu gosto de interfaces fluentes", a questão é qual variante lê com mais naturalidade.
Para OO = modelando entidades do mundo real, qual entidade do mundo real executa essa operação?
Todos esses pontos de vista geralmente estão errados isoladamente. Mas, às vezes, uma ou mais dessas perspectivas são úteis para se chegar a uma decisão de design.
Por exemplo, usando o ponto de vista do polimorfismo: se você tem estratégias de renderização diferentes (como formatos de saída diferentes ou mecanismos de renderização diferentes),
$renderer->render($page)
faz muito sentido. Mas se você tiver diferentes tipos de página que devem ser renderizados de maneira diferente,$page->render()
poderá ser melhor. Se a saída depender do tipo de página e da estratégia de renderização, você poderá fazer o envio duplo através do padrão de visitante.Não esqueça que, em muitos idiomas, as funções não precisam ser métodos. Uma função simples, como
render($page)
se frequentemente uma solução perfeitamente fina (e maravilhosamente simples).fonte
$renderer
ia decidir como renderizar. Quando as$page
conversas com o$renderer
que diz é o que render. Não como. O$page
não tem idéia de como. Isso me causa problemas no SRP?Segundo Alan Kay , os objetos são auto-suficientes, "adultos" e organismos responsáveis. Os adultos fazem coisas, não são operados. Ou seja, a transação financeira é responsável por se salvar , a página é responsável por se renderizar , etc., etc. Mais concisamente, o encapsulamento é a grande coisa no OOP. Em particular, ele se manifesta através do famoso princípio Tell don't ask (que @CandiedOrange gosta de mencionar o tempo todo :)) e reprovação pública de getters e setters .
Na prática, resulta em objetos que possuem todos os recursos necessários para realizar seu trabalho, como instalações de banco de dados, instalações de renderização etc.
Portanto, considerando o seu exemplo, minha versão OOP teria a seguinte aparência:
Caso você esteja interessado, David West fala sobre os princípios originais de POO em seu livro, Object Thinking .
fonte
Aqui nós
page
somos completamente responsáveis por se render. Pode ter sido fornecida com uma renderização por meio de um construtor, ou pode ter essa funcionalidade incorporada.Ignorarei o primeiro caso (fornecido com uma renderização por meio de um construtor) aqui, pois é bem parecido com passar como parâmetro. Em vez disso, examinarei os prós e os contras da funcionalidade incorporada.
O profissional é que ele permite um nível muito alto de encapsulamento. A página não precisa revelar nada sobre seu estado interno diretamente. Apenas o expõe através de uma renderização de si mesmo.
O golpe é que ele quebra o princípio da responsabilidade única (SRP). Temos uma classe que é responsável por encapsular o estado de uma página e também é codificada com regras sobre como se renderizar e, portanto, provavelmente toda uma gama de outras responsabilidades, pois os objetos devem "fazer coisas para si mesmos, não fazer coisas por outras pessoas "
Aqui, ainda estamos exigindo que uma página seja capaz de renderizar a si mesma, mas estamos fornecendo a ela um objeto auxiliar que pode executar a renderização real. Dois cenários podem surgir aqui:
Aqui, respeitamos totalmente o SRP. O objeto de página é responsável por manter as informações em uma página e o renderizador é responsável por renderizar essa página. No entanto, agora enfraquecemos completamente o encapsulamento do objeto de página, pois ele precisa tornar público todo o seu estado.
Além disso, criamos um novo problema: o renderizador agora está fortemente acoplado à classe da página. O que acontece quando queremos renderizar algo diferente para uma página?
Qual é melhor? Nenhum deles. Todos eles têm suas falhas.
fonte
A resposta a esta pergunta é inequívoca. É
$renderer->renderPage($page);
qual é a implementação correta. Para entender como chegamos a essa conclusão, precisamos entender o encapsulamento.O que é uma página? É uma representação de uma exibição que alguém consumirá. Esse "alguém" pode ser humano ou bots. Observe que
Page
é uma representação e não a própria exibição. Existe uma representação sem ser representada? Uma página é algo sem renderizador? A resposta é Sim, uma representação pode existir sem ser representada. Representar é uma etapa posterior.O que é um renderizador sem uma página? Um renderizador pode renderizar sem uma página? Não. Portanto, uma interface Renderer precisa do
renderPage($page);
método.O que há de errado
$page->renderMe($renderer);
?É o fato de que
renderMe($renderer)
ainda terá que ligar internamente$renderer->renderPage($page);
. Isso viola a Lei de Deméter, que declaraA
Page
turma não se importa se existe umRenderer
no universo. Só se preocupa em ser uma representação de uma página. Portanto, a classe ou interfaceRenderer
nunca deve ser mencionada dentro de aPage
.RESPOSTA ATUALIZADA
Se sua pergunta estiver correta, a
PageSnippet
turma deve se preocupar apenas em ser um trecho de página.PdfRenderer
está preocupado com a renderização.Uso do cliente
Alguns pontos a considerar:
$data
como uma matriz associativa. Deve ser uma instância de uma classe.html
propriedade da$data
matriz é um detalhe específico do seu domínio ePageSnippet
está ciente desses detalhes.fonte
printOn:aStream
método, mas tudo o que faz é dizer ao fluxo para imprimir o objeto. A analogia com a sua resposta é que não há razão para que você não possa ter uma página que possa ser renderizada para um renderizador e um renderizador que possa renderizar uma página, com uma implementação e uma escolha de interfaces convenientes.Page
não é possível conhecer o $ renderer. Eu adicionei algum código na minha pergunta, veja aPageSnippet
aula. É efetivamente uma página, mas não pode existir sem fazer algum tipo de referência à$pdf
, que é de fato um renderizador de PDF de terceiros nesse caso. .. No entanto, suponho que eu possa criar umaPageSnippet
classe que contenha apenas uma matriz de instruções de texto no PDF e que outra classe interprete essas instruções. Dessa forma eu posso evitar a injeção$pdf
emPageSnippet
, em detrimento da complexidade extraIdealmente, você deseja o mínimo de dependências entre classes possível, pois isso reduz a complexidade. Uma classe só deve ter dependência de outra classe se realmente precisar.
O seu estado
Page
contém "um conjunto de instruções para um renderizador de página". Eu imagino algo assim:Assim seria
$page->renderMe($renderer)
, já que a página precisa de uma referência ao renderizador.Mas, alternativamente, as instruções de renderização também podem ser expressas como uma estrutura de dados em vez de chamadas diretas, por exemplo.
Nesse caso, o renderizador real obteria essa estrutura de dados da página e a processaria, executando as instruções de renderização correspondentes. Com essa abordagem, as dependências seriam revertidas - a Página não precisa saber sobre o Renderer, mas o Renderer deve receber uma Página que possa renderizar. Então, opção dois:
$renderer->renderPage($page);
Então, qual é o melhor? A primeira abordagem é provavelmente a mais simples de implementar, enquanto a segunda é muito mais flexível e poderosa, então acho que depende de seus requisitos.
Se você não pode decidir ou acha que pode mudar de abordagem no futuro, pode ocultar a decisão por trás de uma camada de indireção, uma função:
A única abordagem que não recomendarei é
$page->renderMe()
que ela sugere que uma página pode ter apenas um único renderizador. Mas e se você tiver umScreenRenderer
e adicionar umPrintRenderer
? A mesma página pode ser renderizada por ambos.fonte
page
é claramente uma entrada para o renderizador, não uma saída, para essa noção claramente não se encaixa.A parte D do SOLID diz
"Abstrações não devem depender de detalhes. Detalhes devem depender de abstrações."
Então, entre Page e Renderer, que é mais provável que seja uma abstração estável, menos provável de mudar, possivelmente representando uma interface? Por outro lado, qual é o "detalhe"?
Na minha experiência, a abstração é geralmente o Renderer. Por exemplo, pode ser um fluxo simples ou XML, muito abstrato e estável. Ou algum layout bastante padrão. É mais provável que sua página seja um objeto de negócios personalizado, um "detalhe". E você tem outros objetos de negócios a serem renderizados, como "imagens", "relatórios", "gráficos" etc ... (Provavelmente não é um "tríptico", como no meu comentário)
Mas obviamente depende do seu design. A página pode ser abstrata, por exemplo, o equivalente a uma
<article>
tag HTML com subpartes padrão. E você tem vários "renderizadores" de relatórios comerciais personalizados diferentes. Nesse caso, o renderizador deve depender da página.fonte
Acho que a maioria das classes pode ser dividida em uma das duas categorias:
Essas são classes que quase não dependem de mais nada. Eles geralmente fazem parte do seu domínio. Eles não devem conter lógica ou apenas lógica que possa ser derivada diretamente de seu estado. Uma classe Employee pode ter uma função
isAdult
que pode ser derivada diretamente dela,birthDate
mas não uma função,hasBirthDay
pois requer informações externas (a data atual).Esses tipos de classes operam em outras classes que contêm dados. Eles são tipicamente configurados uma vez e imutáveis (para que eles sempre executem o mesmo tipo de função). No entanto, esses tipos de classes ainda fornecem uma instância auxiliar de curta duração para realizar operações mais complexas que exigem a manutenção de algum estado por um curto período (como as classes Builder).
Seu exemplo
No seu exemplo,
Page
seria uma classe contendo dados. Ele deve ter funções para obter esses dados e talvez modificá-los se a classe for mutável. Seja burro, para que possa ser usado sem muitas dependências.Dados ou, neste caso, o seu
Page
podem ser representados de várias maneiras. Pode ser renderizada como uma página da Web, gravada em disco, armazenada em um banco de dados, convertida em JSON, qualquer que seja. Você não deseja adicionar métodos a essa classe para cada um desses casos (e criar dependências em todos os tipos de outras classes, mesmo que sua classe deva conter apenas dados).Seu
Renderer
é uma classe de tipo de serviço típico. Ele pode operar em um determinado conjunto de dados e retornar um resultado. Ele não possui muito estado próprio e, geralmente, é imutável e pode ser configurado uma vez e depois reutilizado.Por exemplo, você poderia ter a
MobileRenderer
e aStandardRenderer
, ambas implementações doRenderer
classe, mas com configurações diferentes.Portanto, como
Page
contém dados e deve ser mudado, a solução mais limpa nesse caso seria passar aPage
paraRenderer
:fonte