Python - Arquitectura para atributos de instancia relacionados

7

Quiero comenzar con esta pregunta disculpándome por su longitud, especialmente las muestras de código; sin embargo, creo que he incluido el código mínimo necesario para ilustrar las diferencias en los enfoques.

Tengo una clase con varios atributos de instancia, y cada uno de estos atributos tiene algunos atributos respectivos.

Por ejemplo, consideremos una aplicación GUI donde hay varios atributos, y cada atributo tiene un diseño y campo respectivos. A menudo es necesario realizar alguna acción en cada diseño o campo como grupo.

Se me han ocurrido 3 formas de organizar la clase que pueden satisfacer las condiciones anteriores:

Enfoque ingenuo

class EditUserProfile(AbstractApplication):

def __init__(self, username, fav_food):
    self.username = username
    self.username_layout = Layout()
    self.username_field = Field()

    self.fav_food = fav_food
    self.fav_food_layout = Layout()
    self.fav_food_field = Field()

    self.captureUsername()
    self.updateLayouts()

def captureUsername(self):
    if __name__ == '__main__':
        if self.username_field.is_changed():
            self.username = self.username_field.text

        # Now I have to type out the mouthful 'self.username_field'
        # every time I want to access the field
        # even though 'self.username_field' is the only field in
        # the scope of this method.
        # I could do the same thing as the dict solution below:
        #    field = self.username_field
        # But that also kind of goes against Python convention

def updateLayouts(self):

    print("Doing something to Username Layout")
    self.username_layout.doSomething()

    print("Doing something to Favorite Food Layout")
    self.fav_food_layout.doSomething()

    # I now have to manually go through each layout and do the same thing, or I could do this:

    for attr, value in self.__dict__.viewitems()
        if attr.endswith("_layout") and isinstance(value, Layout):
            print("Doing something to %s Layout" % attr)  # Not equivalent to below answers
                                                          # Would need self.[attr]_display_name attributes
            self.value.doSomething()

Ventajas y problemas con el enfoque ingenuo

Este es el enfoque más simple de implementar, y en casos simples puede estar bien. En este ejemplo, que solo tiene username y fav_food no es gran cosa. Sin embargo, este enfoque no se escala bien.

Este enfoque conduce a una gran cantidad de atributos graves, y atributos con nombres potencialmente muy largos.

También es una molestia seria iterar a través de todos los diseños o campos. El método de iteración a través de __dict__ utilizado aquí depende de la denominación de los atributos, tanto que todos los diseños deseados terminan con _layout , como que ningún atributo no deseado termina con _layout .

Enfoque de dictado

Este es el enfoque que estoy usando actualmente, pero comencé a sospechar que podría estar loco y esto podría ser una mala solución.

class EditUserProfile(AbstractApplication):

def __init__(self, username, fav_food):
    self.username = username
    self.fav_food = fav_food

    self.layouts = {}
    self.fields = {}

    self.layouts["Username"] = Layout()
    self.layouts["Favorite Food"] = Layout()

    self.fields["Username"] = Field()
    self.fields["Favorite Food"] = Field()

    self.captureUsername()
    self.updateLayouts()

def captureUsername(self):
    field = self.fields["Username"]

    if field.is_changed():
        self.username = field.text

    # Do a bunch of other stuff with field

def updateLayouts(self):

    for key, value in self.layouts.viewitems():
        print("Doing something to %s Layout" % key)
        value.doSomething()

Ventajas y problemas con el enfoque de Dict

Llegué a este enfoque por la necesidad de agrupar fácilmente los atributos relacionados, como fields y layouts en este ejemplo.

Además, este enfoque conduce a nombres agradables y cortos al acceder a un atributo en un alcance limitado, como en captureUsername .

Este enfoque permite una iteración bastante sencilla que es seguro que se repita en todos y solo en los atributos deseados.

En términos de desventajas, este enfoque es, en mi opinión, bastante poco convencional. Podría confundir a las personas que leen el código en el futuro.

El método para acceder a un atributo en el dict, al proporcionar una cadena, no es ideal. Los IDE no lo atraparán si comete un error al escribir la clave, ni tampoco lo completarán automáticamente. Tienes que ser muy diligente para nombrar las teclas de manera consistente, o este enfoque podría convertirse en un desastre.

Enfoque orientado a objetos

En realidad llegué a este enfoque mientras estaba pensando en esta pregunta y escribiéndola en StackExchange. No estoy seguro de por qué no lo había pensado antes. Este es probablemente el enfoque más convencional; sin embargo, creo que tiene algunas desventajas con respecto al enfoque de Dict.

