Clase que no representa nada, ¿es correcta?

42

Solo estoy diseñando mi aplicación y no estoy seguro si entiendo SOLID y OOP correctamente. Las clases deben hacer 1 cosa y hacerlo bien, pero por otro lado deben representar objetos reales con los que trabajamos.

En mi caso, hago una extracción de características en un conjunto de datos y luego hago un análisis de aprendizaje automático. Supongo que podría crear tres clases.

  1. FeatureExtractor
  2. Conjunto de datos
  3. Analizador

Pero la clase FeatureExtractor no representa nada, hace algo que hace que sea más una rutina que una clase. Tendrá una sola función que se utilizará: extract_features ()

¿Es correcto crear clases que no representan una cosa sino que hacen una cosa?

EDITAR: no estoy seguro si importa pero estoy usando Python

Y si extract_features () se vería así: ¿vale la pena crear una clase especial para contener ese método?

def extract_features(df):
    extr = PhrasesExtractor()
    extr.build_vocabulary(df["Text"].tolist())

    sent = SentimentAnalyser()
    sent.load()

    df = add_features(df, extr.features)
    df = mark_features(df, extr.extract_features)
    df = drop_infrequent_features(df)
    df = another_processing1(df)
    df = another_processing2(df)
    df = another_processing3(df)
    df = set_sentiment(df, sent.get_sentiment)
    return df
Alicja Głowacka
fuente
13
Esto se ve perfectamente bien como una función. Teniendo en cuenta las tres cosas que enumeró como módulos está bien, y es posible que desee colocarlas en diferentes archivos, pero eso no significa que tengan que ser clases.
Bergi
31
Tenga en cuenta que es bastante común y aceptable utilizar enfoques que no sean OO en Python.
jpmc26
13
Te puede interesar el diseño impulsado por dominio. Las "clases deberían representar objetos del mundo real" son realmente falsas ... deberían representar objetos en el dominio . El dominio a menudo está fuertemente vinculado al mundo real, pero dependiendo de la aplicación, algunas cosas pueden o no considerarse objetos, o algunas cosas que "en realidad" están separadas pueden terminar vinculadas o idénticas dentro del dominio de la aplicación.
Bakuriu
1
A medida que se familiarice con OOP, creo que encontrará que las clases rara vez se corresponden individualmente con entidades del mundo real. Por ejemplo, aquí hay un ensayo que argumenta que tratar de agrupar toda la funcionalidad asociada con una entidad del mundo real en una sola clase es con frecuencia un antipatrón: programmer.97things.oreilly.com/wiki/index.php/…
Kevin - Vuelva a instalar a Mónica el
1
"Deberían representar objetos reales con los que trabajamos". no necesariamente. Muchos idiomas tienen una clase de flujo que representa un flujo de bytes, que es un concepto abstracto en lugar de un "objeto real". Técnicamente, un sistema de archivos tampoco es un 'objeto real', es solo un concepto, pero a veces hay clases que representan un sistema de archivos o una parte de él.
Pharap

Respuestas:

96

Las clases deben hacer 1 cosa y hacerlo bien

Sí, generalmente es un buen enfoque.

pero, por otro lado, deberían representar el objeto real con el que trabajamos.

No, ese es un malentendido común en mi humilde opinión. El buen acceso de un principiante a OOP es a menudo "comenzar con objetos que representan cosas del mundo real" , eso es cierto.

Sin embargo, ¡ no debes detenerte con esto !

Las clases pueden (y deben) usarse para estructurar su programa de varias maneras. Modelar objetos del mundo real es un aspecto de esto, pero no el único. Crear módulos o componentes para una tarea específica es otro caso de uso razonable para las clases. Un "extractor de funciones" es probablemente un módulo de este tipo, e incluso si contiene solo un método públicoextract_features() , me sorprendería que si no también contiene muchos métodos privados y tal vez algún estado compartido. Por lo tanto, tener una clase FeatureExtractorintroducirá una ubicación natural para estos métodos privados.

Nota al margen: en lenguajes como Python que admiten un concepto de módulo separado, también se puede usar un módulo FeatureExtractorpara esto, pero en el contexto de esta pregunta, en mi humilde opinión es una diferencia insignificante.

Además, un "extractor de características" se puede imaginar como "una persona o un bot que extrae características". Esa es una "cosa" abstracta, tal vez no sea una cosa que encontrará en el mundo real, pero el nombre en sí es una abstracción útil, que les da a todos una noción de cuál es la responsabilidad de esa clase. Así que no estoy de acuerdo con que esta clase no "represente nada".

