Qual é a real responsabilidade de uma classe?

40

Eu continuo me perguntando se é legítimo usar verbos baseados em substantivos em OOP.
Eu me deparei com este artigo brilhante , embora eu ainda discorde de o ponto que faz.

Para explicar o problema um pouco mais, o artigo afirma que não deve haver, por exemplo, uma FileWriter de classe, mas como a escrita é uma ação , deve ser um método da classe File . Você vai perceber que muitas vezes é dependente do idioma, já que um programador Ruby provavelmente seria contra o uso de uma classe FileWriter (Ruby usa o método File.open para acessar um arquivo), enquanto um programador Java não o faria.

Meu ponto de vista pessoal (e sim, muito humilde) é que isso quebraria o princípio da Responsabilidade Única. Quando eu programei em PHP (porque o PHP é obviamente a melhor linguagem para OOP, certo?), Eu costumava usar esse tipo de framework:

<?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 de relevante ; mas o ponto é que o princípio da responsabilidade única nos dita que um objeto usado como um modelo contendo dados (pode ser chamado de registro) também não deveria ter a responsabilidade de fazer consultas SQL e acesso à base de dados. Isso de alguma forma invalida o padrão ActionRecord usado, por exemplo, pelo Ruby on Rails.

Me deparei com esse código C # (yay, quarta linguagem de objeto usada neste post) no outro dia:

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

E devo dizer que não faz muito sentido para mim que uma classe Encoding ou Charset realmente codifique strings. Deve ser apenas uma representação do que realmente é uma codificação.

Assim, eu tenderia a pensar que:

  • Não é responsabilidade da classe File abrir, ler ou salvar arquivos.
  • Não é responsabilidade da classe Xml serializar a si mesma.
  • Não é responsabilidade da classe User consultar um banco de dados.
  • etc.

No entanto, se extrapolarmos essas ideias, por que Object teria uma classe toString ? Não é responsabilidade de um carro ou de um cão se converter em uma string, agora é?

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

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

Qual é a responsabilidade de uma turma?

    
por Pierre Arlaud 04.12.2013 / 10:31
fonte

8 respostas

22

Dadas algumas divergências entre idiomas, isso pode ser um assunto complicado. Assim, estou formulando os seguintes comentários de uma forma que tenta ser o mais abrangente possível dentro do reino da OO.

Em primeiro lugar, o chamado "Princípio da Responsabilidade Única" é um reflexo - explicitamente declarado - do conceito coesão . 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í-las de maneira a preservar boas propriedades. Então, eles diriam "aqui está um monte de estruturas e procedimentos, eu vou fazer um módulo deles", mas sem nenhum critério de por que esse conjunto de coisas arbitrárias é empacotado juntos, a organização pode acabar fazendo pouco sentido - pouca "coesão". Assim, surgiram discussões sobre critérios.

Então, a primeira coisa a notar aqui é que, até agora, o debate está em torno da organização e dos efeitos relacionados à manutenção e à compreensibilidade (por um pequeno assunto a um computador, se um módulo "fizer sentido").

Então, outra pessoa (Sr. Martin) entrou e aplicou o mesmo pensamento à unidade de uma classe como um critério para usar quando pensando sobre o que deveria ou não pertencer a ela, promovendo este critério a um princípio, o um sendo discutido aqui. O ponto que ele fez foi que "Uma turma deve 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 inchadas com a funcionalidade ao ponto de serem impenetráveis à manutenção, etc. E para entender o último é ver onde o sr. Martin estava apontando quando ele elaborou sobre o assunto.

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

However, if we extrapolate these ideas, why would Object have a toString class? It's not a Car's or a Dog's responsibility to convert itself to a string, now is it?

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

