As classes com apenas um único método (público) são um problema?

51

Atualmente, estou trabalhando em um projeto de software que realiza compactação e indexação em imagens de vigilância por vídeo. A compactação funciona dividindo objetos em segundo plano e em primeiro plano, salvando o plano de fundo como uma imagem estática e o primeiro plano como um sprite.

Recentemente, comecei a revisar algumas das classes que criei para o projeto.

Percebi que existem muitas classes que possuem apenas um único método público. Algumas dessas classes são:

  • VideoCompressor (com um compressmétodo que captura um vídeo de entrada do tipo RawVideoe retorna um vídeo de saída do tipo CompressedVideo).
  • VideoSplitter (com um splitmétodo que capta um vídeo de entrada do tipo RawVideoe retorna um vetor de 2 vídeos de saída, cada um do tipo RawVideo).
  • VideoIndexer (com um indexmétodo que recebe um vídeo de entrada do tipo RawVideoe retorna um índice de vídeo do tipo VideoIndex).

Encontro-me instanciar cada classe apenas para fazer chamadas como VideoCompressor.compress(...), VideoSplitter.split(...), VideoIndexer.index(...).

Aparentemente, acho que os nomes das classes são suficientemente descritivos de sua função pretendida e são na verdade substantivos. Correspondentemente, seus métodos também são verbos.

Isso é realmente um problema?

yjwong
fonte
14
Depende do idioma. Em uma linguagem de paradigma múltiplo como C ++ ou Python, essas classes não têm negócios: seus "métodos" devem ser funções livres.
3
@ delnan: mesmo em C ++, você normalmente usa classes para criar módulos, mesmo que não precise dos "recursos OO" completos. De fato, existe a alternativa de usar namespaces para agrupar "funções livres" em um módulo, mas não vejo vantagens nisso.
Doc Brown
5
@ DocBrown: C ++ tem namespaces para isso. De fato, no C ++ moderno, você costuma usar funções livres para métodos, porque elas podem ser sobrecarregadas (estaticamente) para todos os argumentos, enquanto os métodos podem ser sobrecarregados apenas para o invocador.
Jan Hudec
2
@ DocBrown: É um cerne da questão. Módulos que usam namespaces com apenas um método "externo" ficam perfeitamente bem quando a função é suficientemente complexa. Como os espaços para nome não pretendem representar nada e não pretendem ser orientados a objetos. Classes sim, mas classes como essa são realmente apenas namespaces. Claro que em Java você não pode ter uma função, então esse é o resultado. Vergonha em Java.
Jan Hudec
2
Relacionado (quase uma duplicata): programmers.stackexchange.com/q/175070/33843
Heinzi

Respostas:

87

Não, isso não é um problema, muito pelo contrário. É um sinal de modularidade e clara responsabilidade da classe. A interface enxuta é fácil de entender do ponto de vista de um usuário dessa classe e incentivará o acoplamento frouxo. Isso tem muitas vantagens, mas quase nenhuma desvantagem. Eu gostaria que mais componentes fossem projetados dessa maneira!

Doc Brown
fonte
6
Essa é a resposta correta, e eu a votei positivamente, mas você pode melhorar explicando algumas das vantagens. Eu acho que o instinto do OP está dizendo a ele que é um problema é outra coisa ... ou seja, que o VideoCompressor é realmente uma interface. Mp4VideoCompressor é uma classe e deve ser intercambiável com outro VideoCompressor sem copiar o código de um VideoSplitter para uma nova classe.
PDR
11
Desculpe, mas você está errado. Uma classe com um único método deve ser apenas uma função autônoma.
Route de milhas
3
@MilesRout: seu comentário mostra que você não entendeu a pergunta - o OP escreveu sobre uma classe com um método público, não com um método (e ele realmente quis dizer uma classe com vários métodos privados, como ele nos disse aqui em um comentário).
Doc Brown
2
"Uma classe com um único método deve ser apenas uma função autônoma". Uma suposição grosseira sobre o que é esse método. Eu tenho uma classe com um método público. ATRÁS DE TI, existem vários métodos nessa classe, além de outras 5 classes que não fazem a menor ideia do cliente. A excelente aplicação do SRP produzirá uma interface limpa e simples da estrutura de classes em camadas, com essa simplicidade se manifestando até o topo.
Radarbob #
2
@ Andy: a questão era "isso é um problema" e não "isso está em conformidade com uma definição específica de OO". Além disso, não há absolutamente nenhum sinal na pergunta de que OP significa classes sem estado.
Doc Brown
26