Doc Brown
fuente
33
Me gusta especialmente que hayas mencionado el tema del estado interno. A menudo encuentro que ese es el factor decisivo para si hago de algo una clase o una función en Python.
David Z
17
Parece que recomiendas "clasitis". No hagas una clase para esto, es un antipatrón estilo Java. Si es una función, conviértala en una función. El estado interno es irrelevante: las funciones pueden tener eso (a través de cierres).
Konrad Rudolph
25
@KonradRudolph: parece que te has perdido de que esto no se trata de hacer una función. Se trata de un código que requiere varias funciones, un nombre común y quizás algún estado compartido. El uso de un módulo para esto puede ser sensato en idiomas con un concepto de módulo aparte de las clases.
Doc Brown
8
@DocBrown Voy a tener que estar en desacuerdo. Es una clase con un método público. API sabio, eso es indistinguible de una función. Vea mi respuesta para más detalles. El uso de clases de método único puede justificarse si necesita crear un tipo específico para representar un estado. Pero este no es el caso aquí (e incluso entonces, las funciones a veces se pueden usar).
Konrad Rudolph
66
@DocBrown Estoy empezando a ver lo que quieres decir: ¿estás hablando de las funciones que se llaman desde adentro extract_features? Asumí que eran funciones públicas de otro lugar. Justo, estoy de acuerdo en que si estos son privados, entonces probablemente deberían entrar en un módulo (pero aún así: no una clase, a menos que compartan el estado), junto con extract_features. (Dicho esto, por supuesto, podría declararlos localmente dentro de esa función.)
Konrad Rudolph
44

Doc Brown es perfecto: las clases no necesitan representar objetos del mundo real. Solo necesitan ser útiles . Las clases son fundamentalmente meramente tipos adicionales, y ¿a qué corresponde into stringcorresponde en el mundo real? Son descripciones abstractas, no cosas concretas, tangibles.

Dicho eso, tu caso es especial. De acuerdo a tu descripción:

Y si extract_features () se vería así: ¿vale la pena crear una clase especial para contener ese método?

Tiene toda la razón: si su código es como se muestra, no tiene sentido convertirlo en una clase. Hay una famosa charla que argumenta que tales usos de las clases en Python son olfato de código, y que las funciones simples a menudo son suficientes. Su caso es un ejemplo perfecto de esto.

El uso excesivo de clases se debe al hecho de que OOP se convirtió en la corriente principal con Java en la década de 1990. Desafortunadamente, Java en ese momento carecía de varias características del lenguaje moderno (como los cierres), lo que significa que muchos conceptos eran difíciles o imposibles de expresar sin el uso de clases. Por ejemplo, era imposible en Java hasta hace poco tener métodos que llevaran estado (es decir, cierres). En cambio, tenía que escribir una clase para llevar el estado, y que expuso un único método (llamado algo así como invoke).

Desafortunadamente, este estilo de programación se hizo popular mucho más allá de Java (en parte debido a un influyente libro de ingeniería de software que de otra manera sería muy útil), incluso en lenguajes que no requieren tales soluciones.

En Python, las clases son obviamente una herramienta muy importante y deben usarse libremente. Pero no son la única herramienta, y no hay razón para usarlas donde no tienen sentido. Es un error común pensar que las funciones gratuitas no tienen cabida en OOP.

Konrad Rudolph
fuente
Sería útil agregar que si las funciones llamadas en el ejemplo son de hecho funciones privadas, encapsularlas en un módulo o clase sería completamente apropiado. De lo contrario, estoy totalmente de acuerdo.
Leliel
11
También es útil recordar que "OOP" no significa "escribir un montón de clases". Las funciones son objetos en Python, por lo que no hay necesidad de considerar esto como "no OOP". Más bien, es simplemente reutilizar los tipos / clases incorporados, y "reutilizar" es uno de los santos griales en la programación. Ajustar esto en una clase evitaría la reutilización, ya que nada sería compatible con esta nueva API (¡a menos que __call__se defina, en cuyo caso solo use una función!)
Warbo
3
También sobre el tema de "clases vs. funciones independientes": eev.ee/blog/2013/03/03/…
Joker_vD
1
¿No es así, aunque en Python las "funciones libres" también son objetos con un tipo que expone un método __call__()? ¿Es realmente tan diferente de una instancia anónima de clase interna? Sintácticamente, claro, pero desde un diseño de lenguaje, parece una distinción menos significativa que la que presenta aquí.
JimmyJames
1
@JimmyJames Derecha. El punto es que ofrecen la misma funcionalidad para el propósito específico pero son más simples de usar.
Konrad Rudolph el
36

Solo estoy diseñando mi aplicación y no estoy seguro si entiendo SOLID y OOP correctamente.

