¿Las clases con un solo método (público) son un problema?

51

Actualmente estoy trabajando en un proyecto de software que realiza compresión e indexación en imágenes de videovigilancia. La compresión funciona dividiendo los objetos de fondo y primer plano, luego guardando el fondo como una imagen estática y el primer plano como un sprite.

Recientemente, me he embarcado en revisar algunas de las clases que he diseñado para el proyecto.

Noté que hay muchas clases que solo tienen un único método público. Algunas de estas clases son:

  • VideoCompressor (con un compressmétodo que toma un video de entrada de tipo RawVideoy devuelve un video de salida de tipo CompressedVideo).
  • VideoSplitter (con un splitmétodo que toma un video de entrada de tipo RawVideoy devuelve un vector de 2 videos de salida, cada uno de tipo RawVideo).
  • VideoIndexer (con un indexmétodo que toma un video de entrada de tipo RawVideoy devuelve un índice de video de tipo VideoIndex).

Me encuentro una instancia de cada clase sólo para hacer llamadas como VideoCompressor.compress(...), VideoSplitter.split(...), VideoIndexer.index(...).

En la superficie, creo que los nombres de clase son suficientemente descriptivos de su función prevista, y en realidad son sustantivos. En consecuencia, sus métodos también son verbos.

¿Es esto realmente un problema?

yjwong
fuente
14
Depende del idioma. En un lenguaje multi-paradigmático como C ++ o Python, estas clases no tienen ningún negocio: sus "métodos" deberían ser funciones libres.
3
@delnan: incluso en C ++, normalmente usa clases para crear módulos, incluso si no necesita todas las "capacidades OO". De hecho, existe la alternativa de usar espacios de nombres en lugar de agrupar "funciones libres" en un módulo, pero no veo ninguna ventaja en eso.
Doc Brown
55
@DocBrown: C ++ tiene espacios de nombres para eso. De hecho, en C ++ moderno, a menudo se utilizan funciones gratuitas para los métodos, ya que se pueden sobrecargar (estáticamente) para todos los argumentos, mientras que los métodos solo se pueden sobrecargar para el invocante.
Jan Hudec
2
@DocBrown: Es el núcleo de la pregunta. Los módulos que usan espacios de nombres que tienen un solo método "externo" están perfectamente bien cuando la función es suficientemente compleja. Debido a que los espacios de nombres no pretenden representar nada y no pretenden estar orientados a objetos. Las clases sí, pero clases como esta son realmente solo espacios de nombres. Por supuesto, en Java no puede tener una función, por lo que este es el resultado. Vergüenza en Java.
Jan Hudec
2
Relacionado (casi un duplicado): programmers.stackexchange.com/q/175070/33843
Heinzi

Respuestas:

87

No, esto no es un problema, todo lo contrario. Es un signo de modularidad y una clara responsabilidad de la clase. La interfaz esbelta es fácil de entender desde el punto de vista de un usuario de esa clase, y fomentará el acoplamiento flojo. Esto tiene muchas ventajas pero casi ningún inconveniente. ¡Deseo que más componentes se diseñen de esa manera!

Doc Brown
fuente
66
Esta es la respuesta correcta, y la he votado positivamente, pero podría mejorarla explicando algunas de las ventajas. Creo que lo que el instinto del OP le dice es que un problema es otra cosa ... es decir, VideoCompressor es realmente una interfaz. Mp4VideoCompressor es una clase y debe ser intercambiable con otro VideoCompressor sin copiar el código de un VideoSplitter a una nueva clase.
pdr
1
Lo siento, pero te equivocas. Una clase con un solo método debería ser una función independiente.
Miles Rout
3
@MilesRout: su comentario muestra que no entendió la pregunta: el OP escribió sobre una clase con un método público, no con un método (y de hecho se refería a una clase con múltiples métodos privados, como nos dijo aquí en un comentario).
Doc Brown
2
"Una clase con un solo método debería ser una función independiente". Una suposición general sobre cuál es ese método. Tengo una clase con un método público. Detrás de él hay múltiples métodos en esa clase más otras 5 clases que no tienen absolutamente ninguna idea del cliente. La excelente aplicación de SRP producirá una interfaz limpia y simple de la estructura de clases en capas, con esa simplicidad que se manifiesta hasta la cima.
radarbob
2
@Andy: la pregunta era "¿es esto un problema" y no "esto se ajusta a una definición OO específica"? Además, no hay absolutamente ningún signo en la pregunta de que OP significa clases sin estado.
Doc Brown
26

