Devo creare una classe se la mia funzione è complessa e ha molte variabili?

40

Questa domanda è in qualche modo indipendente dal linguaggio, ma non completamente, poiché la programmazione orientata agli oggetti (OOP) è diversa, ad esempio, Java , che non ha funzioni di prima classe, rispetto a Python .

In altre parole, mi sento meno in colpa per la creazione di classi non necessarie in un linguaggio come Java, ma sento che potrebbe esserci un modo migliore nei linguaggi meno stereotipati come Python.

Il mio programma deve eseguire un'operazione relativamente complessa un numero di volte. Quell'operazione richiede molta "contabilità", deve creare ed eliminare alcuni file temporanei, ecc.

Ecco perché ha anche bisogno di chiamare molte altre "sotto-operazioni": mettere tutto in un unico enorme metodo non è molto bello, modulare, leggibile, ecc.

Ora questi sono gli approcci che mi vengono in mente:

1. Crea una classe con un solo metodo pubblico e mantiene lo stato interno necessario per le operazioni secondarie nelle sue variabili di istanza.

Sembrerebbe qualcosa del genere:

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.

Il problema che vedo con questo approccio è che lo stato di questa classe ha senso solo internamente e non può fare nulla con le sue istanze, tranne chiamare il loro unico metodo pubblico.

# 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. Crea un sacco di funzioni senza una classe e passa tra le varie variabili necessarie internamente (come argomenti).

Il problema che vedo con questo è che devo passare un sacco di variabili tra le funzioni. Inoltre, le funzioni sarebbero strettamente correlate tra loro, ma non sarebbero raggruppate insieme.

3. Mi piace 2. ma rendi globali le variabili di stato invece di passarle.

Questo non sarebbe affatto positivo, dal momento che devo fare l'operazione più di una volta, con input diversi.

Esiste un quarto approccio migliore? In caso contrario, quale di questi approcci sarebbe migliore e perché? C'è qualcosa che mi manca?

    
posta iCanLearn 12.09.2015 - 16:28
fonte

6 risposte

47
  1. Make a class that has one public method only and keeps the internal state needed for the suboperations in its instance variables.

The problem I see with this approach is that the state of this class makes sense only internally, and can't do anything with its instances except call their only public method.

L'opzione 1 è un buon esempio di incapsulamento usato correttamente. vuoi lo stato interno da nascondere dal codice esterno.

Se questo significa che la tua classe ha solo un metodo pubblico, allora così sia. Sarà molto più facile da mantenere.

In OOP se hai una classe che fa esattamente 1 cosa, ha una piccola superficie pubblica e mantiene tutti i suoi stati interni nascosti, allora sei (come direbbe Charlie Sheen) VINCENTE .

  1. Make a bunch of functions without a class and pass the various internally needed variables between them (as arguments).

The problem I see with this is that I have to pass a lot of variables between functions. Also, the functions would be closely related to each other, but wouldn't be grouped together.

L'opzione 2 soffre di bassa coesione . Ciò renderà la manutenzione più difficile.

  1. Like 2. but make the state variables global instead of passing them.

L'opzione 3, come l'opzione 2, soffre di bassa coesione, ma molto più severamente!

La storia ha dimostrato che la convenienza delle variabili globali è superata dal costo di manutenzione brutale che comporta. Questo è il motivo per cui senti vecchie scoregge come me che sbraitano continuamente sull'incapsulamento.

L'opzione vincente è # 1 .

    
risposta data 12.09.2015 - 17:39
fonte
23

Penso che il # 1 sia in realtà una cattiva opzione.

Consideriamo la tua funzione:

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

Quali parti di dati utilizza la sub-operazione1? Raccoglie i dati utilizzati dalla sub-operazione2? Quando si passano i dati in giro memorizzandoli su sé stessi, non posso dire in che modo sono correlate le funzionalità. Quando guardo me stesso, alcuni degli attributi provengono dal costruttore, alcuni dalla chiamata a_public_method e alcuni aggiunti in modo casuale in altri posti. A mio parere, è un casino.

E il numero 2? Bene, in primo luogo, diamo un'occhiata al secondo problema:

Also, the functions would be closely related to each other, but wouldn't be grouped together.

Sarebbero in un modulo insieme, quindi sarebbero completamente raggruppati.

The problem I see with this is that I have to pass a lot of variables between functions.

Secondo me, questo è buono. Ciò rende esplicite le dipendenze dei dati nel tuo algoritmo. Memorizzandoli sia in variabili globali che in se stessi, nascondi le dipendenze e le fai apparire meno male, ma sono ancora lì.

Di solito, quando si verifica questa situazione, significa che non hai trovato il modo giusto di scomporre il tuo problema. Stai trovando difficile dividere in più funzioni perché stai cercando di dividerlo nel modo sbagliato.

Naturalmente, senza vedere la tua funzione effettiva è difficile indovinare quale sarebbe un buon suggerimento. Ma dai un po 'di quello che hai a che fare qui:

