Em linguagens orientadas a objetos, quando os objetos devem realizar operações por si mesmos e quando devem ser realizadas operações nos objetos?


11

Suponha que exista uma Pageclasse, que represente um conjunto de instruções para um renderizador de página. E suponha que exista uma Rendererclasse 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 $pdfe com qualquer um $datapara que ele funcione.

Mas, eu posso criar uma PdfRendererclasse 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, $rendererrecebe o PageSnippete todo o $datanecessá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.


2
Infelizmente, você entrou no mundo das "guerras religiosas" do software aqui, na linha de se deve usar espaços ou guias, que adotam o estilo de usar etc. Não há "melhor" aqui, apenas opiniões fortes de ambos os lados. Faça uma pesquisa na Internet sobre os benefícios e as desvantagens dos modelos de domínio rico e anêmico e forme sua própria opinião.
David Arno

7
@DavidArno Use espaços que você pagou ! :)
candied_orange

1
Ha, sério, eu não entendo esse site às vezes. As perguntas perfeitamente boas que obtêm boas respostas são encerradas em nenhum momento como baseadas em opiniões. No entanto, uma pergunta obviamente baseada em opiniões como essa aparece e esses suspeitos comuns não são encontrados. Oh bem, se você não pode vencê-los e tudo o que ... :)
David Arno

@Erik Eidt, você poderia recuperar sua resposta por favor, pois eu a considero uma resposta muito boa para a "quarta opção".
David Arno

1
Além dos princípios do SOLID, você pode dar uma olhada no GRASP , especialmente na parte Especialista . A questão é: quais são as informações para você cumprir a responsabilidade?
download

Respostas:


13

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).


Er, espere um minuto. Ainda posso obter a renderização polimórfica se a página contém uma referência ao renderizador, mas não faz ideia de qual renderizador. Significa apenas que o polimorfismo está um pouco mais abaixo na toca do coelho. Também posso escolher o que passar para o renderizador. Não preciso passar a página inteira.
Candied_orange

@CandiedOrange Esse é um bom argumento, mas gostaria de registrar seu argumento sob o SRP: seria responsabilidade R da página capital decidir como é renderizada, talvez usando algum tipo de estratégia de renderização polimórfica.
amon

Eu imaginei que o $rendereria decidir como renderizar. Quando as $pageconversas com o $rendererque diz é o que render. Não como. O $pagenão tem idéia de como. Isso me causa problemas no SRP?
Candied_orange

Eu realmente não acho que estamos discordando. Eu estava tentando classificar seu primeiro comentário na estrutura conceitual dessa resposta, mas posso ter usado palavras desajeitadas. Você está me lembrando uma coisa que eu não mencionei na resposta: o fluxo de dados do tipo "não pergunte" também é uma boa heurística.
585 amon

Hmm ok. Você está certo. O que eu tenho falado seguiria dizer-não-pergunte. Agora me corrija se eu estiver errado. A outra estratégia, onde o renderizador faz uma referência de página, significa que o renderizador teria que se virar e pedir informações à página, usando os getters de páginas.
Candied_orange

2

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:

class Page
{
    private $data;
    private $renderer;

    public function __construct(ICanRender $renderer, $data)
    {
        $this->renderer = $renderer;
        $this->data = $data;
    }

    public function render()
    {
        $this->renderer->render($this->data);
    }
}

Caso você esteja interessado, David West fala sobre os princípios originais de POO em seu livro, Object Thinking .


1
Para ser franco, quem se importa com o que alguém disse sobre algo a ver com o desenvolvimento de software, há 15 anos, exceto por interesse histórico?
David Arno

1
" Eu me importo com o que um homem que inventou o conceito orientado a objetos disse sobre o que é objeto. " Por quê? Além de induzi-lo a usar as falácias de "apelo à autoridade" em seus argumentos, que relação possível os pensamentos do inventor de um termo podem ter sobre a aplicação do termo 15 anos depois?
David Arno

2
@ Zapadlo: Você não apresenta um argumento sobre o motivo da mensagem ser da Página para o Renderer e não o contrário. Ambos são objetos e, portanto, ambos adultos, certo?
JacquesB

1
"O apelo à falácia da autoridade não pode ser aplicado aqui " ... " Portanto, o conjunto de conceitos que, na sua opinião, representa POO, está realmente errado [porque é uma distorção da definição original] ". Entendo que você não sabe o que é um apelo à falácia da autoridade? Pista: você usou um aqui. :)
David Arno

1
@ David Arno Então, todos os apelos à autoridade estão errados? Você prefere "Apelar à minha opinião?" Toda vez que alguém cita um tio Bobism, você vai reclamar apelo à autoridade Zapadio proporcionou uma fonte bem respeitado Você pode discordar, ou citar conflitantes fontes, mas repeatefly reclamando que alguém tinha fornecido uma citação não é construtivo?..
user949300

2

$page->renderMe();

Aqui nós pagesomos 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 "

