Esta pregunta es algo independiente del lenguaje, pero no del todo, ya que la Programación Orientada a Objetos (OOP) es diferente, por ejemplo, en Java , que no tiene funciones de primera clase, que en Python .
En otras palabras, me siento menos culpable por crear clases innecesarias en un lenguaje como Java, pero siento que podría haber una mejor manera en los lenguajes menos repetitivos como Python.
Mi programa necesita hacer una operación relativamente compleja varias veces. Esa operación requiere mucha "contabilidad", tiene que crear y eliminar algunos archivos temporales, etc.
Es por eso que también necesita llamar a muchas otras "suboperaciones": poner todo en un método enorme no es muy agradable, modular, legible, etc.
Ahora, estos son los enfoques que me vienen a la mente:
1. Cree una clase que tenga un solo método público y mantenga el estado interno necesario para las suboperaciones en sus variables de instancia.
Se vería algo así:
class Thing:
def __init__(self, var1, var2):
self.var1 = var1
self.var2 = var2
self.var3 = []
def the_public_method(self, param1, param2):
self.var4 = param1
self.var5 = param2
self.var6 = param1 + param2 * self.var1
self.__suboperation1()
self.__suboperation2()
self.__suboperation3()
def __suboperation1(self):
# Do something with self.var1, self.var2, self.var6
# Do something with the result and self.var3
# self.var7 = something
# ...
self.__suboperation4()
self.__suboperation5()
# ...
def suboperation2(self):
# Uses self.var1 and self.var3
# ...
# etc.
El problema que veo con este enfoque es que el estado de esta clase tiene sentido solo internamente, y no puede hacer nada con sus instancias excepto llamar a su único método público.
# Make a thing object
thing = Thing(1,2)
# Call the only method you can call
thing.the_public_method(3,4)
# You don't need thing anymore
2. Haga un montón de funciones sin una clase y pase las diversas variables internas necesarias entre ellas (como argumentos).
El problema que veo con esto es que tengo que pasar muchas variables entre funciones. Además, las funciones estarían estrechamente relacionadas entre sí, pero no estarían agrupadas.
3. Como 2. pero haga que las variables de estado sean globales en lugar de pasarlas.
Esto no sería bueno en absoluto, ya que tengo que hacer la operación más de una vez, con diferentes entradas.
¿Hay un cuarto enfoque mejor? Si no, ¿cuál de estos enfoques sería mejor y por qué? ¿Se me escapa algo?
fuente
Respuestas:
La opción 1 es un buen ejemplo de encapsulación utilizada correctamente. Usted desea el estado interno que se oculta de código fuera.
Si eso significa que su clase solo tiene un método público, entonces que así sea. Será mucho más fácil de mantener.
En OOP, si tiene una clase que hace exactamente 1 cosa, tiene una pequeña superficie pública y mantiene todo su estado interno oculto, entonces está (como diría Charlie Sheen) GANADOR .
La opción 2 sufre de baja cohesión . Esto hará que el mantenimiento sea más difícil.
La opción 3, como la opción 2, sufre de baja cohesión, ¡pero mucho más grave!
La historia ha demostrado que la conveniencia de las variables globales se ve compensada por el brutal costo de mantenimiento que conlleva. Es por eso que escuchas viejos pedos como yo despotricando sobre la encapsulación todo el tiempo.
La opción ganadora es la # 1 .
fuente
ThingDoer(var1, var2).do_it()
vsdo_thing(var1, var2)
.def do_thing(var1, var2): return _ThingDoer(var1, var2).run()
para hacer que la API externa sea un poco más bonita.Creo que # 1 es en realidad una mala opción.
Consideremos su función:
¿Qué datos utiliza suboperation1? ¿Recopila datos utilizados por suboperation2? Cuando pasa datos almacenándolos en uno mismo, no puedo decir cómo se relacionan las piezas de funcionalidad. Cuando me miro a mí mismo, algunos de los atributos provienen del constructor, algunos de la llamada al método_público y algunos se agregan al azar en otros lugares. En mi opinión, es un desastre.
¿Qué pasa con el número 2? Bueno, en primer lugar, veamos el segundo problema:
Estarían en un módulo juntos, por lo que estarían totalmente agrupados.
En mi opinión, esto es bueno. Esto hace explícitas las dependencias de datos en su algoritmo. Al almacenarlos en variables globales o en uno mismo, oculta las dependencias y las hace parecer menos malas, pero aún están allí.
Por lo general, cuando surge esta situación, significa que no ha encontrado la manera correcta de descomponer su problema. Le resulta incómodo dividirse en múltiples funciones porque está tratando de dividirlo de la manera incorrecta.
Por supuesto, sin ver su función real es difícil adivinar cuál sería una buena sugerencia. Pero da una pista de lo que está tratando aquí:
Permítanme elegir un ejemplo de algo que se ajuste a su descripción, un instalador. Un instalador tiene que copiar un montón de archivos, pero si lo cancela a la mitad, debe rebobinar todo el proceso, incluida la devolución de los archivos que reemplazó. El algoritmo para esto se parece a:
Ahora multiplique esa lógica por tener que hacer configuraciones de registro, íconos de programas, etc., y tendrá un desastre.
Creo que su solución # 1 se ve así:
Esto hace que el algoritmo general sea más claro, pero oculta la forma en que las diferentes piezas se comunican.
El enfoque n. ° 2 se parece a:
Ahora es explícito cómo se mueven los datos entre las funciones, pero es muy incómodo.
Entonces, ¿cómo debe escribirse esta función?
El objeto FileTransactionLog es un administrador de contexto. Cuando copy_new_files copia un archivo, lo hace a través de FileTransactionLog, que se encarga de hacer la copia temporal y realiza un seguimiento de los archivos que se han copiado. En caso de excepción, copia de nuevo los archivos originales y, en caso de éxito, elimina las copias temporales.
Esto funciona porque hemos encontrado una descomposición más natural de la tarea. Anteriormente, estábamos entremezclando la lógica sobre cómo instalar la aplicación con la lógica sobre cómo recuperar una instalación cancelada. Ahora el registro de transacciones maneja todos los detalles sobre los archivos temporales y la contabilidad, y la función puede enfocarse en el algoritmo básico.
Sospecho que su caso está en el mismo barco. Debe extraer los elementos de contabilidad en algún tipo de objeto para que su tarea compleja pueda expresarse de manera más simple y elegante.
fuente
Dado que el único inconveniente aparente del método 1 es su patrón de uso subóptimo, creo que la mejor solución es impulsar la encapsulación un paso más allá: usar una clase, pero también proporcionar una función independiente, que solo construye el objeto, llama al método y devuelve :
Con eso, tiene la interfaz más pequeña posible para su código, y la clase que usa internamente se convierte realmente en un detalle de implementación de su función pública. El código de llamada nunca necesita saber acerca de su clase interna.
fuente
Por un lado, la pregunta es de alguna manera agnóstica del lenguaje; pero, por otro lado, la implementación depende del lenguaje y sus paradigmas. En este caso, es Python, que admite múltiples paradigmas.
Además de sus soluciones, también existe la posibilidad de realizar las operaciones sin estado de una manera más funcional, por ejemplo
Todo se reduce a
Pero -Si su base de código es OOP, viola la consistencia si de repente algunas partes se escriben en un estilo (más) funcional.
fuente
¿Por qué diseñar cuando puedo codificar?
Ofrezco una visión contraria a las respuestas que he leído. Desde esa perspectiva, todas las respuestas y, francamente, incluso la pregunta en sí se centran en la mecánica de codificación. Pero este es un problema de diseño.
Sí, ya que tiene sentido para su diseño. Puede ser una clase en sí misma o parte de otra clase o su comportamiento puede distribuirse entre las clases.
El diseño orientado a objetos se trata de la complejidad
El objetivo de OO es construir y mantener con éxito sistemas grandes y complejos encapsulando el código en términos del sistema mismo. El diseño "adecuado" dice que todo está en alguna clase.
El diseño OO naturalmente maneja la complejidad principalmente a través de clases enfocadas que se adhieren al principio de responsabilidad única. Estas clases dan estructura y funcionalidad a lo largo de la respiración y la profundidad del sistema, interactúan e integran estas dimensiones.
Dado eso, a menudo se dice que las funciones que cuelgan sobre el sistema, la clase de utilidad general demasiado ubicua, es un olor a código que sugiere un diseño insuficiente. Tiendo a estar de acuerdo.
fuente
¿Por qué no comparar sus necesidades con algo que existe en la biblioteca estándar de Python y luego ver cómo se implementa?
Tenga en cuenta que si no necesita un objeto, aún puede definir funciones dentro de las funciones. Con Python 3 existe la nueva
nonlocal
declaración que le permite cambiar las variables en su función principal.Puede que todavía le resulte útil tener algunas clases privadas simples dentro de su función para implementar abstracciones y operaciones de limpieza.
fuente
nonlocal
ningún lugar en mis bibliotecas python actualmente instaladas. Tal vez pueda tomar el corazón deltextwrap.py
que tiene unaTextWrapper
clase, pero también unadef wrap(text)
función que simplemente crea unaTextWrapper
instancia, llama al.wrap()
método y regresa. Entonces use una clase, pero agregue algunas funciones convenientes.