Qual é a real responsabilidade de uma classe?


42

Fico me perguntando se é legítimo usar verbos baseados em substantivos em OOP.
Me deparei com este artigo brilhante , embora ainda discorde do que afirma.

Para explicar um pouco mais o problema, o artigo afirma que não deveria haver, por exemplo, uma FileWriterclasse, mas como escrever é uma ação , deve ser um método da classe File. Você perceberá que muitas vezes depende da linguagem, já que um programador Ruby provavelmente seria contra o uso de uma FileWriterclasse (o Ruby usa o método File.openpara acessar um arquivo), enquanto um programador Java não.

Meu ponto de vista pessoal (e sim, muito humilde) é que isso violaria o princípio da responsabilidade única. Quando programava em PHP (porque o PHP é obviamente a melhor linguagem para OOP, certo?), Usava frequentemente esse tipo de estrutura:

<?php

// This is just an example that I just made on the fly, may contain errors

class User extends Record {

    protected $name;

    public function __construct($name) {
        $this->name = $name;
    }

}

class UserDataHandler extends DataHandler /* knows the pdo object */ {

    public function find($id) {
         $query = $this->db->prepare('SELECT ' . $this->getFields . ' FROM users WHERE id = :id');
         $query->bindParam(':id', $id, PDO::PARAM_INT);
         $query->setFetchMode( PDO::FETCH_CLASS, 'user');
         $query->execute();
         return $query->fetch( PDO::FETCH_CLASS );
    }


}

?>

Entendo que o sufixo DataHandler não adiciona nada relevante ; mas o ponto é que o princípio de responsabilidade única nos determina que um objeto usado como modelo contendo dados (que pode ser chamado de Registro) também não deve ter a responsabilidade de fazer consultas SQL e acesso ao DataBase. De alguma forma, isso invalida o padrão ActionRecord usado, por exemplo, pelo Ruby on Rails.

Me deparei com esse código C # (sim, a quarta linguagem de objetos usada neste post) outro dia:

byte[] bytes = Encoding.Default.GetBytes(myString);
myString = Encoding.UTF8.GetString(bytes);

E eu tenho que dizer que não faz muito sentido para mim que uma classe Encodingou Charsetrealmente codifique strings. Deve ser apenas uma representação do que realmente é uma codificação.

Assim, eu tenderia a pensar que:

  • Não é Fileresponsabilidade da classe abrir, ler ou salvar arquivos.
  • Não é Xmlresponsabilidade da classe serializar-se.
  • Não é Userresponsabilidade da classe consultar um banco de dados.
  • etc.

No entanto, se extrapolarmos essas idéias, por que Objectuma toStringaula? Não é responsabilidade de um carro ou de um cachorro se converter em uma corda, agora é?

Entendo que, de um ponto de vista pragmático, livrar-se do toStringmétodo pela beleza de seguir uma forma rígida do SOLID, que torna o código mais sustentável, tornando-o inútil, não é uma opção aceitável.

Entendo também que pode não haver uma resposta exata (que seria mais um ensaio do que uma resposta séria) a isso, ou que pode ser baseada em opiniões. Ainda assim, eu ainda gostaria de saber se minha abordagem realmente segue o que realmente é o princípio de responsabilidade única.

Qual é a responsabilidade de uma classe?


Objectos toString é principalmente conveniência, e toString é geralmente usado para visualizar rapidamente o estado interno durante a depuração
roquete aberração

3
@ratchetfreak Eu concordo, mas foi um exemplo (quase uma piada) mostrar que a responsabilidade única é muitas vezes quebrada da maneira que descrevi.
Pierre Arlaud 4/13/13

8
Embora o princípio de responsabilidade única tenha seus fervorosos defensores, na minha opinião, é uma diretriz, na melhor das hipóteses, e muito frouxa para a esmagadora maioria das opções de design. O problema com o SRP é que você acaba com centenas de classes com nomes estranhos, tornando muito difícil encontrar o que você precisa (ou descobrir se o que você precisa já existe) e entender o cenário geral. Eu prefiro ter menos classes bem nomeadas que me dizem instantaneamente o que esperar delas, mesmo que tenham algumas responsabilidades. Se no futuro as coisas mudarem, eu dividirei a classe.
quer