Ya no está orientado a objetos. Debido a que esas clases no representan nada, son solo recipientes para las funciones.

Eso no significa que esté mal. Si la funcionalidad es suficientemente compleja o cuando es genérica (es decir, los argumentos son interfaces, no tipos finales concretos), tiene sentido colocar esa funcionalidad en un módulo separado .

A partir de ahí depende de tu idioma. Si el lenguaje tiene funciones libres, deberían ser módulos que exporten funciones. ¿Por qué pretender que es una clase cuando no lo es? Si el lenguaje no tiene funciones libres como, por ejemplo, Java, entonces crea clases con un único método público. Bueno, eso solo muestra los límites del diseño orientado a objetos. A veces funcional es simplemente una mejor combinación.

Hay un caso en el que puede necesitar una clase con un método público único porque tiene que implementar una interfaz con un método público único. Ya sea por patrón de observación o inyección de dependencia o lo que sea. Aquí nuevamente depende del idioma. En lenguajes que tienen functores de primera clase (C ++ ( std::functiono parámetro de plantilla), C # (delegado), Python, Perl, Ruby ( proc), Lisp, Haskell, ...) estos patrones usan tipos de función y no necesitan clases. Java no tiene (aún, tendrá en la versión 8) tipos de funciones, por lo que utiliza interfaces de método único y las clases de método único correspondientes.

Por supuesto, no estoy abogando por escribir una sola función enorme. Debe tener subrutinas privadas, pero pueden ser privadas para el archivo de implementación (espacio de nombres estático o anónimo a nivel de archivo en C ++) o en una clase auxiliar privada que solo se instancia dentro de la función pública (¿ Para almacenar datos o no? ).

Jan Hudec
fuente
12
"¿Por qué fingir que es una clase cuando no lo es?" Tener un objeto en lugar de una función libre permite el estado y la subtipificación. Eso podría resultar valioso en un mundo donde los requisitos cambian. Tarde o temprano, es necesario jugar con la configuración de compresión de video o proporcionar algoritmos de compresión alternativos. Antes de eso, la palabra "clase" simplemente nos dice que esta función pertenece a un módulo de software fácilmente extensible e intercambiable con una clara responsabilidad. ¿No es eso a lo que realmente apunta OO?
VENIDO DEL
1
Bueno, si solo tiene un método público (y no implica que esté protegido), en realidad no se puede extender. Por supuesto, empaquetar los parámetros de compresión podría tener sentido en cuyo caso se convierte en objeto de función (algunos lenguajes tienen soporte separado para esos, otros no).
Jan Hudec
3
Esta es una conexión fundamental entre objetos y funciones: una función es isomorfa a un objeto con un único método y sin campos. Un cierre es isomorfo a un objeto con un único método y algunos campos. Una función de selector que devuelve una de varias funciones que se cierran sobre el mismo conjunto de variables es isomorfa a un objeto. (De hecho, así es como se codifican los objetos en JavaScript, excepto que se utiliza una estructura de datos de diccionario en lugar de una función de selección).
Jörg W Mittag
44
"Ya no está orientado a objetos. Debido a que esas clases no representan nada, son solo recipientes para las funciones". - Esto está mal. Yo diría que este enfoque está MÁS orientado a objetos. OO no significa que represente un objeto del mundo real. Una gran cantidad de programación trata con conceptos abstractos, pero ¿significa esto que no puede aplicar un enfoque OO para resolverlo? Definitivamente no. Se trata más de modularidad y abstracción. Cada objeto tiene una sola responsabilidad. Esto hace que sea fácil comprender el programa y hacer cambios. No hay suficiente espacio para enumerar los numerosos beneficios de la POO.
Despertar
2
@Phoshi: Sí, lo entiendo. Nunca dije que un enfoque funcional no funcionaría tan bien. Sin embargo, ese claramente no es el tema. Un compresor de video o un transfigurador de video o lo que sea que sea un candidato perfectamente válido para un objeto
VENIDO DEL
13

Puede haber razones para extraer un método dado en una clase dedicada. Una de esas razones es permitir la inyección de dependencia.

Imagine que tiene una clase llamada VideoExporterque, eventualmente, debería poder comprimir un video. Una forma limpia sería tener una interfaz:

interface IVideoCompressor
{
    Stream compress(Video video);
}

que se implementaría así:

class MpegVideoCompressor : IVideoCompressor
{
    // ...
}

class FlashVideoCompressor : IVideoCompressor
{
    // ...
}

y usado así:

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

Una mala alternativa sería tener uno VideoExporterque tenga muchos métodos públicos y haga todo el trabajo, incluida la compresión. Se convertiría rápidamente en una pesadilla de mantenimiento, lo que dificulta agregar soporte para otros formatos de video.

Arseni Mourzenko
fuente
2
Su respuesta no distingue entre métodos públicos y privados. Podría entenderse como una recomendación para poner todo el código de una clase en un solo método, pero supongo que eso no es lo que querías decir.
Doc Brown
2
@DocBrown: los métodos privados no son relevantes para esta pregunta. Se pueden poner en clase de ayuda interna o lo que sea.
Jan Hudec
2
@JanHudec: actualmente el texto es "Una mala alternativa sería tener un VideoExporter que tenga muchos métodos", pero debería ser "Una mala alternativa sería tener un VideoExporter que tenga muchos métodos públicos ".
Doc Brown
@DocBrown: De acuerdo aquí.
Jan Hudec
2
@DocBrown: gracias por sus comentarios. Edité mi respuesta. Originalmente, pensé que se suponía que la pregunta (y, por lo tanto, mi respuesta) es solo sobre métodos públicos. Parece que no es tan obvio.
Arseni Mourzenko
6

Esta es una señal de que desea pasar funciones como argumentos a otras funciones. Supongo que su lenguaje (¿Java?) No lo admite; Si ese es el caso, no se trata tanto de una falla en su diseño como de una deficiencia en el idioma de su elección. Este es uno de los mayores problemas con los idiomas que insisten en que todo debe ser una clase.

Si no estás pasando estas funciones falsas, entonces solo quieres una función libre / estática.

Doval
fuente
1
Desde Java 8, tiene lambdas, por lo que prácticamente puede pasar funciones.
Silviu Burcea
Punto justo, pero eso no se lanzará oficialmente por un tiempo más, y algunos lugares de trabajo tardan en pasar a versiones más nuevas.
Doval
La fecha de lanzamiento es en marzo. Además, las compilaciones de EAP fueron muy populares para JDK 8 :)
Silviu Burcea
Aunque, dado que las lambdas de Java 8 son simplemente una forma abreviada de definir objetos con un solo método público, aparte de guardar un poco de código repetitivo, no hacen ninguna diferencia aquí.
Jules
5