Não é mais orientado a objetos. Como essas classes não representam nada, elas são apenas vasos para as funções.

Isso não significa que está errado. Se a funcionalidade for suficientemente complexa ou genérica (ou seja, os argumentos são interfaces, não tipos finais concretos), faz sentido colocar essa funcionalidade em um módulo separado .

A partir daí, depende do seu idioma. Se o idioma tiver funções livres, eles deverão ser módulos que exportam funções. Por que fingir que é uma aula quando não é? Se a linguagem não possui funções livres como, por exemplo, Java, você cria classes com um único método público. Bem, isso apenas mostra os limites do design orientado a objetos. Às vezes, funcional é simplesmente uma combinação melhor.

Há um caso em que você pode precisar de uma classe com um método público único, porque ele precisa implementar uma interface com um método público único. Seja pelo padrão do observador ou pela injeção de dependência ou qualquer outra coisa. Aqui, novamente, depende do idioma. Em linguagens que possuem functores de primeira classe (C ++ ( std::functionou parâmetro de modelo), C # (delegado), Python, Perl, Ruby ( proc), Lisp, Haskell, ...) esses padrões usam tipos de função e não precisam de classes. Java (ainda, na versão 8) não possui tipos de função, portanto, você usa interfaces de método único e classes de método único correspondentes.

É claro que não estou defendendo escrever uma única função enorme. Ele deve ter sub-rotinas privadas, mas elas podem ser privadas para o arquivo de implementação (espaço para nome estático ou anônimo no nível do arquivo em C ++) ou em uma classe auxiliar privada que é instanciada apenas dentro da função pública ( Para armazenar dados ou não? ).

Jan Hudec
fonte
12
"Por que fingir que é uma aula quando não é." Ter um objeto em vez de uma função livre permite estado e subtipagem. Isso pode ser valioso em um mundo onde os requisitos mudam. Mais cedo ou mais tarde, é necessário jogar com as configurações de compactação de vídeo ou fornecer algoritmos de compactação alternativos. Antes disso, a palavra "classe" simplesmente nos diz que essa função pertence a um módulo de software facilmente extensível e intercambiável, com uma responsabilidade clara. Não é isso que o OO realmente visa?
Vem de
11
Bem, se ele tiver apenas um método público (e meio que não implica nenhum método protegido), ele realmente não pode ser estendido. Obviamente, compactar parâmetros de compactação pode fazer sentido; nesse caso, ele se torna um objeto de função (algumas linguagens têm suporte separado para elas, outras não).
Jan Hudec
3
Essa é uma conexão fundamental entre objetos e funções: uma função é isomórfica para um objeto com um único método e sem campos. Um fechamento é isomórfico para um objeto com um único método e alguns campos. Uma função seletora que retorna uma das várias funções que se fecham sobre o mesmo conjunto de variáveis ​​é isomórfica para um objeto. (Na verdade, esta é a forma como os objetos são codificados em JavaScript, exceto que você usar uma estrutura de dados do dicionário em vez de uma função de seleção.)
Jörg W Mittag
4
"Não é mais orientado a objetos. Como essas classes não representam nada, são apenas vasos para as funções". - Isto está errado. Eu diria que essa abordagem é mais orientada a objetos. OO não significa que representa um objeto do mundo real. Uma grande quantidade de programação lida com conceitos abstratos, mas isso significa que você não pode aplicar uma abordagem OO para resolvê-la? Definitivamente não. É mais sobre modularidade e abstração. Cada objeto tem uma única responsabilidade. Isso facilita a compreensão do programa e as alterações. Não há espaço suficiente para listar os inúmeros benefícios do POO.
Despertar
2
@ Phoshi: Sim, eu entendo. Eu nunca afirmei que uma abordagem funcional não funcionaria tão bem. No entanto, esse claramente não é o assunto. Um compressor de vídeo ou um transmogrificador de vídeo ou o que quer que seja ainda é um candidato perfeitamente válido para um objeto.
Vem de
13

Pode haver razões para extrair um determinado método em uma classe dedicada. Uma dessas razões é permitir a injeção de dependência.

Imagine que você tem uma classe chamada VideoExporterque, eventualmente, poderá compactar um vídeo. Uma maneira limpa seria ter uma interface:

interface IVideoCompressor
{
    Stream compress(Video video);
}

que seria implementado assim:

class MpegVideoCompressor : IVideoCompressor
{
    // ...
}

class FlashVideoCompressor : IVideoCompressor
{
    // ...
}

e usado assim:

class VideoExporter
{
    // ...
    void export(Destination destination, IVideoCompressor compressor)
    {
        // ...
        destination = compressor(this.source);
        // ...
    }
    // ...
}

Uma alternativa ruim seria ter um VideoExportermétodo que possua muitos métodos públicos e faça todo o trabalho, incluindo a compactação. Isso rapidamente se tornaria um pesadelo de manutenção, dificultando a adição de suporte para outros formatos de vídeo.

Arseni Mourzenko
fonte
2
Sua resposta não faz distinção entre métodos públicos e privados. Poderia ser entendido como uma recomendação para colocar todo o código de uma classe em apenas um método, mas acho que não foi isso que você quis dizer?
Doc Brown
2
@DocBrown: Métodos particulares não são relevantes para esta questão. Eles podem ser colocados na classe auxiliar interna ou o que for.
Jan Hudec
2
@JanHudec: atualmente o texto é "Uma alternativa ruim seria ter um VideoExporter com muitos métodos" - mas deveria ser "Uma alternativa ruim seria ter um VideoExporter com muitos métodos públicos ".
Doc Brown
@ DocBrown: Concorde aqui.
Jan Hudec
2
@ DocBrown: obrigado por suas observações. Eu editei minha resposta. Originalmente, pensei que fosse assumido que a pergunta (e, portanto, minha resposta) é apenas sobre métodos públicos. Parece que não é tão óbvio.
Arseni Mourzenko
6

Este é um sinal de que você deseja passar funções como argumentos para outras funções. Suponho que sua linguagem (Java?) Não a suporta; se for esse o caso, não é tanto uma falha no seu design quanto uma falha no seu idioma de escolha. Esse é um dos maiores problemas com idiomas que insistem em que tudo deve ser uma classe.

Se você não está realmente passando essas funções falsas, apenas deseja uma função livre / estática.

Doval
fonte
11
Desde o Java 8, você tem lambdas, então você pode praticamente passar funções.
Silviu Burcea 30/01
Ponto justo, mas isso não será lançado oficialmente por um pouco mais de tempo, e alguns locais de trabalho demoram a mudar para versões mais recentes.
Doval 30/01
A data de lançamento é em março. Além disso, as compilações do EAP eram muito populares para o JDK 8 :) #
Silviu Burcea
Embora o Java 8 lambdas seja simplesmente uma maneira abreviada de definir objetos com apenas um método público, além de economizar um pouco de código padrão, eles não fazem nenhuma diferença aqui.
Jules
5

