Python - Architettura per attributi di istanza correlati

7

Voglio prefigurare questa domanda scusandomi per la sua lunghezza, specialmente per gli esempi di codice; tuttavia, credo di aver incluso il codice minimo necessario per illustrare le differenze negli approcci.

Ho una classe con un numero di attributi di istanza e ognuno di questi attributi ha alcuni attributi rispettivi.

Ad esempio, consideriamo un'applicazione GUI in cui sono presenti numerosi attributi e ogni attributo ha un rispettivo layout e campo. Spesso è necessario eseguire un'azione su ogni layout o campo come gruppo.

Ho trovato 3 modi per organizzare la classe che potrebbe soddisfare le condizioni di cui sopra:

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

Vantaggi e problemi con l'approccio ingenuo

Questo è l'approccio più semplice da implementare e in casi semplici potrebbe andare bene. In questo esempio, che ha solo username e fav_food , non è un grosso problema. Tuttavia, questo approccio non scala bene.

Questo approccio porta ad alcuni attributi seri e attributi con nomi potenzialmente molto lunghi.

È anche una seria seccatura per scorrere tutti i layout o campi. Il metodo di iterazione attraverso __dict__ usato qui dipende dalla denominazione degli attributi - sia che tutti i layout desiderati terminino con _layout , sia che nessun attributo indesiderato termini con _layout .

Approccio Dict

Questo è l'approccio che sto attualmente utilizzando, ma ho iniziato a sospettare che potrei essere pazzo e questa potrebbe essere una pessima soluzione.

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

Vantaggi e problemi con l'approccio Dict

Sono giunto a questo approccio per necessità di raggruppare facilmente gli attributi correlati, come fields e layouts in questo esempio.

Inoltre, questo approccio porta a nomi brevi carini quando si accede a un attributo in ambito limitato, come in captureUsername .

Questo approccio consente l'iterazione abbastanza semplice che è sicura per iterare su tutti e solo gli attributi desiderati.

In termini di svantaggi, questo approccio è, a mia conoscenza, abbastanza anticonvenzionale. Potrebbe confondere le persone che leggono il codice in futuro.

Il metodo di accesso a un attributo nel dict, fornendo una stringa, è un po 'non ideale. Gli IDE non ti cattureranno se commetti un errore digitando la chiave, né forniranno il completamento automatico. Devi essere molto diligente nel nominare le chiavi in modo coerente, o questo approccio potrebbe diventare un disastro.

Approccio orientato agli oggetti

In realtà sono arrivato a questo approccio mentre stavo pensando a questa domanda e digitandola in StackExchange. Non sono sicuro del motivo per cui non ci ho pensato prima. Questo è probabilmente l'approccio più convenzionale; tuttavia, ritengo che abbia alcuni svantaggi rispetto all'approccio 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

Vantaggi e svantaggi del metodo orientato agli oggetti

In un paradigma orientato agli oggetti, questo approccio ha molto senso. Un oggetto ti consente di raggruppare i valori correlati con facilità.

L'approccio Dict, tuttavia, offre un lavoro un po 'migliore all'accesso a tutti gli attributi Field o Layout come gruppo, e iterando attraverso di essi. Ti dà esattamente ciò di cui hai bisogno, piuttosto che iterare attraverso tutti gli attributi dell'istanza e richiede meno convalida.

Quindi, in conclusione, qual è l'approccio preferito a questo problema? Il mio approccio Dict è intrinsecamente sbagliato? Sono corretto che l'approccio orientato agli oggetti sia preferibile? C'è un altro approccio al problema a cui non ho pensato?

    
posta John T 31.01.2017 - 05:21
fonte

1 risposta

2

Gli oggetti sono molto utili nelle interfacce utente. Aiutano a rappresentare gli aspetti multipli naturali di ciascuno dei componenti (ad es. Il loro modello, vista e controller elementi), nonché le loro connessioni.

Quindi OOP è un buon punto di partenza. Vi siete lanciati, tuttavia, dando poca importanza ad alcuni principi chiave OOP come l'ereditarietà e l'occultamento delle informazioni. Inoltre, non sembra che tu abbia adottato una chiara strategia dell'interfaccia utente (che sia MVC o una delle sue ultime alternative come MVP, MVVC, ecc.). Anche alcuni nomi di classi lunghi tendono a rendere il codice più dettagliato di quanto non sia necessario.

Sembra che tu stia cercando di delineare contemporaneamente una struttura e un'applicazione, che è un compito arduo. In pratica, in generale, vorrai fare affidamento su framework esistenti e ben collaudati (ad es. WTForms ) piuttosto che costruirti da solo . Ma per capire, scrivere il tuo può essere utile.

Ecco alcuni miglioramenti che farei come prossimi passi per aumentare la chiarezza, l'astrazione e l'organizzazione 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
    
risposta data 31.01.2017 - 16:17
fonte

Leggi altre domande sui tag