Respostas:


24

Dadas algumas divergências entre os idiomas, este pode ser um tópico complicado. Assim, estou formulando os seguintes comentários de uma maneira que tente ser o mais abrangente possível dentro do domínio da OO.

Em primeiro lugar, o chamado "princípio da responsabilidade única" é um reflexo - declarado explicitamente - da coesão do conceito . Lendo a literatura da época (por volta de 70), as pessoas estavam (e ainda estão) lutando para definir o que é um módulo e como construí-lo de maneira a preservar boas propriedades. Então, eles diriam "aqui estão algumas estruturas e procedimentos, eu farei um módulo com eles", mas sem nenhum critério sobre por que esse conjunto de coisas arbitrárias é empacotado, a organização pode acabar fazendo pouco sentido - pouca "coesão". Assim, surgiram discussões sobre critérios.

Portanto, a primeira coisa a ser notada aqui é que, até agora, o debate é sobre a organização e os efeitos relacionados à manutenção e à compreensibilidade (pouco importa para o computador se um módulo "faz sentido").

Então, outra pessoa (Sr. Martin) entrou e aplicou o mesmo pensamento à unidade de uma classe como um critério a ser usado ao pensar sobre o que deveria ou não pertencer a ela, promovendo esse critério a um princípio, o que está sendo discutido. Aqui. Ele argumentou que "uma classe deveria ter apenas um motivo para mudar" .

Bem, sabemos por experiência que muitos objetos (e muitas classes) que parecem fazer "muitas coisas" têm uma boa razão para fazê-lo. O caso indesejável seria as classes que estão cheias de funcionalidade, a ponto de serem impenetráveis ​​para manutenção, etc. E entender o último é ver onde o sr. Martin estava mirando quando ele falou sobre o assunto.