Eu sei que estou atrasado para a festa, mas como todos parecem ter esquecido de apontar isso:

Este é um padrão de design bem conhecido chamado: Padrão de Estratégia .

O padrão de estratégia é usado quando existem várias estratégias possíveis para resolver um subproblema. Normalmente, você define uma interface que aplica um contrato em todas as implementações e, em seguida, usa alguma forma de injeção de dependência para fornecer a estratégia concreta para você.

Por exemplo, neste caso, você poderia ter interface VideoCompressore, em seguida, ter várias implementações alternativas, por exemplo, class H264Compressor implements VideoCompressore class XVidCompressor implements VideoCompressor. Não está claro pelo OP que existe uma interface envolvida, mesmo que não exista, pode ser simplesmente que o autor original tenha deixado a porta aberta para implementar o padrão de estratégia, se necessário. Que por si só também é um bom design.

O problema que o OP constantemente se vê instanciando as classes a chamar um método é o problema de ela não usar a injeção de dependência e o padrão de estratégia corretamente. Em vez de instanciar onde você precisar, a classe que contém deve ter um membro com o objeto de estratégia. E esse membro deve ser injetado, por exemplo, no construtor.

Em muitos casos, o padrão de estratégia resulta em classes de interface (como você está mostrando) com apenas um único doStuff(...)método.