He estado en esto durante más de 20 años y tampoco estoy seguro.

Las clases deben hacer 1 cosa y hacerlo bien

Difícil equivocarse aquí.

deberían representar objetos reales con los que trabajamos.

¿Oh enserio? Deja que te presente a la clase más popular y exitoso single de todos los tiempos: String. Lo usamos para el texto. Y el objeto del mundo real que representa es este:

Pescador sostiene 10 peces suspendidos de un cordón

Por qué no, no todos los programadores están obsesionados con la pesca. Aquí estamos usando algo llamado metáfora. Está bien hacer modelos de cosas que realmente no existen. Es la idea la que debe quedar clara. Estás creando imágenes en la mente de tus lectores. Esas imágenes no tienen que ser reales. Simplemente entendido fácilmente.

Un buen diseño de OOP agrupa los mensajes (métodos) alrededor de los datos (estado) para que las reacciones a esos mensajes puedan variar dependiendo de esos datos. Si hacer eso modela algo del mundo real, genial. Si no, bueno. Mientras tenga sentido para el lector, está bien.

Ahora seguro, podrías pensarlo así:

Las letras festivas suspendidas de una cadena decían "¡DEJE HACER COSAS!

pero si crees que esto tiene que existir en el mundo real antes de que puedas hacer uso de la metáfora, bueno, tu carrera de programación implicará muchas artes y oficios.

naranja confitada
fuente
1
Una bonita serie de fotos ...
Zev Spitz
En este punto, no estoy seguro de que "cadena" sea incluso metafórica. Sólo tiene un significado específico en el dominio de la programación, al igual que palabras como classy table, y column...
Kyralessa
@ Kyralessa, tú le enseñas la metáfora al novato o dejas que sea mágico para ellos. Por favor, sálvame de los codificadores que creen en la magia.
candied_orange
6

¡Tener cuidado! En ningún lugar SOLID dice que una clase solo debe "hacer una cosa". Si ese fuera el caso, las clases solo tendrían un método único, y realmente no habría una diferencia entre clases y funciones.

SOLID dice que una clase debería representar una sola responsabilidad . Son como las responsabilidades de las personas en un equipo: el conductor, el abogado, el carterista, el diseñador gráfico, etc. Cada una de estas personas puede realizar múltiples tareas (relacionadas), pero todas relacionadas con una sola responsabilidad.

El punto de esto es: si hay un cambio en los requisitos, lo ideal es que solo necesite modificar una sola clase. Esto solo hace que el código sea más fácil de entender, más fácil de modificar y reduce el riesgo.

No existe una regla que indique que un objeto debe representar "una cosa real". Esto es solo una tradición de culto de carga desde que OO se inventó inicialmente para su uso en simulaciones. Pero su programa no es una simulación (pocas aplicaciones OO modernas lo son), por lo que esta regla no se aplica. Mientras cada clase tenga una responsabilidad bien definida, usted debe estar bien.

Si una clase realmente solo tiene un método único y la clase no tiene ningún estado, podría considerar convertirla en una función independiente. Esto está muy bien y sigue los principios de KISS y YAGNI: no es necesario hacer una clase si puede resolverlo con una función. Por otro lado, si tiene razones para creer que podría necesitar un estado interno o múltiples implementaciones, también podría convertirlo en una clase por adelantado. Tendrás que usar tu mejor juicio aquí.

JacquesB
fuente
+1 para "no es necesario hacer una clase si puedes resolverlo con una función". A veces alguien necesita decir la verdad.
tchrist
5

¿Es correcto crear clases que no representan una cosa sino que hacen una cosa?

En general eso está bien.

Sin una descripción un poco más específica, lo que FeatureExtractorse supone que la clase debe hacer exactamente es difícil de decir.

De todos modos, incluso si FeatureExtractorexpone solo una extract_features()función pública , podría pensar en configurarlo con una Strategyclase, que determina cómo exactamente se debe hacer la extracción.

Otro ejemplo es una clase con una función de plantilla .

Y hay más patrones de diseño conductual , que se basan en modelos de clase.


A medida que agregó un código para aclarar.

Y si extract_features () se vería así: ¿vale la pena crear una clase especial para contener ese método?

La línea

 sent = SentimentAnalyser()

comprende exactamente lo que quise decir con que podrías configurar una clase con una estrategia .

Si tiene una interfaz para esa SentimentAnalyserclase, puede pasarla a la FeatureExtractorclase en su punto de construcción, en lugar de acoplarla directamente a esa implementación específica en su función.