Claro, depois de ler o que o sr. Martin escreveu, deve ficar claro que esses são critérios de direção e design para evitar cenários problemáticos, de forma alguma para perseguir qualquer tipo de conformidade, sem falar em conformidade forte, especialmente quando "responsabilidade" está mal definida (e perguntas como "fazem isso" viola o princípio? "são exemplos perfeitos da confusão generalizada). Assim, acho lamentável que seja chamado de princípio, enganando as pessoas a tentar levá-lo até as últimas consequências, onde isso não faria nenhum bem. O próprio Martin discutiu projetos que "fazem mais de uma coisa" que provavelmente deveriam ser mantidos assim, pois a separação traria resultados piores. Além disso, existem muitos desafios conhecidos em relação à modularidade (e esse é o caso), não estamos no ponto de ter boas respostas, mesmo para algumas perguntas simples sobre o assunto.

No entanto, se extrapolarmos essas idéias, por que Object teria uma classe toString? Não é responsabilidade de um carro ou de um cachorro se converter em uma corda, agora é?

Agora, deixe-me fazer uma pausa para dizer algo aqui toString: há uma coisa fundamental geralmente negligenciada quando se faz essa transição de pensamento de módulos para classes e reflete sobre quais métodos devem pertencer a uma classe. E a coisa é despacho dinâmico (aka, ligação tardia, "polimorfismo").

Em um mundo sem "métodos de substituição", escolher entre "obj.toString ()" ou "toString (obj)" é apenas uma questão de preferência de sintaxe. No entanto, em um mundo em que os programadores podem alterar o comportamento de um programa adicionando uma subclasse com implementação distinta de um método existente / substituído, essa opção não é mais agradável: tornar um procedimento um método também pode torná-lo um candidato à substituição, e o mesmo pode não ser verdade para "procedimentos gratuitos" (idiomas que suportam vários métodos têm uma saída para essa dicotomia). Consequentemente, não é mais uma discussão apenas sobre organização, mas também sobre semântica. Por fim, a qual classe o método está vinculado também se torna uma decisão impactante (e, em muitos casos, até o momento, temos pouco mais do que diretrizes para nos ajudar a decidir aonde as coisas pertencem,

Finalmente, somos confrontados com linguagens que carregam decisões terríveis de design, por exemplo, forçando uma a criar uma classe para cada pequeno detalhe. Assim, qual era a razão canônica e os principais critérios para ter objetos (e na classe, portanto, classes), ou seja, ter esses "objetos" que são uma espécie de "comportamentos que também se comportam como dados" , mas proteja sua representação concreta (se houver) da manipulação direta a todo custo (e essa é a principal dica para qual deve ser a interface de um objeto, do ponto de vista de seus clientes), fica confusa.


31

[Nota: eu vou falar sobre objetos aqui. Objetos é o que é a programação orientada a objetos, afinal, não classes.]

Qual é a responsabilidade de um objeto depende principalmente do seu modelo de domínio. Geralmente, existem muitas maneiras de modelar o mesmo domínio e você escolhe uma forma ou outra com base em como o sistema será usado.

Como todos sabemos, a metodologia "verbo / substantivo", frequentemente ensinada nos cursos introdutórios, é ridícula, porque depende muito de como você formula uma frase. Você pode expressar quase tudo como um substantivo ou um verbo, em uma voz ativa ou passiva. Tendo seu modelo de domínio dependente do que está errado, deve ser uma escolha consciente do design se algo é um objeto ou um método, não apenas uma consequência acidental de como você formula os requisitos.

No entanto, ele não mostrar que, assim como você tem muitas possibilidades de expressar a mesma coisa em Inglês, você tem muitas possibilidades de expressar a mesma coisa em seu modelo de domínio ... e nenhuma dessas possibilidades é inerentemente mais correto do que os outros. Depende do contexto.

Aqui está um exemplo que também é muito popular nos cursos introdutórios de OO: uma conta bancária. Que normalmente é modelado como um objeto com um balancecampo e um transfermétodo. Em outras palavras: o saldo da conta são dados e uma transferência é uma operação . Qual é uma maneira razoável de modelar uma conta bancária.

Exceto que não é assim que as contas bancárias são modeladas no software bancário do mundo real (e, de fato, não é assim que os bancos funcionam no mundo real). Em vez disso, você possui uma guia de transação e o saldo da conta é calculado adicionando (e subtraindo) todas as guias de transação de uma conta. Em outras palavras: a transferência é de dados e a balança é uma operação ! (Interessante, isso também torna seu sistema puramente funcional, já que você nunca precisa modificar o saldo, os objetos de sua conta e os objetos de transação nunca precisam mudar, eles são imutáveis.)

Quanto à sua pergunta específica sobre toString, eu concordo. Eu prefiro muito a solução Haskell de uma Showable classe de tipo . (Scala nos ensina que as classes de tipo se encaixam perfeitamente com o OO.) O mesmo com a igualdade. A igualdade geralmente não é uma propriedade de um objeto, mas do contexto em que o objeto é usado. Pense nos números de ponto flutuante: qual deve ser o epsilon? Novamente, Haskell tem a Eqclasse de tipo.


Eu gosto das suas comparações de haskell, isso torna a resposta um pouco mais ... nova. O que você diria sobre os projetos que permitem ActionRecordser um modelo e um solicitante de banco de dados? Não importa o contexto, parece estar quebrando o princípio de responsabilidade única.
Pierre Arlaud 4/13/13

5
"... porque depende muito de como você formula uma sentença." Yay! Ah, e eu amo o seu exemplo bancário. Nunca soube que, nos anos 80, eu já tinha aprendido programação funcional :) (design centrado em dados e armazenamento independente do tempo, onde saldos etc. são computáveis ​​e não devem ser armazenados, e o endereço de alguém não deve ser alterado, mas armazenado como um fato novo) ...
Marjan Venema

1
Eu não diria que a metodologia Verbo / Substantivo é "ridícula". Eu diria que os projetos simplistas falharão e que é muito difícil acertar. Porque, por exemplo, uma API RESTful não é uma metodologia de verbos / substantivos? Onde, para um grau extra de dificuldade, os verbos são extremamente limitados?
user949300

@ user949300: O fato de o conjunto de verbos ser corrigido evita precisamente o problema que mencionei: você pode formular sentenças de várias maneiras diferentes, tornando o que é um substantivo e o que é um verbo dependentes do estilo de escrita e não da análise de domínio .
Jörg W Mittag 30/03

8

Com muita frequência, o Princípio de Responsabilidade Única se torna um Princípio de Responsabilidade Zero, e você acaba com Classes Anêmicas que não fazem nada (exceto levantadores e recebedores), o que leva a um desastre do Reino dos Substantivos .

Você nunca ficará perfeito, mas é melhor para uma classe fazer um pouco do que muito pouco.

No seu exemplo com Codificação, IMO, ele definitivamente deve ser capaz de codificar. O que você acha que deve fazer? Ter um nome "utf" é de responsabilidade zero. Agora, talvez o nome deva ser Encoder. Mas, como disse Konrad, dados (que codificação) e comportamento (fazendo isso) estão juntos .


7

Uma instância de uma classe é um fechamento. É isso aí. Se você pensa assim, todos os softwares bem projetados que você vê parecerão corretos, e todos os softwares mal pensados ​​não. Deixe-me expandir.

Se você deseja escrever algo para gravar em um arquivo, pense em todas as coisas que você precisa especificar (para o sistema operacional): nome do arquivo, método de acesso (leitura, gravação, anexação), a sequência que deseja escrever.

Então você constrói um objeto File com o nome do arquivo (e o método de acesso). O objeto File agora está fechado sobre o nome do arquivo (nesse caso, provavelmente o terá como um valor somente leitura / const). A instância do arquivo está pronta para receber chamadas para o método "write" definido na classe. Isso leva apenas um argumento de string, mas no corpo do método write, a implementação, também há acesso ao nome do arquivo (ou identificador de arquivo que foi criado a partir dele).

Uma instância da classe File, portanto, encaixa alguns dados em algum tipo de blob composto, para que o uso posterior dessa instância seja mais simples. Quando você tem um objeto File, não precisa se preocupar com o nome do arquivo, apenas se preocupa com a string que você coloca como argumento na chamada de gravação. Para reiterar - o objeto File encapsula tudo o que você não precisa saber sobre para onde está indo realmente a string que deseja escrever.

A maioria dos problemas que você deseja resolver são agrupados dessa maneira - coisas criadas no início, coisas criadas no início de cada iteração de algum tipo de loop de processo, depois coisas diferentes declaradas nas duas metades de um if else, então coisas criadas em o início de algum tipo de sub-loop, depois as coisas declaradas como parte do algoritmo dentro desse loop, etc. À medida que você adiciona mais e mais chamadas de função à pilha, você está fazendo um código cada vez mais refinado, mas cada vez você terá agrupou os dados nas camadas inferiores da pilha em algum tipo de abstração agradável que facilita o acesso. Veja o que você está fazendo é usar "OO" como um tipo de estrutura de gerenciamento de fechamento para uma abordagem de programação funcional.

A solução de atalho para alguns problemas envolve um estado mutável de objetos, que não é tão funcional - você está manipulando os fechamentos de fora. Você está tornando algumas das coisas da sua classe "globais", pelo menos no escopo acima. Toda vez que você escreve um levantador, tente evitá-los. Eu diria que é aqui que você tem a responsabilidade de sua classe, ou das outras classes em que ela opera, incorreta. Mas não se preocupe demais - às vezes é pragmático agitar um pouco de estado mutável para resolver certos tipos de problemas rapidamente, sem muita carga cognitiva e realmente fazer qualquer coisa.

Então, em resumo, para responder à sua pergunta - qual é a real responsabilidade de uma classe? É apresentar um tipo de plataforma, com base em alguns dados subjacentes, para obter um tipo de operação mais detalhado. É encapsular metade dos dados envolvidos em uma operação que muda com menos frequência do que a outra metade dos dados (pense em currying ...). Por exemplo, nome do arquivo vs string para escrever.

Frequentemente, esses encapsulamentos parecem superficialmente objetos do mundo real / objetos no domínio do problema, mas não se deixe enganar. Quando você cria uma classe Car, na verdade não é um carro, é uma coleção de dados que forma uma plataforma na qual é possível alcançar certos tipos de coisas que você pode querer fazer em um carro. Formar uma representação de string (toString) é uma dessas coisas - cuspir todos esses dados internos. Lembre-se de que algumas vezes uma classe de carro pode não ser a classe certa, mesmo que o domínio do problema seja sobre carros. Ao contrário do Reino dos substantivos, são as operações, os verbos em que uma classe deve se basear.


4

Entendo também que pode não haver uma resposta exata (que seria mais um ensaio do que uma resposta séria) a isso, ou que pode ser baseada em opiniões. No entanto, ainda gostaria de saber se minha abordagem realmente segue o que realmente é o princípio da responsabilidade única.

Enquanto isso acontece, pode não ser necessariamente um bom código. Além do simples fato de que qualquer tipo de regra seguida às cegas leva a códigos incorretos, você começa com uma premissa inválida: objetos (de programação) não são objetos (físicos).

Objetos são um conjunto de pacotes coesos de dados e operações (e às vezes apenas um dos dois). Embora esses modelos frequentemente modelem objetos do mundo real, a diferença entre um modelo de computador de algo e aquilo em si exige diferenças.

Ao colocar essa linha dura entre um "substantivo" que representa uma coisa e outras que as consomem, você contraria um benefício importante da programação orientada a objetos (tendo função e estado juntos para que a função possa proteger os invariantes do estado) . Pior, você está tentando representar o mundo físico em código, que, como a história mostrou, não funcionará bem.


4

Me deparei com esse código C # (sim, a quarta linguagem de objetos usada neste post) outro dia:

byte[] bytes = Encoding.Default.GetBytes(myString);
myString = Encoding.UTF8.GetString(bytes);

E eu tenho que dizer que não faz muito sentido para mim que uma classe Encodingou Charsetrealmente codifique strings. Deve ser apenas uma representação do que realmente é uma codificação.

Em teoria, sim, mas acho que o C # faz um compromisso justificado por uma questão de simplicidade (em oposição à abordagem mais rigorosa do Java).

Se Encodingrepresentou apenas (ou identificado) determinada codificação - por exemplo, UTF-8 - então você obviamente precisa de uma Encoderclasse, também, para que você possa implementar GetBytesnele - mas então você precisa para gerenciar a relação entre Encoders e Encodings, por isso, acabar com o nosso bom e velho

EncodersFactory.getDefaultFactory().createEncoder(new UTF8Encoding()).getBytes(myString)

tão brilhantemente enganado naquele artigo ao qual você vinculou (ótima leitura, a propósito).

Se eu te entendo bem, você está perguntando onde está a linha.

Bem, no lado oposto do espectro está a abordagem processual, difícil de implementar nas linguagens OOP com o uso de classes estáticas:

HelpfulStuff.GetUTF8Bytes(myString)

O grande problema é que, quando o autor de HelpfulStufffolhas e eu estou sentado na frente do monitor, não tenho como saber onde GetUTF8Bytesé implementado.

Eu olhar para ele em HelpfulStuff, Utils, StringHelper?

Deus me ajude se o método for realmente implementado independentemente em todos os três ... o que acontece muito, basta que o cara diante de mim também não soubesse onde procurar essa função e acreditasse que não havia ainda assim eles pegaram outro e agora nós os temos em abundância.

Para mim, o design adequado não é satisfazer critérios abstratos, mas sim a facilidade de continuar depois de me sentar na frente do seu código. Agora como

EncodersFactory.getDefaultFactory().createEncoder(new UTF8Encoding()).getBytes(myString)

taxa neste aspecto? Mal também. Não é uma resposta que quero ouvir quando grito para meu colega de trabalho "como codifico uma sequência em uma sequência de bytes?" :)