Em um mundo sem "métodos substitutos", escolher entre "obj.toString ()" ou "toString (obj)" é apenas uma questão de preferência de sintaxe. No entanto, em um mundo onde os programadores podem alterar o comportamento de um programa adicionando uma subclasse com implementação distinta de um método existente / substituído, essa escolha não é mais de gosto: fazer um procedimento também pode torná-lo um candidato a substituir, e o mesmo pode não ser verdade para "procedimentos livres" (linguagens que apóiam multi-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. Finalmente, para qual classe o método está vinculado, também se torna uma decisão impactante (e em muitos casos, até agora, temos pouco mais do que diretrizes para nos ajudar a decidir onde as coisas pertencem, já que compensações não óbvias emergem de escolhas diferentes). .

Finalmente, nos deparamos com linguagens que carregam decisões de design terríveis, por exemplo, forçando alguém a criar uma classe para cada pedacinho de coisa. Assim, o que antes era a razão canônica e os principais critérios para ter objetos (e na classe-terra, portanto, classes), isto é, ter esses "objetos" que são tipos de "comportamentos que também se comportam como dados". , mas proteger sua representação concreta (se houver) da manipulação direta a todo custo (e essa é a principal dica para o que deveria ser a interface de um objeto, do ponto de vista de seus clientes), fica borrada e confusa.

    
por 04.12.2013 / 19:17
fonte
30

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

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

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

No entanto, faz 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 que os outros. Depende do contexto.

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

Exceto que não são como as contas bancárias são modeladas em software bancário do mundo real (e, na verdade, não é como os bancos funcionam no mundo real). Em vez disso, você tem um recibo de transação e o saldo da conta é calculado adicionando (e subtraindo) todos os recibos de transação de uma conta. Em outras palavras: a transferência é data e o saldo é uma operação ! (Curiosamente, isso também torna seu sistema puramente funcional, já que você nunca precisa modificar o equilíbrio, tanto os objetos de sua conta quanto os objetos de transação nunca precisam mudar, eles são imutáveis.)

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

    
por 04.12.2013 / 11:23
fonte
8

Muitas vezes, 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 setters e getters), o que leva a um desastre do Kingdom of the Nouns .

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

Em seu exemplo com Codificação, IMO, definitivamente deve ser capaz de codificar. O que você acha que deveria fazer em vez disso? Apenas ter um nome "utf" é de responsabilidade zero. Agora, talvez o nome seja Encoder. Mas, como Konrad disse, os dados (que codificam) e o comportamento (fazendo isso) pertencem juntos .

    
por 07.12.2013 / 01:49
fonte
7

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

Se você quiser escrever algo para gravar em um arquivo, pense em todas as coisas que precisa especificar (no sistema operacional): nome do arquivo, método de acesso (ler, escrever, anexar), a string que você 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, ele provavelmente o terá como valor readonly / const). A instância File agora 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 ao identificador de arquivo que foi criado a partir dele).

Uma instância da classe File, portanto, coloca 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, você não precisa se preocupar com o nome do arquivo, você só está preocupado com qual string você colocou como argumento na chamada de escrita. Para reiterar - o objeto File encapsula tudo o que você não precisa saber sobre onde a string que você deseja gravar está realmente indo.

A maioria dos problemas que você quer resolver são dispostos dessa maneira - coisas criadas na frente, coisas criadas no início de cada iteração de algum tipo de loop de processo, então coisas diferentes são declaradas nas duas metades de um if else, então coisas criadas no início de algum tipo de sub loop, então as coisas são declaradas como parte do algoritmo dentro desse loop, etc. Ao adicionar mais e mais chamadas de função à pilha, você está fazendo um código cada vez mais refinado, mas toda vez que Você terá encaixado os dados nas camadas inferiores da pilha em algum tipo de abstração agradável que facilite o acesso. Veja o que você está fazendo usando "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 o estado mutável de objetos, o que não é tão funcional - você está manipulando os fechamentos a partir do exterior. Você está fazendo algumas das coisas da sua classe "globais", pelo menos para o escopo acima. Toda vez que você escreve um setter - tente evitá-lo. Eu diria que é aqui que você tem a responsabilidade da sua classe, ou das outras classes em que opera, errado. Mas não se preocupe muito - às vezes é pragmático mexer um pouco no estado mutável para resolver rapidamente certos tipos de problemas, sem muita carga cognitiva, e realmente fazer qualquer coisa.

Então, em resumo, para responder à sua pergunta - qual é a real responsabilidade de uma aula? É 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 curry ...). Por exemplo. filename vs string para escrever.

Muitas vezes, esses encapsulamentos parecem superficialmente com objetos do mundo real / um objeto no domínio do problema, mas não se deixem enganar. Quando você cria uma classe Car, ela não é na verdade um carro, é uma coleção de dados que forma uma plataforma na qual você pode conseguir certos tipos de coisas que você pode querer fazer em um carro. Formar uma representação de string (toString) é uma dessas coisas - cuspir todos os dados internos. Lembre-se de que, às vezes, uma classe Car pode não ser a classe certa, mesmo que o domínio do problema seja todo sobre carros. Em contraste com o Reino dos substantivos, são as operações, os verbos em que uma classe deve se basear.

    
por 06.12.2013 / 09:48
fonte
4