πάντα ῥεῖ
fuente
2
No veo una razón para agregar complejidad (una FeatureExtractorclase) solo para introducir aún más complejidad (una interfaz para la SentimentAnalyserclase). Si es deseable el desacoplamiento, entonces extract_featurespuede tomar la get_sentimentfunción como un argumento (la loadllamada parece ser independiente de la función y solo requiere sus efectos). También tenga en cuenta que Python no tiene / alienta interfaces.
Warbo
1
@warbo: incluso si proporciona la función como argumento, al convertirla en una función, restringe las implementaciones potenciales que se ajusten al formato de una función, pero si es necesario administrar el estado persistente entre una invocación y siguiente (por ejemplo, CacheingFeatureExtractorao a TimeSeriesDependentFeatureExtractor), entonces un objeto encajaría mucho mejor. Solo porque no hay necesidad de un objeto actualmente no significa que nunca lo habrá.
Jules
3
@Jules En primer lugar, no lo necesitará (YAGNI), en segundo lugar, las funciones de Python pueden hacer referencia al estado persistente (cierres) si lo necesita (no lo va a hacer), en tercer lugar, el uso de una función no restringe nada, ya que cualquier objeto con un __call__el método será compatible si lo necesita (no lo hará), en cuarto lugar al agregar un contenedor como FeatureExtractorsi estuviera haciendo que el código sea incompatible con todos los demás códigos escritos (a menos que proporcione un __call__método, en cuyo caso una función sería claramente más simple) )
Warbo
0

Patrones y todo el lenguaje / conceptos sofisticados a un lado: lo que has encontrado es un trabajo o un proceso por lotes .

Al final del día, incluso un programa OOP puro necesita ser impulsado de alguna manera por algo, para realmente realizar el trabajo; Debe haber un punto de entrada de alguna manera. En el patrón MVC, por ejemplo, el controlador "C" recibe eventos de clic, etc. de la GUI y luego organiza los otros componentes. En las herramientas de línea de comandos clásicas, una función "principal" haría lo mismo.

¿Es correcto crear clases que no representan una cosa sino que hacen una cosa?

Su clase representa una entidad que hace algo y organiza todo lo demás. Puede nombrarlo Controlador , Trabajo , Principal o lo que se le ocurra.

Y si extract_features () se vería así: ¿vale la pena crear una clase especial para contener ese método?

Eso depende de las circunstancias (y no estoy familiarizado con la forma habitual en Python). Si esto es solo una pequeña herramienta de línea de comando de un solo disparo, entonces un método en lugar de una clase debería estar bien. La primera versión de su programa puede salirse con la suya con un método seguro. Si, más tarde, descubres que terminas con docenas de tales métodos, tal vez incluso con variables globales mezcladas, entonces es hora de refactorizar en clases.

AnoE
fuente
3
Tenga en cuenta que en escenarios como este, llamar a los procedimientos independientes "métodos" puede ser confuso. La mayoría de los lenguajes, incluido Python, los llaman "funciones". "Métodos" son funciones / procedimientos que están vinculados a una instancia particular de una clase, que es lo opuesto a su uso del término :)
Warbo
Es cierto, @Warbo. O podríamos llamarlos procedimiento o defun o sub o ...; y pueden ser métodos de clase (sic) no relacionados con una instancia. Espero que el amable lector pueda abstraer el significado deseado. :)
AnoE
@Warbo, ¡es bueno saberlo! La mayoría del material de aprendizaje con el que me he encontrado afirma que los términos función y método son intercambiables, y que es simplemente una preferencia dependiente del idioma.
Dom
@Dom En general, una "función" ("pura") es una asignación de valores de entrada a valores de salida; un "procedimiento" es una función que también puede causar efectos (por ejemplo, eliminar un archivo); ambos se envían estáticamente (es decir, se buscan en el ámbito léxico). Un "método" es una función o procedimiento (generalmente) que se despacha dinámicamente (se busca) desde un valor (llamado "objeto"), que se vincula automáticamente a un argumento (implícito thiso explícito self) del método. Los métodos de un objeto son recursivos mutuamente "abiertamente", por lo que el reemplazo foohace que todas las self.foollamadas usen este reemplazo.
Warbo
0

Podemos pensar en OOP como un modelo del comportamiento de un sistema. Tenga en cuenta que el sistema no tiene que existir en el "mundo real", aunque las metáforas del mundo real a veces pueden ser útiles (por ejemplo, "tuberías", "fábricas", etc.).

Si nuestro sistema deseado es demasiado complicado para modelarlo todo de una vez, podemos dividirlo en pedazos más pequeños y modelarlos (el "dominio del problema"), lo que puede implicar descomponerse aún más, y así sucesivamente hasta que lleguemos a pedazos cuyo comportamiento coincida (más o menos) el de algún objeto de lenguaje incorporado como un número, una cadena, una lista, etc.

