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

40

Esta pregunta es un tanto agnóstica del lenguaje, pero no completamente, ya que la Programación Orientada a Objetos (POO) es diferente en, por ejemplo, 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 creo que podría haber una mejor manera en los lenguajes menos populares como Python.

Mi programa necesita realizar una operación relativamente compleja varias veces. Esa operación requiere una gran cantidad de "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 solo método no es muy agradable, modular, legible, etc.

Ahora estos son los enfoques que vienen a mi 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 como esto:

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 solo tiene sentido 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 que se necesitan internamente 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 se agruparían.

3. Me gusta 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, mejor, enfoque? Si no, ¿cuál de estos enfoques sería mejor y por qué? ¿Hay algo que me falta?

    
pregunta iCanLearn 12.09.2015 - 16:28

6 respuestas

47
  
  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.
  2.   

El problema que veo con este enfoque es que el estado de esta clase solo tiene sentido 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 que el estado interno se oculte del código externo.

Si eso significa que tu clase solo tiene un método público, que así sea. Será mucho más fácil de mantener.

En POO, si tienes una clase que hace exactamente 1 cosa, tiene una superficie pública pequeña y mantiene oculto todo su estado interno, entonces eres (como diría Charlie Sheen) GANANDO . / p>

  
  1. Haga un montón de funciones sin una clase y pase las diversas variables que se necesitan internamente entre ellas (como argumentos).
  2.   

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 se agruparían.

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

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

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 comodidad de las variables globales se ve compensada por el brutal costo de mantenimiento que conlleva. Es por eso que escuchas a viejos locos como yo quejándose sobre la encapsulación todo el tiempo.

La opción ganadora es # 1 .

    
respondido por el MetaFight 12.09.2015 - 17:39
23

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

Consideremos tu 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 la suboperación1? ¿Recopila datos utilizados por suboperación2? Cuando transfieres datos almacenándolos en uno mismo, no puedo decir cómo se relacionan las piezas de funcionalidad. Cuando me veo a mí mismo, algunos de los atributos son del constructor, algunos de la llamada a the_public_method y otros se agregan aleatoriamente 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 con él:

  

Además, las funciones estarían estrechamente relacionadas con   entre sí, pero no se agruparían.

Estarían juntos en un módulo, por lo que se agruparían totalmente.

  

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, ya sea en variables globales o en uno mismo, ocultas las dependencias y las haces parecer menos malas, pero todavía están allí.

Generalmente, cuando surge esta situación, significa que no ha encontrado la manera correcta de descomponer su problema. Le resulta incómodo dividirse en varias funciones porque está intentando dividirlo de manera incorrecta.

Por supuesto, sin ver su función real es difícil adivinar cuál sería una buena sugerencia. Pero da una pequeña pista de lo que está tratando aquí:

  

Mi programa necesita hacer una operación relativamente compleja una serie de   veces. Esa operación requiere mucha "contabilidad", tiene que crear   y borra algunos archivos temporales, etc.

Permítame elegir un ejemplo de algo que se adapte a su descripción, un instalador. Un instalador tiene que copiar un montón de archivos, pero si lo cancela a mitad de camino, debe rebobinar todo el proceso, incluida la recuperación de los archivos que haya reemplazado. El algoritmo para esto se ve algo como:

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 multiplica esa lógica para tener que hacer configuraciones de registro, íconos de programas, etc., y tienes bastante desorden.

Creo que tu 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 se comunican las diferentes piezas.

El enfoque # 2 se ve algo así como:

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 las partes de los datos entre las funciones, pero es muy incómodo.

Entonces, ¿cómo debería 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 del FileTransactionLog que se encarga de realizar la copia temporal y realizar un seguimiento de los archivos que se han copiado. En el caso de excepción, vuelve a copiar 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 mezclando 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 centrarse en el algoritmo básico.

Sospecho que tu 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.

    
respondido por el Winston Ewert 13.09.2015 - 07:23
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 llevar 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 devoluciones:

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 realmente se convierte en solo un detalle de implementación de su función pública. El código de llamada nunca necesita saber acerca de su clase interna.

    
respondido por el cmaster 13.09.2015 - 08:51
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 soporta múltiples paradigmas.

Además de sus soluciones, también existe la posibilidad de completar 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 tu base de código es OOP, viola la coherencia si de repente algunas partes se escriben en un estilo (más) funcional.

    
respondido por el Thomas Junk 12.09.2015 - 23:42
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 propia pregunta se centran en la mecánica de codificación. Pero esto 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 alguna otra clase o su comportamiento puede distribuirse entre clases.

El diseño orientado a objetos tiene que ver con la complejidad

El objetivo de OO es construir y mantener con éxito sistemas grandes y complejos encapsulando el código en términos del propio sistema. El diseño "correcto" 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 proporcionan estructura y funcionalidad a lo largo de la respiración y la profundidad del sistema, interactúan e integran estas dimensiones.

Teniendo en cuenta esto, a menudo se dice que las funciones que cuelgan sobre el sistema, la clase de utilidad general demasiado ubicua, es un olor de código que sugiere un diseño insuficiente. Tiendo a estar de acuerdo.

    
respondido por el radarbob 14.09.2015 - 00:24
3

¿Por qué no compara sus necesidades con algo que existe en la biblioteca estándar de Python y luego observa 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 declaración nonlocal para permitirle cambiar las variables en su función principal.

Puede que aún le resulte útil tener algunas clases privadas simples dentro de su función para implementar abstracciones y operaciones de ordenación.

    
respondido por el meuh 12.09.2015 - 17:01

Lea otras preguntas en las etiquetas