Emily L.
fonte
1
public interface IVideoProcessor
{
   void Split();

   void Compress();

   void Index();
}

O que você tem é modular e isso é bom, mas se você agrupasse essas responsabilidades no IVideoProcessor, provavelmente faria mais sentido do ponto de vista do DDD.

Por outro lado, se a divisão, a compactação e a indexação não estivessem relacionadas de alguma forma, eu as manteria como componentes separados.

CodeART
fonte
Como a razão para cada uma dessas funções precisar mudar é um pouco diferente, eu argumentaria que colocá-las juntas dessa forma viola o SRP.
Jules
0

É um problema - você está trabalhando a partir do aspecto funcional do design, e não dos dados. O que você realmente tem são três funções independentes que foram executadas por OO.

Por exemplo, você tem uma classe VideoCompressor. Por que você está trabalhando com uma classe projetada para compactar vídeo - por que você não possui uma classe Video com métodos para compactar os dados (vídeo) que cada objeto desse tipo contém?

Ao projetar sistemas OO, é melhor criar classes que representam objetos, em vez de classes que representam atividades que você pode aplicar. Antigamente, as classes eram chamadas de tipos - OO era uma maneira de estender um idioma com suporte para novos tipos de dados. Se você pensar em OO assim, terá uma maneira melhor de projetar suas classes.

EDITAR:

deixe-me tentar me explicar um pouco melhor, imagine uma classe de string que tenha um método concat. Você pode implementar uma coisa em que cada objeto instanciado da classe contenha os dados da string, para poder dizer

string mystring("Hello"); 
mystring.concat("World");

mas o OP quer que funcione assim:

string mystring();
string result = mystring.concat("Hello", "World");

agora há lugares em que uma classe pode ser usada para armazenar uma coleção de funções relacionadas, mas isso não é OO, é uma maneira prática de usar os recursos OO de uma linguagem para ajudar a gerenciar melhor sua base de código, mas não é de forma alguma de "OO Design". O objeto nesses casos é totalmente artificial, simplesmente usado assim, porque a linguagem não oferece nada melhor para gerenciar esse tipo de problema. por exemplo. Em linguagens como C #, você usaria uma classe estática para fornecer essa funcionalidade - ela reutiliza o mecanismo de classe, mas não é mais necessário instanciar um objeto apenas para chamar os métodos nele. Você acaba com métodos como o string.IsNullOrEmpty(mystring)que eu acho ruim em comparação com mystring.isNullOrEmpty().

Portanto, se alguém perguntar "como faço para projetar minhas classes", recomendo pensar nos dados que a classe conterá, e não nas funções que ela contém. Se você optar por "uma classe é um monte de métodos", você acaba escrevendo o código de estilo "melhor C". (o que não é necessariamente algo ruim se você estiver aprimorando o código C), mas não oferecerá o melhor programa projetado para OO.

gbjbaanb
fonte
9
-1, eu discordo totalmente. O projeto OO para iniciantes funciona apenas com objetos de dados como a Videoe tende a sobrecarregar essas classes com a funcionalidade, que geralmente termina em código confuso com> 10K LOC por classe. O design avançado de OO divide a funcionalidade em unidades menores como a VideoCompressor(e permite que uma Videoclasse seja apenas uma classe de dados ou uma fachada para a VideoCompressor).
Doc Brown
5
@ DocBrown: Nesse ponto, no entanto, não é mais um design orientado a objetos, porque o VideoCompressornão representa um objeto . Não há nada de errado nisso, apenas mostra o limite do design orientado a objetos.
Jan Hudec
3
ah, mas OO para iniciantes, onde 1 função é transformada em classe, não é realmente OO, e apenas incentiva a dissociação maciça que acaba em mil arquivos de classes que ninguém pode manter. Eu acho que é melhor pensar em uma classe como um "invólucro de dados" e não um "invólucro de funcionalidade", que dará ao iniciante uma melhor compreensão de como pensar nos programas OO.
Gbjbaanb
4
O @DocBrown realmente destacou com precisão minha preocupação; Na verdade, eu tenho uma Videoclasse, mas a metodologia de compactação não é de forma alguma trivial, então eu realmente dividiria o compressmétodo em vários outros métodos privados. Isso é parte do motivo da minha pergunta, depois de ter lido Não criar classes de verbos
yjwong
2
Eu acho que pode ser bom ter uma Videoclasse, mas seria composta de a VideoCompressor, a VideoSplittere outras classes relacionadas, que devem, em boa forma OO, ser classes individuais altamente coesas.
Eric King
-1