Una vez que tenemos esas piezas simples, podemos combinarlas para describir el comportamiento de piezas más grandes, que podemos combinar en piezas aún más grandes, y así sucesivamente hasta que podamos describir todos los componentes del dominio que se necesitan para un todo sistema.

Es en esta fase de "combinación" donde podríamos escribir algunas clases. Escribimos clases cuando no hay un objeto existente que se comporte de la manera que queremos. Por ejemplo, nuestro dominio puede contener "foos", colecciones de foos llamados "bares" y colecciones de bares llamados "bazs". Podemos notar que los foos son lo suficientemente simples como para modelar con cadenas, por lo que hacemos eso. Encontramos que las barras requieren que sus contenidos obedezcan alguna restricción particular que no coincide con nada de lo que Python proporciona, en cuyo caso podríamos escribir una nueva clase para hacer cumplir esta restricción. Quizás los bazs no tienen tales peculiaridades, por lo que podemos representarlos con una lista.

Tenga en cuenta que podríamos escribir una nueva clase para cada uno de esos componentes (foos, bars y bazs), pero no necesitamos hacerlo si ya hay algo con el comportamiento correcto. En particular, para que una clase sea útil necesita 'proporcionar' algo (datos, métodos, constantes, subclases, etc.), por lo que incluso si tenemos muchas capas de clases personalizadas, debemos usar alguna característica incorporada; por ejemplo, si escribiéramos una nueva clase para foos, probablemente solo contendría una cadena, entonces ¿por qué no olvidar la clase foo y hacer que la clase bar contenga esas cadenas? Tenga en cuenta que las clases también son un objeto incorporado, solo son particularmente flexibles.

Una vez que tenemos nuestro modelo de dominio, podemos tomar algunas instancias particulares de esas piezas y organizarlas en una "simulación" del sistema particular que queremos modelar (por ejemplo, "un sistema de aprendizaje automático para ...").

Una vez que tenemos esta simulación, podemos ejecutarla y listo, tenemos un sistema de aprendizaje automático (simulación de) para ... (o cualquier otra cosa que estemos modelando).


Ahora, en su situación particular, está tratando de modelar el comportamiento de un componente "extractor de características". La pregunta es, ¿hay algún objeto incorporado que se comporte como un "extractor de características", o necesitarás dividirlo en cosas más simples? Parece que los extractores de características se comportan de manera muy parecida a los objetos de función, por lo que creo que estaría bien usarlos como modelo.


Una cosa a tener en cuenta al aprender sobre este tipo de conceptos es que diferentes lenguajes pueden proporcionar diferentes características y objetos integrados (y, por supuesto, ¡algunos ni siquiera usan terminología como "objetos"!). Por lo tanto, las soluciones que tienen sentido en un idioma pueden ser menos útiles en otro (¡esto incluso puede aplicarse a diferentes versiones del mismo idioma!).

Históricamente, gran parte de la literatura de OOP (especialmente "patrones de diseño") se ha centrado en Java, que es bastante diferente de Python. Por ejemplo, las clases de Java no son objetos, Java no tenía objetos de función hasta hace muy poco, Java tiene una verificación de tipo estricta (que fomenta las interfaces y subclases), mientras que Python fomenta la tipificación de pato, Java no tiene objetos de módulo, enteros de Java / flotadores / etc. no son objetos, la metaprogramación / introspección en Java requiere "reflexión", etc.

No estoy tratando de elegir Java (como otro ejemplo, mucha de la teoría OOP gira en torno a Smalltalk, que nuevamente es muy diferente de Python), solo estoy tratando de señalar que debemos pensar con mucho cuidado sobre el contexto y restricciones en las que se desarrollaron las soluciones y si eso coincide con la situación en la que nos encontramos.

En su caso, un objeto de función parece una buena opción. Si se pregunta por qué algunas pautas de "mejores prácticas" no mencionan los objetos de función como una posible solución, ¡podría ser simplemente porque esas pautas fueron escritas para versiones antiguas de Java!

Warbo
fuente
0

Hablando pragmáticamente, cuando tengo una "cosa miscelánea que hace algo importante y debe estar separada", y no tiene un hogar claro, la pongo en una Utilitiessección y la uso como mi convención de nomenclatura. es decir. FeatureExtractionUtility.

Olvídate del número de métodos en una clase; un solo método hoy puede necesitar crecer a cinco métodos mañana. Lo que importa es una estructura organizativa clara y coherente, como un área de servicios públicos para colecciones diversas de funciones.

Dom
fuente