Então, eu adotaria uma abordagem moderada e seria liberal em relação à responsabilidade única. Prefiro que a Encodingclasse identifique um tipo de codificação e faça a codificação real: dados não separados do comportamento.

Eu acho que passa como um padrão de fachada, o que ajuda a lubrificar as engrenagens. Esse padrão legítimo viola o SOLID? Uma questão relacionada: o padrão Facade viola o SRP?

Observe que é assim que os bytes codificados são implementados internamente (nas bibliotecas .NET). As classes simplesmente não são publicamente visíveis e apenas essa API "fachada" é exposta do lado de fora. Não é tão difícil de verificar se isso é verdade, estou com preguiça de fazê-lo agora :) Provavelmente não, mas isso não invalida meu argumento.

Você pode optar por simplificar a implementação publicamente visível por uma questão de facilidade, mantendo uma implementação canônica mais severa internamente.


1

Em Java, a abstração usada para representar arquivos é um fluxo, alguns fluxos são unidirecionais (somente leitura ou gravação), outros são bidirecionais. Para Ruby, a classe File é a abstração que representa os arquivos do SO. Então a questão passa a ser qual é a sua responsabilidade única. Em Java, a responsabilidade do FileWriter é fornecer um fluxo unidirecional de bytes conectado a um arquivo do SO. No Ruby, a responsabilidade do arquivo é fornecer acesso bidirecional a um arquivo do sistema. Ambos cumprem o SRP, apenas têm responsabilidades diferentes.