Sé que llego tarde a la fiesta, pero como todos parecen haber dejado de señalar esto:

Este es un patrón de diseño bien conocido llamado: Patrón de estrategia .

El patrón de estrategia se usa cuando hay varias estrategias posibles para resolver un subproblema. Por lo general, define una interfaz que hace cumplir un contrato en todas las implementaciones y luego usa alguna forma de Inyección de dependencias para proporcionar la estrategia concreta para usted.

Por ejemplo, en este caso, podría tener interface VideoCompressory luego tener varias implementaciones alternativas, por ejemplo, class H264Compressor implements VideoCompressory class XVidCompressor implements VideoCompressor. Desde el OP no está claro que haya una interfaz involucrada, incluso si no la hay, puede ser simplemente que el autor original dejó la puerta abierta para implementar el patrón de estrategia si es necesario. Lo cual en sí mismo es un buen diseño también.

El problema con el que OP se encuentra constantemente creando instancias de las clases para llamar a un método es un problema con ella que no usa la inyección de dependencia y el patrón de estrategia correctamente. En lugar de instanciarlo donde lo necesita, la clase que lo contiene debe tener un miembro con el objeto de estrategia. Y este miembro debe inyectarse, por ejemplo, en el constructor.

En muchos casos, el patrón de estrategia da como resultado clases de interfaz (como se muestra) con un solo doStuff(...)método.

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

   void Compress();

   void Index();
}