O ISP (princípio de segregação de interface) diz que nenhum cliente deve ser forçado a depender dos métodos que não usa. Os benefícios são múltiplos e claros. Sua abordagem respeita totalmente o ISP, e isso é bom.

Uma abordagem diferente também respeitando o ISP é, por exemplo, criar uma interface para cada método (ou conjunto de métodos com alta coesão) e, em seguida, ter uma única classe implementando todas essas interfaces. Se esta é ou não uma solução melhor, depende do cenário. O benefício disso é que, ao usar a injeção de dependência, você pode ter um cliente com diferentes colaboradores (um por cada interface), mas no final todos os colaboradores apontarão para a mesma instância do objeto.

A propósito, você disse

Eu me pego instanciando cada aula

, essas classes parecem ser serviços (e, portanto, sem estado). Você já pensou em fazê-los singletons?

diegomtassis
fonte
11
Singletons são, geralmente, antipadrão. Consulte Singletons: resolvendo problemas que você não sabia que não tinha desde 1995 .
Jan Hudec
3
Quanto ao resto, embora o princípio de separação de interface seja um bom padrão, essa pergunta é sobre as implementações, não as interfaces, então não tenho certeza de que seja relevante.
Jan Hudec
3
Não vou entrar para discutir se o singleton é um padrão ou um antipadrão. Sugeri uma possível solução (que tem até um nome) para o problema, cabe a ele decidir se encaixa ou não.
diegomtassis
Se o ISP é relevante ou não, bem, o assunto da pergunta é "são classes com apenas um método único um problema?", O oposto disso é uma classe com muitos métodos, que se torna um problema no momento em que você cria um método. o cliente depende diretamente dele ... exatamente o exemplo xerox de Robert Martin que levou para formular o ISP.
diegomtassis
-1

Sim, há um problema. Mas não grave. Quero dizer, você pode estruturar seu código assim e nada de ruim acontecerá, é sustentável. Mas existem algumas fraquezas nesse tipo de estruturação. Por exemplo, considere se a representação do seu vídeo (no seu caso, está agrupada na classe RawVideo) muda, você precisará atualizar todas as suas classes de operação. Ou considere que você pode ter várias representações de vídeo que variam em tempo de execução. Então você terá que combinar a representação com a classe "operação" específica. Subjetivamente, é irritante arrastar uma dependência para cada operação que você deseja executar na smth. e para atualizar a lista de dependências transmitidas toda vez que você decide que a operação não é mais necessária ou que precisa de nova operação.

Também isso é realmente uma violação do SRP. Algumas pessoas apenas tratam o SRP como um guia para dividir responsabilidades (e levam isso longe ao tratar cada operação de uma responsabilidade distinta), mas esquecem que o SRP também é um guia para agrupar responsabilidades. E, de acordo com as responsabilidades do SRP, as mudanças pelo mesmo motivo devem ser agrupadas para que, se as mudanças acontecerem, sejam localizadas no menor número possível de classes / módulos. Quanto às grandes classes, não é um problema ter vários algoritmos na mesma classe, desde que esses algoritmos estejam relacionados (ou seja, compartilhe algum conhecimento que não deve ser conhecido fora dessa classe). O problema são grandes classes que possuem algoritmos que não estão relacionados de nenhuma maneira e que mudam / variam por diferentes motivos.

ddiukariev
fonte