$page->renderMe($renderer);

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:

  1. A página simplesmente precisa conhecer as regras de renderização (quais métodos chamar em qual ordem) para criar essa renderização. O encapsulamento é preservado, mas o SRP ainda está quebrado, pois a página ainda precisa supervisionar o processo de renderização, ou
  2. A página chama apenas um método no objeto renderizador, informando seus detalhes. Estamos nos aproximando de respeitar o SRP, mas agora enfraquecemos o encapsulamento.

$renderer->renderPage($page);

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.


Discordo de que a V3 respeita o SRP. O renderizador tem pelo menos dois motivos para alterar: se a página for alterada ou se a maneira como você a renderizar for alterada. E um terceiro, que você cobre, se o Renderer precisar renderizar objetos que não sejam o Pages. Caso contrário, boa análise.
user949300

2

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 declara

Cada unidade deve ter apenas conhecimento limitado sobre outras unidades

A Pageturma não se importa se existe um Rendererno universo. Só se preocupa em ser uma representação de uma página. Portanto, a classe ou interface Renderernunca deve ser mencionada dentro de a Page.


RESPOSTA ATUALIZADA

Se sua pergunta estiver correta, a PageSnippetturma deve se preocupar apenas em ser um trecho de página.

class PageSnippet
{    
    /** string */
    private $html;

    function __construct($data = ['html' => '']): void
    {
        $this->html = $data['html'];
    }

   public function getHtml()
   {
       return $this->html;
   }
}

PdfRenderer está preocupado com a renderização.

class PdfRenderer
{
    /**@var TCPDF */
    protected $pdf;

    function __construct(TCPDF $pdf = new TCPDF())
    {
        $this->pdf = $pdf;
    }

    function runTemplate(string $html): void
    {
        $this->pdf->writeHTML($html);
    }
}

Uso do cliente

$renderer = new PdfRenderer();
$snippet = new PageSnippet(['html' => '<html />']);
$renderer->runTemplate($snippet->getHtml());

Alguns pontos a considerar:

  • É uma má prática passar $datacomo uma matriz associativa. Deve ser uma instância de uma classe.
  • O fato de o formato da página estar contido na htmlpropriedade da $datamatriz é um detalhe específico do seu domínio e PageSnippetestá ciente desses detalhes.

Mas e se, além de Pages, você tiver Imagens, Artigos e Triptichs? No seu esquema, um Renderer precisaria saber sobre todos eles. Isso é muito vazamento. Apenas comida para pensar.
User949300

@ user949300: Bem, se o Renderer precisar renderizar fotos etc., obviamente, ele precisará saber sobre elas.
JacquesB

1
O Smalltalk Best Practice Patterns de Kent Beck introduz o padrão Reversing Method , no qual ambos são suportados. O artigo vinculado mostra que um objeto suporta um printOn:aStreammé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.
Graham Lee

2
Você precisará interromper / falsificar o SRP em qualquer caso, mas se o Renderer precisar saber como renderizar muitas coisas diferentes, isso realmente é "muitas responsabilidades" e, se possível, deve ser evitado.
User949300

1
Gosto da sua resposta, mas sou tentado a pensar que Pagenão é possível conhecer o $ renderer. Eu adicionei algum código na minha pergunta, veja a PageSnippetaula. É 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 uma PageSnippetclasse 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 $pdfem PageSnippet, em detrimento da complexidade extra
Dennis

1

Idealmente, 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 Pagecontém "um conjunto de instruções para um renderizador de página". Eu imagino algo assim:

renderer.renderLine(x, y, w, h, Color.Black)
renderer.renderText(a, b, Font.Helvetica, Color.Black, "bla bla...")
etc...

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.

[
  Line(x, y, w, h, Color.Black), 
  Text(a, b, Font.Helvetica, Color.Black, "bla bla...")
]

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:

renderPage($page, $renderer)

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 um ScreenRenderere adicionar um PrintRenderer? A mesma página pode ser renderizada por ambos.


No contexto do EPUB ou HTML, o conceito de página não existe sem um renderizador.
Mouviciel

1
@mouviciel: Não sei ao certo o que você quer dizer. Certamente você pode ter uma página HTML sem renderizá-la? Por exemplo, o rastreador do Google processa páginas sem renderizá-las.
JacquesB

2
Existe uma noção diferente da página de palavras: o resultado de um processo de paginação quando uma página HTML formatada para ser impressa, talvez seja isso que @mouviciel tinha em mente. No entanto, nesta questão, a pageé claramente uma entrada para o renderizador, não uma saída, para essa noção claramente não se encaixa.
Doc Brown

1

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.


0

Acho que a maioria das classes pode ser dividida em uma das duas categorias:

  • Classes que contêm dados (mutável ou imutável não importa)

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 isAdultque pode ser derivada diretamente dela, birthDatemas não uma função, hasBirthDaypois requer informações externas (a data atual).

  • Classes que prestam serviços

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, Pageseria 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 MobileRenderere a StandardRenderer, ambas implementações doRenderer classe, mas com configurações diferentes.

Portanto, como Pagecontém dados e deve ser mudado, a solução mais limpa nesse caso seria passar a Pagepara Renderer:

$renderer->renderPage($page)

2
Lógica muito processual.
user949300
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.