class EditUserProfile(AbstractApplication):

def __init__(self, username, fav_food):
    self.username = UserProfileItem(username, "Username", Layout(), Field())
    self.fav_food = UserProfileItem(fav_food, "Favorite Food", Layout(), Field())

    self.captureUsername()
    self.updateLayouts()

def captureUsername(self):

    # This still feels a little verbose, but is broken up more nicely
    if self.username.field.is_changed():
        self.username.item = self.username.field.text

    # Do a bunch of other stuff with self.username.field

def updateLayouts(self):

    # You could go through the layout attributes manually,
    # or you could use the class' __dict__ attribute, as below.
    #
    # This still seems less than ideal, as you have to iterate
    # through all of the class' attributes, which could be many,
    # and then check the type.

    for attr, value in self.__dict__.viewitems():
        if isinstance(value, UserProfileItem):
            print("Doing something to %s Layout" % value.name)
            value.layout.doSomething()

class UserProfileItem(object):
    def __init__(self, item, name, layout, field):
        self.item = item
        self.name = name
        self.layout = layout
        self.field = field

Ventajas y desventajas del enfoque orientado a objetos

En un paradigma orientado a objetos, este enfoque tiene bastante sentido. Un objeto le permite agrupar valores relacionados con facilidad.

Sin embargo, el enfoque de Dict hace un mejor trabajo al acceder a todos los atributos Field o Layout como grupo e iterar a través de ellos. Le brinda exactamente lo que necesita, en lugar de iterar a través de todos los atributos de la instancia, y requiere menos validación.

Entonces, en conclusión, ¿cuál es el enfoque preferido para este problema? ¿Es mi enfoque de Dict inherentemente malo? ¿Estoy en lo cierto que es preferible el enfoque orientado a objetos? ¿Hay otro enfoque del problema que no haya pensado?

    
pregunta John T 31.01.2017 - 05:21

1 respuesta

2

Los objetos son muy útiles en las interfaces de usuario. Ayudan a representar los múltiples aspectos naturales de cada uno de los componentes (por ejemplo, su model, view , y el controlador elementos), así como sus conexiones.

OOP es un buen punto de partida. Sin embargo, se ha lanzado a ello, dando poca importancia a algunos principios clave de la POO, como la herencia y el ocultamiento de la información. Tampoco parece haber adoptado una estrategia clara de interfaz de usuario (ya sea MVC o una de sus alternativas de los últimos días, como MVP, MVVC, etc.). Algunos de los nombres de clase largos también tienden a hacer que el código sea más detallado de lo que debe ser.

Parece que estás intentando esbozar un marco y una aplicación al mismo tiempo, lo cual es una tarea difícil. En la práctica, generalmente querrá confiar en los marcos existentes y bien probados (p. Ej., WTForms ) en lugar de crear su propio . Pero para la comprensión, escribir el tuyo puede ser útil.

Aquí hay algunas mejoras que haría como próximos pasos para aumentar la claridad, la abstracción y la organización OOP:

class UserProfile(ProfileApp):
    # shortens the names for clarity

    def __init__(self, username, fav_food):
        super().__init__()
        # initializing superclasses can be important

        self.username = ProfileItem("Username", username)
        self.fav_food = ProfileItem("Favorite Food", fav_food)
        # use constructors with some default behaviors

        self.captureUsername()
        self.updateLayouts()

    def captureUsername(self):
        if self.username.field.is_changed():
            self.username.item = self.username.field.text

    def updateLayouts(self):
        for attr, value in self.profileItems():
            print("Doing something to %s Layout" % value.name)
            value.layout.doSomething()
        # don't deeply inspect and introspect everywhere - instead
        # use higher level abstractions

class ProfileApp(Application):

    def profileItems(self):
        for attr, value in self.__dict__.items():
            if isinstance(value, ProfileItem):
                yield attr, value

   # now a reason to have a super-class - it can do some
   # heavy lifting and help hide information, like how 
   # profile items are iterated

class ProfileItem(object):
    def __init__(self, name, value, layout=None, field=None):
        self.name = name
        self.layout = layout or Layout()
        self.field = field or Field()
        self.field.text = str(value)

    # default some parameters for clarity
    # actually transmit default values into field objects,
    # where that information will be used
    
respondido por el Jonathan Eunice 31.01.2017 - 16:17

Lea otras preguntas en las etiquetas