Não é responsabilidade de um carro ou de um cachorro se converter em uma corda, agora é?

Claro, por que não? Espero que a classe Car represente totalmente uma abstração de carro. Se faz sentido que a abstração do carro seja usada onde uma string é necessária, espero que a classe Car suporte a conversão em uma string.

Responder diretamente à pergunta maior a responsabilidade de uma classe é agir como uma abstração. Esse trabalho pode ser complexo, mas esse é o ponto, uma abstração deve esconder coisas complexas por trás de uma interface mais fácil de usar e menos complexa. O SRP é uma diretriz de design, para que você se concentre na pergunta "o que essa abstração representa?". Se vários comportamentos diferentes são necessários para abstrair completamente o conceito, que assim seja.


0

A classe pode ter várias responsabilidades. Pelo menos na OOP clássica centrada em dados.

Se você estiver aplicando o princípio de responsabilidade única em toda sua extensão, obterá um design centrado na responsabilidade, onde basicamente todos os métodos não auxiliares pertencem à sua própria classe.

Eu acho que não há nada errado em extrair um pouco de complexidade de uma classe base, como no caso de File e FileWriter. A vantagem é que você obtém uma separação clara do código responsável por uma determinada operação e também pode substituir (subclasse) apenas esse trecho de código em vez de substituir toda a classe base (por exemplo, Arquivo). No entanto, aplicar isso sistematicamente parece ser um exagero para mim. Você simplesmente obtém muito mais classes para manipular e, para cada operação, precisa invocar sua respectiva classe. É a flexibilidade que tem algum custo.