My program needs to do a relatively complex operation a number of times. That operation requires a lot of "bookkeeping", has to create and delete some temporary files etc.

Lasciami scegliere un esempio di qualcosa che si adatta alla tua descrizione, un programma di installazione. Un programma di installazione deve copiare un gruppo di file, ma se lo annulli parzialmente, è necessario riavvolgere l'intero processo, inclusi i file sostituiti. L'algoritmo per questo assomiglia 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)

Ora moltiplica questa logica per dover fare le impostazioni del registro, le icone dei programmi, ecc. e hai un bel pasticcio.

Penso che la tua soluzione numero 1 assomigli a:

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

Questo rende l'algoritmo generale più chiaro, ma nasconde il modo in cui i diversi pezzi comunicano.

L'approccio n.2 assomiglia 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)

Ora è esplicito come parti dei dati si spostano tra le funzioni, ma è molto difficile.

Quindi come dovrebbe essere scritta questa funzione?

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

L'oggetto FileTransactionLog è un gestore di contesto. Quando copy_new_files copia un file, lo fa tramite FileTransactionLog che gestisce la copia temporanea e tiene traccia di quali file sono stati copiati. In caso di eccezione, copia i file originali e, in caso di successo, cancella le copie temporanee.

Funziona perché abbiamo trovato una decomposizione più naturale del compito. In precedenza, stavamo mescolando la logica su come installare l'applicazione con la logica su come ripristinare un'installazione annullata. Ora il registro delle transazioni gestisce tutti i dettagli sui file temporanei e sulla contabilità e la funzione può concentrarsi sull'algoritmo di base.

Sospetto che il tuo caso sia sulla stessa barca. È necessario estrarre gli elementi di contabilità in una sorta di oggetto in modo che il compito complesso possa esprimersi in modo più semplice ed elegante.

    
risposta data 13.09.2015 - 07:23
fonte
9

Poiché l'unico inconveniente apparente del metodo 1 è il suo modello di utilizzo subottimale, penso che la soluzione migliore sia quella di guidare l'incapsulamento un ulteriore passo: usare una classe, ma anche fornire una funzione free standing, che costruisce solo l'oggetto, chiama il metodo e restituisce:

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

Con ciò, hai la più piccola interfaccia possibile per il tuo codice, e la classe che usi internamente diventa davvero solo un dettaglio di implementazione della tua funzione pubblica. Il codice di chiamata non ha bisogno di sapere della tua classe interna.

    
risposta data 13.09.2015 - 08:51
fonte
4

Da una parte, la domanda è in qualche modo indipendente dal linguaggio; ma d'altra parte, l'implementazione dipende dal linguaggio e dai suoi paradigmi. In questo caso è Python, che supporta più paradigmi.

Oltre alle tue soluzioni, c'è anche la possibilità, per fare le operazioni complete stateless in un modo più funzionale, ad es.

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

Tutto si riduce a

  • leggibilità
  • coerenza
  • manutenibilità

Ma -Se il tuo codebase è OOP, viola la coerenza se improvvisamente alcune parti sono scritte in uno (più) stile funzionale.

    
risposta data 12.09.2015 - 23:42
fonte
4

Perché progettare quando posso programmare?

Offro una visione contraria alle risposte che ho letto. Da quella prospettiva tutte le risposte e francamente anche la domanda stessa si concentrano sulla meccanica di codifica. Ma questo è un problema di design.

Should I create a class if my function is complex and has a lot of variables?

Sì, perché ha senso per il tuo design. Può essere una classe a sé o parte di un'altra classe o il suo comportamento può essere distribuito tra le classi.

La progettazione orientata agli oggetti riguarda la complessità

Il punto di OO è creare e mantenere con successo sistemi grandi e complessi incapsulando il codice in termini di sistema stesso. Il design "corretto" dice che tutto è in qualche classe.

Il design di OO gestisce naturalmente la complessità principalmente attraverso classi mirate che rispettano il principio della singola responsabilità. Queste classi danno struttura e funzionalità in tutto il respiro e la profondità del sistema, interagiscono e integrano queste dimensioni.

Detto questo, si dice spesso che le funzioni che pendono sul sistema - la classe di utilità generale troppo onnipresente - è un odore di codice che suggerisce un design insufficiente. Tendo ad essere d'accordo.

    
risposta data 14.09.2015 - 00:24
fonte
3

Perché non paragonare le tue esigenze a qualcosa che esiste nella libreria standard di Python, e poi vedere come viene implementato?

Nota, se non hai bisogno di un oggetto, puoi comunque definire le funzioni all'interno delle funzioni. Con Python 3 c'è la nuova dichiarazione nonlocal per permetterti di cambiare le variabili nella tua funzione genitore.

Potresti comunque trovare utile avere alcune semplici classi private all'interno della tua funzione per implementare astrazioni e riordinare le operazioni.

    
risposta data 12.09.2015 - 17:01
fonte

Leggi altre domande sui tag