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

45

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 os objetos de fundo e de primeiro plano, salvando o plano de fundo como uma imagem estática e o primeiro plano como um sprite.

Recentemente, embarquei na revisão de algumas das classes que projetei para o projeto.

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

  • VideoCompressor (com um método compress que recebe um vídeo de entrada do tipo RawVideo e retorna um vídeo de saída do tipo CompressedVideo ).
  • VideoSplitter (com um método split que recebe um vídeo de entrada do tipo RawVideo e retorna um vetor de 2 vídeos de saída, cada um do tipo RawVideo ).
  • VideoIndexer (com um método index que recebe um vídeo de entrada do tipo RawVideo e retorna um índice de vídeo do tipo VideoIndex ).

Eu me ponho instanciando cada classe apenas para fazer chamadas como VideoCompressor.compress(...) , VideoSplitter.split(...) , VideoIndexer.index(...) .

Na superfície, 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?

    
por yjwong 29.01.2014 / 08:16
fonte

9 respostas

78

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 flexível. Isto tem muitas vantagens, mas quase nenhuma desvantagem. Eu gostaria que mais componentes fossem projetados dessa maneira!

    
por 29.01.2014 / 08:37
fonte
24

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

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

De lá, depende do seu idioma. Se o idioma tiver funções livres, elas devem ser funções de exportação de módulos. Por que fingir que é uma aula quando não é. Se o idioma não tiver funções gratuitas como, por exemplo, Java, então você cria classes com um único método público. Bem, isso mostra apenas os limites do design orientado a objetos. Às vezes funcional é simplesmente melhor correspondência.

Há um caso em que você pode precisar de uma classe com um único método público, pois ele precisa implementar a interface com um único método público. Seja para padrão de observador ou injeção de dependência ou qualquer outra coisa. Aqui novamente depende da linguagem. Em linguagens que possuem functores de primeira classe (C ++ ( std::function ou parâmetro de modelo), C # (delegate), Python, Perl, Ruby ( proc ), Lisp, Haskell, ...) esses padrões usam tipos de função e don ' t precisa de aulas. O Java não possui (ainda, na versão 8) 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 a criação de uma única função enorme. Ele deve ter sub-rotinas particulares, mas elas podem ser privadas para o arquivo de implementação (namespace estático ou anônimo em nível de arquivo em C ++) ou em uma classe auxiliar privada que é instanciada somente dentro da função pública (Para armazenar dados ou não? ).

    
por 29.01.2014 / 13:25
fonte
13

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

Imagine que você tenha uma classe chamada VideoExporter , que, 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 bad seria ter um VideoExporter que tenha 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 o suporte a outros formatos de vídeo.

    
por 29.01.2014 / 08:39
fonte
6

Este é um sinal de que você deseja passar funções como argumentos para outras funções. Eu estou supondo que sua linguagem (Java?) Não suporta isso; Se for esse o caso, não é tanto uma falha no seu design, pois é uma lacuna na sua linguagem de escolha. Este é um dos maiores problemas com idiomas que insistem em que tudo deve ser uma aula.

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

    
por 29.01.2014 / 20:02
fonte
5

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

Este é um padrão de design conhecido como: 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 impõe um contrato em todas as implementações e depois 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 VideoCompressor e, em seguida, ter várias implementações alternativas, por exemplo, class H264Compressor implements VideoCompressor e class XVidCompressor implements VideoCompressor . Não está claro pelo OP que existe uma interface envolvida, mesmo que não haja, pode ser simplesmente que o autor original tenha deixado a porta aberta para implementar o padrão de estratégia, se necessário. Que em si é um bom design também.

O problema que a OP constantemente encontra-se instanciando as classes para chamar um método é um problema com ela não usar a injeção de dependência e o padrão de estratégia corretamente. Em vez de instanciá-lo onde você precisa, a classe contendo 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 método doStuff(...) .

    
por 19.02.2016 / 16:17
fonte
1
public interface IVideoProcessor
{
   void Split();

   void Compress();

   void Index();
}

O que você tem é modular e isso é bom, mas se você fosse agrupar essas responsabilidades em IVideoProcessor, isso 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 forma alguma, então eu as manteria como componentes separados.

    
por 30.01.2014 / 10:55
fonte
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 3 funções autônomas que foram OO-ified.

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 tem uma classe Video com métodos para compactar os dados (de vídeo) que cada objeto desse tipo contém?

Ao projetar sistemas OO, é melhor criar classes que representem objetos, em vez de classes que representem atividades que você possa aplicar. Antigamente, as classes eram chamadas de tipos - OO era uma forma de estender uma linguagem com suporte para novos tipos de dados. Se você pensar em OO dessa forma, terá uma maneira melhor de projetar suas aulas.

EDITAR:

deixe-me tentar me explicar um pouco melhor, imagine uma classe de string que tenha um método concat. Você pode implementar tal coisa onde cada objeto instanciado da classe contém os dados da string, então você pode 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 onde 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 é maneira qualquer tipo de "OO Design". O objeto em tais 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 você não precisa mais instanciar um objeto apenas para chamar os métodos nele. Você acaba com métodos como string.IsNullOrEmpty(mystring) , o que eu acho ruim em comparação com mystring.isNullOrEmpty() .

Então, se alguém estiver perguntando "como faço para projetar minhas aulas", recomendo pensar nos dados que a classe conterá, e não nas funções que ela contém. Se você optar pela "classe é um monte de métodos", você acaba escrevendo o código de estilo "melhor C". (o que não é necessariamente uma coisa ruim se você está melhorando o código C), mas não vai lhe dar o melhor programa projetado por OO.

    
por 29.01.2014 / 12:10
fonte
-1

O ISP (princípio de segregação da interface) diz que nenhum cliente deve ser forçado a depender de 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 que também respeita o ISP é, por exemplo, criar uma interface para cada método (ou conjunto de métodos com alta coesão) e então 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 para cada interface), mas no final todos os colaboradores apontarão para a mesma instância de objeto.

A propósito, você disse

I find myself instantiating each class

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

    
por 29.01.2014 / 15:21
fonte
-1

Sim, existe um problema. Mas não grave. Quer dizer, você pode estruturar seu código assim e nada de ruim vai acontecer, é sustentável. Mas há algumas fraquezas nesse tipo de estruturação. Por exemplo, considere se a representação do seu vídeo (no seu caso ele está agrupado 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 corresponder a representação para uma determinada classe de "operação". Também subjetivamente é chato arrastar uma dependência para cada operação que você quer fazer no smth. e para atualizar a lista de dependências passadas toda vez que você decidir que a operação não é mais necessária ou você precisa de nova operação.

Isso também é uma violação do SRP. Algumas pessoas simplesmente tratam o SRP como um guia para dividir responsabilidades (e levá-lo ao extremo, tratando 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, essas mudanças, pela mesma razão, devem ser agrupadas de forma que, se a mudança ocorrer, ela seja localizada no menor número possível de classes / módulos. Quanto às grandes classes, não é um problema ter múltiplos algoritmos na mesma classe, desde que esses algoritmos estejam relacionados (ou seja, compartilhem 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 alguma forma e mudam / variam por razões diferentes.

    
por 14.12.2017 / 17:15
fonte