Essas classes extraídos devem ter nomes muito descritivo com base na operação que eles contêm, por exemplo <X>Writer, <X>Reader, <X>Builder. Nomes como <X>Manager, <X>Handlernão descrevem a operação e, se são chamados assim porque contêm mais de uma operação, não está claro o que você conseguiu. Você separou a funcionalidade de seus dados, e ainda quebra o princípio de responsabilidade única, mesmo nessa classe extraída, e se estiver procurando por um determinado método, poderá não saber onde procurá-lo (se estiver dentro <X>ou <X>Manager).

Agora, seu caso com UserDataHandler é diferente porque não há classe base (a classe que contém dados reais). Os dados para esta classe são armazenados em um recurso externo (banco de dados) e você está usando uma API para acessá-los e manipulá-los. Sua classe User representa um usuário em tempo de execução, que é realmente algo diferente de um usuário persistente e a separação de responsabilidades (preocupações) pode ser útil aqui, especialmente se você tiver muita lógica relacionada ao usuário em tempo de execução.

Você provavelmente nomeou UserDataHandler assim porque basicamente existem apenas funcionalidades nessa classe e nenhum dado real. No entanto, você também pode abstrair esse fato e considerar os dados no banco de dados como pertencentes à classe. Com essa abstração, você pode nomear a classe com base no que é e não no que faz. Você pode atribuir um nome como UserRepository, que é bastante liso e também sugere o uso do padrão de repositório .

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.