Lo que tiene es modular y eso es bueno, pero si tuviera que agrupar estas responsabilidades en IVideoProcessor, eso probablemente tendría más sentido desde el punto de vista DDD.

Por otro lado, si la división, la compresión y la indexación no estuvieran relacionadas de ninguna manera, las mantendría como componentes separados.

CodeART
fuente
Como la razón por la cual cada una de estas funciones necesita cambiar es algo diferente, diría que unirlas de esta manera viola SRP.
Julio
0

Es un problema: está trabajando desde el aspecto funcional del diseño, en lugar de los datos. Lo que realmente tiene son 3 funciones independientes que han sido OO-ified.

Por ejemplo, tiene una clase VideoCompressor. ¿Por qué está trabajando con una clase diseñada para comprimir video? ¿Por qué no tiene una clase de Video con métodos para comprimir los datos (de video) que contiene cada objeto de este tipo?

Al diseñar sistemas OO, es mejor crear clases que representen objetos, en lugar de clases que representen actividades que pueda aplicar. En los viejos tiempos, las clases se llamaban tipos: OO era una forma de extender un idioma con soporte para nuevos tipos de datos. Si piensas en OO así, obtienes una mejor manera de diseñar tus clases.

EDITAR:

déjame intentar explicarme un poco mejor, imagina una clase de cadena que tiene un método concat. Puede implementar tal cosa donde cada objeto instanciado de la clase contiene los datos de la cadena, por lo que puede decir

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

pero el OP quiere que funcione así:

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

ahora hay lugares donde una clase se puede usar para mantener una colección de funciones relacionadas, pero eso no es OO, es una forma práctica de usar las características OO de un lenguaje para ayudar a administrar mejor su base de código, pero de ninguna manera de "OO Design". El objeto en tales casos es totalmente artificial, simplemente se usa así porque el lenguaje no ofrece nada mejor para manejar este tipo de problema. p.ej. En lenguajes como C #, usaría una clase estática para proporcionar esta funcionalidad: reutiliza el mecanismo de clase, pero ya no necesita crear una instancia de un objeto solo para invocar los métodos. Terminas con métodos como los string.IsNullOrEmpty(mystring)que creo que son pobres en comparación mystring.isNullOrEmpty().

Entonces, si alguien pregunta "cómo diseño mis clases", recomiendo pensar en los datos que contendrá la clase en lugar de las funciones que contiene. Si opta por "una clase es un montón de métodos", entonces termina escribiendo un código de estilo "mejor C". (que no es necesariamente algo malo si está mejorando el código C) pero no le dará el mejor programa diseñado por OO.

gbjbaanb
fuente
99
-1, estoy totalmente en desacuerdo. El diseño OO para principiantes solo funciona con objetos de datos como a Videoy tiende a exagerar tales clases con funcionalidad, que a menudo termina en un código desordenado con> 10K LOC por clase. El diseño avanzado de OO desglosa la funcionalidad en unidades más pequeñas como una VideoCompressor(y permite que una Videoclase solo sea una clase de datos o una fachada para la VideoCompressor).
Doc Brown
55
@DocBrown: en ese momento, sin embargo, ya no es un diseño orientado a objetos, porque VideoCompressorno representa un objeto . No hay nada de malo en eso, solo muestra el límite del diseño orientado a objetos.
Jan Hudec
3
ah, pero los principiantes OO donde 1 función se convierte en una clase no es realmente OO en absoluto, y solo alienta el desacoplamiento masivo que termina en mil archivos de clases que nadie puede mantener. Creo que es mejor pensar en una clase como un "contenedor de datos", no un "contenedor de funciones", que le dará a un principiante una mejor comprensión de cómo pensar en los programas OO.
gbjbaanb
44
@DocBrown realmente resaltó con precisión mi preocupación; De hecho, tengo una Videoclase, pero la metodología de compresión no es en absoluto trivial, por lo que realmente dividiría el compressmétodo en varios otros métodos privados. Esta es parte de la razón de mi pregunta, después de haber leído No crear clases de verbos
yjwong
2
Creo que podría estar bien tener una Videoclase, pero estaría compuesta de a VideoCompressor, a VideoSplittery otras clases relacionadas, que deberían, en buena forma OO, ser clases individuales altamente cohesivas.
Eric King
-1

