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.
- FeatureExtractor
- Conjunto de datos
- 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
fuente
Respuestas:
Sí, generalmente es un buen enfoque.
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úblico
extract_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 claseFeatureExtractor
introducirá 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
FeatureExtractor
para 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".
fuente
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 conextract_features
. (Dicho esto, por supuesto, podría declararlos localmente dentro de esa función.)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
int
ostring
corresponde en el mundo real? Son descripciones abstractas, no cosas concretas, tangibles.Dicho eso, tu caso es especial. De acuerdo a tu descripción:
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.
fuente
__call__
se defina, en cuyo caso solo use una función!)__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í.He estado en esto durante más de 20 años y tampoco estoy seguro.
Difícil equivocarse aquí.
¿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: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í:
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.
fuente
class
ytable
, ycolumn
...¡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í.
fuente
En general eso está bien.
Sin una descripción un poco más específica, lo que
FeatureExtractor
se supone que la clase debe hacer exactamente es difícil de decir.De todos modos, incluso si
FeatureExtractor
expone solo unaextract_features()
función pública , podría pensar en configurarlo con unaStrategy
clase, 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.
La línea
comprende exactamente lo que quise decir con que podrías configurar una clase con una estrategia .
Si tiene una interfaz para esa
SentimentAnalyser
clase, puede pasarla a laFeatureExtractor
clase en su punto de construcción, en lugar de acoplarla directamente a esa implementación específica en su función.fuente
FeatureExtractor
clase) solo para introducir aún más complejidad (una interfaz para laSentimentAnalyser
clase). Si es deseable el desacoplamiento, entoncesextract_features
puede tomar laget_sentiment
función como un argumento (laload
llamada parece ser independiente de la función y solo requiere sus efectos). También tenga en cuenta que Python no tiene / alienta interfaces.CacheingFeatureExtractor
ao aTimeSeriesDependentFeatureExtractor
), entonces un objeto encajaría mucho mejor. Solo porque no hay necesidad de un objeto actualmente no significa que nunca lo habrá.__call__
el método será compatible si lo necesita (no lo hará), en cuarto lugar al agregar un contenedor comoFeatureExtractor
si 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) )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.
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.
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.
fuente
this
o explícitoself
) del método. Los métodos de un objeto son recursivos mutuamente "abiertamente", por lo que el reemplazofoo
hace que todas lasself.foo
llamadas usen este reemplazo.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!
fuente
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
Utilities
secció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.
fuente