I also understand that there may not be an exact answer (which would more be an essay than a serious answer) to this, or that it may be opinion-based. Nevertheless I would still like to know if my approach actually follows what the single-responsibility principle really is.

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

Os objetos são um conjunto de conjuntos de dados e operações coesos (e às vezes apenas um dos dois). Enquanto estes frequentemente modelam objetos do mundo real, a diferença entre um modelo computacional de alguma coisa e aquela coisa em si requer diferenças .

Ao tomar essa linha dura entre um "substantivo" que representa uma coisa e outras coisas que os consomem, você está indo contra um benefício-chave da programação orientada a objeto (ter função e estado juntos para que a função possa proteger invariantes de o Estado). Pior, você está tentando representar o mundo físico no código, o que, como a história mostrou, não funcionará bem.

    
por 04.12.2013 / 18:29
fonte
4

I came across this C# code (yay, fourth object language used in this post) just the other day:

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

And I gotta say that it doesn't make much sense to me that an Encoding or Charset class actually encodes strings. It should merely be a representation of what an encoding really is.

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

Se Encoding apenas representasse (ou identificasse) determinada codificação - digamos, UTF-8 - então obviamente você precisaria de uma classe Encoder , para que você possa implementar GetBytes nela - mas então você precisa gerenciar a relação entre Encoder s e Encoding s, então acabamos com o nosso bom e velho

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

tão brilhantemente satirizada nesse artigo que você vinculou (ótima leitura, a propósito).

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

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

HelpfulStuff.GetUTF8Bytes(myString)

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

Procuro em HelpfulStuff , Utils , StringHelper ?

Senhor, ajude-me se o método é realmente implementado de forma independente em todos os três ... o que acontece muito, tudo o que acontece é que o sujeito antes de mim também não sabia onde procurar essa função, e acreditava que não havia ainda assim, eles quebraram outro e agora os temos em abundância.

Para mim, o design adequado não é sobre a satisfação de critérios abstratos, mas sobre a minha facilidade de continuar depois de sentar na frente do seu código. Agora como é que

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

taxa neste aspecto? Mal também. Não é uma resposta que quero ouvir quando grito ao meu colega de trabalho "como faço para codificar uma string em uma seqüência de bytes?" :)

Então eu vou com uma abordagem moderada e ser liberal sobre responsabilidade única. Prefiro ter a classe Encoding identificando um tipo de codificação e fazendo 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. Este padrão legítimo viola o SOLID? Uma questão relacionada:

Observe que isso pode ser o modo como a obtenção de bytes codificados é implementada internamente (em bibliotecas .NET). As classes não são visíveis publicamente e somente essa API "fachada" é exposta do lado de fora. Não é tão difícil de verificar se isso é verdade, eu sou apenas um pouco preguiçoso para fazer isso agora :) Provavelmente não é, mas isso não invalida o meu ponto.

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

    
por 05.12.2013 / 11:59
fonte
1

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

It's not a Car's or a Dog's responsibility to convert itself to a string, now is it?

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

Para responder diretamente à questão maior, a responsabilidade de uma classe é agir como uma abstração. Esse trabalho pode ser complexo, mas isso é o que importa, 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 totalmente o conceito, então que assim seja.

    
por 04.12.2013 / 17:36
fonte
0

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

Se você estiver aplicando o princípio da responsabilidade única em toda a sua extensão, obterá a responsabilidade design centralizado onde basicamente todo método não-auxiliar pertence à sua própria classe.

Acho que não há nada errado em extrair uma complexidade de uma classe base como no caso de File e FileWriter. A vantagem é que você obtém separação clara de código que é responsável por uma determinada operação e também que você pode substituir (subclasse) apenas esse pedaço 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 recebe muito mais classes para manipular e para cada operação você precisa invocar sua respectiva classe. É a flexibilidade que tem algum custo.

Estas classes extraídas devem ter nomes muito descritivos com base na operação que contêm, por ex. <X>Writer , <X>Reader , <X>Builder . Nomes como <X>Manager , <X>Handler não descrevem a operação e se eles são chamados assim porque contêm mais de uma operação, então não está claro o que você conseguiu. Você separou a funcionalidade de seus dados, você ainda quebra o princípio de responsabilidade única mesmo naquela classe extraída e, se estiver procurando um determinado método, talvez não saiba onde procurá-lo (se estiver em <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 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 há basicamente apenas funcionalidade 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 em o que é e não no que ela faz. Você pode dar a ele um nome como UserRepository, que é bem simples e também sugere o uso do padrão de repositório .

    
por 08.12.2013 / 19:30
fonte