¿Debo crear una clase si mi función es compleja y tiene muchas variables?

40

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?

puedo aprender
fuente
9
"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". Has articulado esto como un problema, pero no por qué crees que lo es.
Patrick Maupin
@Patrick Maupin - Tienes razón. Y realmente no sé, ese es el problema. Parece que estoy usando clases para algo para lo que debería usarse algo más, y hay muchas cosas en Python que aún no he explorado, así que pensé que tal vez alguien sugeriría algo más adecuado.
iCanLearn
Tal vez se trata de ser claro sobre lo que estoy tratando de hacer. Al igual que, no veo nada particularmente malo con el uso de clases ordinarias en lugar de enumeraciones en Java, pero aún hay cosas para las que las enumeraciones son más naturales. Entonces, esta pregunta es realmente sobre si hay un enfoque más natural de lo que estoy tratando de hacer.
iCanLearn
1
Si simplemente dispersa su código existente en métodos y variables de instancia y no cambia nada sobre su estructura, entonces no gana nada más que perder claridad. (1) es una mala idea.
usr

Respuestas:

47
  1. Cree una clase que solo tenga un método público y mantenga el estado interno necesario para las suboperaciones en sus variables de instancia.

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.

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 .

  1. 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.

La opción 2 sufre de baja cohesión . Esto hará que el mantenimiento sea más difícil.

  1. Como 2. pero haga que las variables de estado sean globales en lugar de pasarlas.

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 .

MetaFight
fuente
66
Sin embargo, el # 1 es una API bastante fea. Si decidiera hacer una clase para esto, probablemente haría una sola función pública que delegue a la clase y haga que toda la clase sea privada. ThingDoer(var1, var2).do_it()vs do_thing(var1, var2).
user2357112 es compatible con Monica el
77
Si bien la opción 1 es un claro ganador, iría un paso más allá. En lugar de usar el estado interno del objeto, use variables locales y parámetros de método. Esto hace que la función pública sea reentrante, lo que es más seguro.
44
Una cosa adicional que podría hacer en la extensión de la opción 1: proteger la clase resultante (agregando un guión bajo delante del nombre) y luego definir una función de nivel de módulo def do_thing(var1, var2): return _ThingDoer(var1, var2).run()para hacer que la API externa sea un poco más bonita.
Sjoerd Job Postmus
44
No sigo tu razonamiento para 1. El estado interno ya está oculto. Agregar una clase no cambia eso. Por lo tanto, no veo por qué recomienda (1). De hecho, no es necesario exponer la existencia de la clase en absoluto.
usr
3
Para mí, cualquier clase que cree una instancia e inmediatamente llame a un método único y que nunca vuelva a usar es un olor de código importante. Es isomorfo a una función simple, solo con flujo de datos oscurecido (ya que la parte dividida de la implementación se comunica asignando variables en la instancia desechable en lugar de pasar parámetros). Si sus funciones internas toman tantos parámetros que las llamadas son complejas y difíciles de manejar, el código no se vuelve menos complicado cuando oculta ese flujo de datos.
Ben
23

Creo que # 1 es en realidad una mala opción.

Consideremos su función:

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()

¿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:

Además, las funciones estarían estrechamente relacionadas entre sí, pero no estarían agrupadas.

Estarían en un módulo juntos, por lo que estarían totalmente agrupados.

El problema que veo con esto es que tengo que pasar muchas variables entre funciones.

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í:

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.

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:

def install_program():
    copied_files = []
    try:
        for filename in FILES_TO_COPY:
           temporary_file = create_temporary_file()
           copy(target_filename(filename), temporary_file)
           copied_files = [target_filename(filename), temporary_file)
           copy(source_filename(filename), target_filename(filename))
     except CancelledException:
        for source_file, temp_file in copied_files:
            copy(temp_file, source_file)
     else:
        for source_file, temp_file in copied_files:
            delete(temp_file)

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í:

class Installer:
    def install(self):
        try:
            self.copy_new_files()
        except CancellationError:
            self.restore_original_files()
        else:
            self.remove_temp_files()

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:

def install_program():
    try:
       temp_files = copy_new_files()
    except CancellationError as error:
       restore_old_files(error.files_that_were_copied)
    else:
       remove_temp_files(temp_files)

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?

def install_program():
    with FileTransactionLog() as file_transaction_log:
         copy_new_files(file_transaction_log)

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.

Winston Ewert
fuente
9

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 :

def publicFunction(var1, var2, param1, param2)
    thing = Thing(var1, var2)
    thing.theMethod(param1, param2)

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.

cmaster
fuente
4

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

def outer(param1, param2):
    def inner1(param1, param2, param3):
        pass
    def inner2(param1, param2):
        pass
    return inner2(inner1(param1),param2,param3)

Todo se reduce a

  • legibilidad
  • consistencia
  • mantenibilidad

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.

Thomas Junk
fuente
4

¿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.

¿Debo crear una clase si mi función es compleja y tiene muchas variables?

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.

radarbob
fuente
El diseño OO gestiona naturalmente la complejidad OOP introduce naturalmente una complejidad innecesaria.
Miles Rout
"Complejidad innecesaria" me recuerda comentarios invaluables de compañeros de trabajo como "oh, son demasiadas clases". La experiencia me dice que vale la pena exprimir el jugo. En el mejor de los casos, veo clases prácticamente enteras con métodos de 1 a 3 líneas y con cada clase haciendo parte, la complejidad de cualquier método se minimiza: un solo LOC corto que compara dos colecciones y devuelve los duplicados; por supuesto, hay un código detrás de eso. No confunda "mucho código" con código complejo. En cualquier caso, nada es gratis.
radarbob
1
Una gran cantidad de código "simple" que interactúa de manera complicada es MUCHO peor que una pequeña cantidad de código complejo.
Miles Rout
1
La pequeña cantidad de código complejo ha contenido complejidad . Hay complejidad, pero solo allí. No se escapa. Cuando tienes una gran cantidad de piezas individualmente simples que trabajan juntas de una manera realmente compleja y difícil de entender, eso es mucho más confuso porque no hay fronteras ni muros para la complejidad.
Miles Rout
3

¿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 nonlocaldeclaració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.

meuh
fuente
Gracias. ¿Y sabes algo en la biblioteca estándar que suene como lo que tengo en la pregunta?
iCanLearn
Me resultaría difícil citar algo usando funciones anidadas, de hecho, no encuentro nonlocalningún lugar en mis bibliotecas python actualmente instaladas. Tal vez pueda tomar el corazón del textwrap.pyque tiene una TextWrapperclase, pero también una def wrap(text)función que simplemente crea una TextWrapper instancia, llama al .wrap()método y regresa. Entonces use una clase, pero agregue algunas funciones convenientes.
meuh