El ISP (principio de segregación de interfaz) dice que ningún cliente debería verse obligado a depender de métodos que no utiliza. Los beneficios son múltiples y claros. Su enfoque respeta totalmente al ISP, y eso es bueno.

Un enfoque diferente que también respeta al ISP es, por ejemplo, crear una interfaz para cada método (o conjunto de métodos con una alta cohesión) y luego tener una sola clase que implemente todas esas interfaces. Si esta es o no una mejor solución depende del escenario. El beneficio de esto es que, al usar la inyección de dependencia, podría tener un cliente con diferentes colaboradores (uno por cada interfaz) pero al final todos los colaboradores apuntarán a la misma instancia de objeto.

Por cierto, dijiste

Me encuentro instanciando cada clase

, estas clases parecen ser servicios (y, por lo tanto, sin estado). ¿Has pensado en hacerlos solteros?

diegomtasis
fuente
1
Los singletons son, por lo general, antipatrones. Ver Singletons: Resolviendo problemas que no sabías que no tenías desde 1995 .
Jan Hudec
3
Por lo demás, aunque el principio de separación de la interfaz es un buen patrón, esta pregunta se trata de las implementaciones, no de las interfaces, por lo que no estoy seguro de que sea relevante.
Jan Hudec
3
No voy a entrar para discutir si el singleton es un patrón o un antipatrón. Sugerí una posible solución (incluso tiene un nombre) a su problema, depende de él decidir si encaja o no.
diegomtassis
Con respecto a si el ISP es relevante o no, bueno, el tema de la pregunta es "¿las clases con un solo método son un problema?", Lo contrario de eso es una clase con muchos métodos, que se convierte en un problema en el momento en que se el cliente depende directamente de él ... exactamente el ejemplo xerox de Robert Martin que le llevó a formular el ISP.
diegomtassis
-1

Sí, hay un problema Pero no severo. Quiero decir que puedes estructurar tu código de esta manera y no pasará nada malo, es mantenible. Pero hay algunas debilidades en ese tipo de estructuración. Por ejemplo, considere si cambia la representación de su video (en su caso, está agrupado en la clase RawVideo), deberá actualizar todas sus clases de operación. O considere que puede tener múltiples representaciones de video que varían en tiempo de ejecución. Entonces tendrá que hacer coincidir la representación con una clase particular de "operación". También subjetivamente es molesto arrastrar una dependencia para cada operación que desee hacer en smth. y para actualizar la lista de dependencias que se pasan cada vez que decide que la operación ya no es necesaria o necesita una nueva operación.

También eso es en realidad una violación de SRP. Algunas personas simplemente tratan a SRP como una guía para dividir las responsabilidades (y lo llevan demasiado lejos al tratar cada operación como una responsabilidad distinta) pero se olvidan de que SRP también es una guía para agrupar responsabilidades. Y de acuerdo con las responsabilidades de SRP que cambian por la misma razón, deben agruparse para que, si se produce un cambio, se localice en la menor cantidad posible de clases / módulos. En cuanto a las clases grandes, no es un problema tener múltiples algoritmos en la misma clase siempre que esos algoritmos estén relacionados (es decir, compartan algún conocimiento que no debería conocerse fuera de esa clase). El problema son las clases grandes que tienen algoritmos que no están relacionados de ninguna manera y cambian / varían por diferentes razones.

ddiukariev
fuente