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, dal momento che la programmazione orientata agli oggetti (OOP) è diversa, ad esempio, in 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 complessi come Python.

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

Ecco perché deve anche chiamare molte altre "operazioni secondarie": mettere tutto in un unico metodo enorme non è molto bello, modulare, leggibile, ecc.

Ora questi sono approcci che mi vengono in mente:

1. Creare una classe che abbia un solo metodo pubblico e mantenga lo stato interno necessario per le operazioni secondarie nelle sue variabili di istanza.

Sarebbe simile a questo:

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 se non 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 gruppo di funzioni senza una classe e passa tra loro le varie variabili necessarie internamente (come argomenti).

Il problema che vedo in questo è che devo passare molte variabili tra le funzioni. Inoltre, le funzioni sarebbero strettamente correlate tra loro, ma non sarebbero raggruppate insieme.

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

Questo non sarebbe affatto positivo, dal momento che devo eseguire 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?


9
"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 se non chiamare il loro unico metodo pubblico." Hai articolato questo problema come un problema, ma non perché lo pensi.
Patrick Maupin,

@Patrick Maupin - Hai ragione. E non lo so davvero, questo è il problema. Mi sento come se stessi usando le classi per qualcosa per cui qualcos'altro dovrebbe essere usato, e ci sono molte cose in Python che non ho ancora esplorato, quindi ho pensato che forse qualcuno avrebbe suggerito qualcosa di più adatto.
iCanLearn,

Forse si tratta di essere chiari su ciò che sto cercando di fare. Ad esempio, non vedo nulla di particolarmente sbagliato nell'usare classi ordinarie al posto degli enum in Java, ma ci sono ancora cose per le quali gli enum sono più naturali. Quindi questa domanda riguarda davvero se esiste un approccio più naturale a ciò che sto cercando di fare.
iCanLearn,


1
Se disperdi il tuo codice esistente in metodi e variabili di istanza e non cambi nulla sulla sua struttura, non vinci nulla ma perdi chiarezza. (1) è una cattiva idea.
usr,

Risposte:


47
  1. Crea una classe che abbia un solo metodo pubblico e mantenga lo stato interno necessario per le operazioni secondarie nelle sue variabili di istanza.

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.

L'opzione 1 è un buon esempio di incapsulamento usato correttamente. Volete che lo stato interno sia nascosto dal codice esterno.

Se ciò significa che la tua classe ha un solo 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 nascosto tutto il suo stato interno, allora sei (come direbbe Charlie Sheen) VINCERE .

  1. Crea un gruppo di funzioni senza una classe e passa tra loro le varie variabili necessarie internamente (come argomenti).

Il problema che vedo in questo è che devo passare molte variabili tra le funzioni. Inoltre, le funzioni sarebbero strettamente correlate tra loro, ma non sarebbero raggruppate insieme.

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

  1. Come 2. ma rendi globali le variabili di stato invece di passarle.

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

La storia ha dimostrato che la convenienza delle variabili globali è compensata dal brutale costo di manutenzione che comporta. Ecco perché ascolti sempre vecchie scoregge come me che urlano incapsulamento.


L'opzione vincente è # 1 .


6
# 1 è un'API piuttosto brutta, però. Se avessi deciso di creare una classe per questo, probabilmente avrei creato un'unica funzione pubblica che delegava alla classe e rendeva privata l'intera classe. ThingDoer(var1, var2).do_it()vs do_thing(var1, var2).
user2357112 supporta Monica il

7
Mentre l'opzione 1 è un chiaro vincitore, farei un ulteriore passo avanti. Invece di utilizzare lo stato dell'oggetto interno, utilizzare le variabili locali e i parametri del metodo. Ciò rende la funzione pubblica rientrante, il che è più sicuro.

4
Un'ulteriore cosa che potresti fare nell'estensione dell'opzione 1: rendere protetta la classe risultante (aggiungendo un carattere di sottolineatura davanti al nome), quindi definire una funzione a livello di modulo def do_thing(var1, var2): return _ThingDoer(var1, var2).run(), per rendere l'API esterna un po 'più carina.
Sjoerd Job Postmus,

4
Non seguo il tuo ragionamento per 1. Lo stato interno è già nascosto. L'aggiunta di una classe non cambia questo. Pertanto non vedo perché mi raccomandi (1). In realtà non è necessario esporre affatto l'esistenza della classe.
usr,

3
Qualsiasi classe su cui crei un'istanza e poi richiami immediatamente un singolo metodo e che poi non utilizzi mai più è un odore importante del codice, per me. È isomorfo rispetto a una funzione semplice, solo con flusso di dati oscurato (poiché i dettagli scomposti dell'implementazione comunicano assegnando variabili sull'istanza usa e getta anziché passare parametri). Se le tue funzioni interne accettano così tanti parametri che le chiamate sono complesse e ingombranti, il codice non diventa meno complicato quando nascondi quel flusso di dati!
Ben

23

Penso che il numero 1 sia in realtà una pessima 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 dati utilizza Suboperation1? Raccoglie i dati utilizzati da suboperation2? Quando si passano i dati archiviandoli su se stessi, non posso dire in che modo le funzionalità sono correlate. Quando guardo me stesso, alcuni degli attributi provengono dal costruttore, alcuni dalla chiamata a the_public_method e altri aggiunti casualmente in altri luoghi. Secondo me, è un casino.

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

Inoltre, le funzioni sarebbero strettamente correlate tra loro, ma non sarebbero raggruppate insieme.

Sarebbero in un modulo insieme, quindi sarebbero totalmente raggruppati insieme.

Il problema che vedo in questo è che devo passare molte variabili tra le funzioni.

Secondo me, va bene. Questo rende esplicite le dipendenze dei dati nel tuo algoritmo. Memorizzandoli in variabili globali o su un sé, nascondi le dipendenze e le fai sembrare meno brutte, ma sono ancora lì.

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

Ovviamente, senza vedere la tua reale funzione è difficile indovinare quale sarebbe un buon suggerimento. Ma dai un piccolo suggerimento a cosa stai trattando qui:

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

Vorrei scegliere un esempio di qualcosa che si adatta alla tua descrizione, un programma di installazione. Un installatore deve copiare un sacco di file, ma se lo annulli a metà strada, devi riavvolgere l'intero processo incluso il rimpiazzo di tutti i file che hai sostituito. L'algoritmo per questo assomiglia a qualcosa del tipo:

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 quella logica per dover fare le impostazioni del registro, le icone dei programmi, ecc. E hai un bel casino.

Penso che la tua soluzione n. 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 più chiaro l'algoritmo generale, ma nasconde il modo in cui comunicano i diversi pezzi.

L'approccio n. 2 è simile al seguente:

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 pezzi di dati si spostano tra le funzioni, ma è molto imbarazzante.

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 nuovamente i file originali e, in caso di successo, elimina le copie temporanee.

Questo funziona perché abbiamo trovato una decomposizione più naturale dell'attività. In precedenza, mescolavamo 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 su file temporanei e contabilità e la funzione può concentrarsi sull'algoritmo di base.

Sospetto che il tuo caso sia nella stessa barca. Devi estrarre gli elementi contabili in una sorta di oggetto in modo che il tuo compito complesso possa essere espresso in modo più semplice ed elegante.


9

Poiché l'unico inconveniente evidente del metodo 1 è il suo modello di utilizzo non ottimale, penso che la soluzione migliore sia guidare l'incapsulamento di un ulteriore passo: utilizzare una classe, ma anche fornire una funzione indipendente, 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 al tuo codice e la classe che usi internamente diventa davvero solo un dettaglio di implementazione della tua funzione pubblica. Il codice chiamante non ha mai bisogno di conoscere la tua classe interna.


4

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

Oltre alle vostre soluzioni, esiste anche la possibilità di eseguire le operazioni senza stato in 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à
  • consistenza
  • manutenibilità

Ma -Se la tua base di codice è OOP, viola la coerenza se improvvisamente alcune parti sono scritte in uno (più) stile funzionale.


4

Perché progettare quando posso codificare?

Offro una visione contrarian alle risposte che ho letto. Da quel punto di vista tutte le risposte e francamente anche la domanda stessa si concentrano sulla meccanica dei codici. Ma questo è un problema di progettazione.

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

Sì, poiché 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.


Il design orientato agli oggetti riguarda la complessità

Il punto di OO è costruire 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.

La progettazione OO gestisce naturalmente la complessità principalmente attraverso classi mirate che aderiscono al principio della responsabilità singola. 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 penzolano sul sistema - la classe di utilità generale fin troppo onnipresente - è un odore di codice che suggerisce un design insufficiente. Tendo ad essere d'accordo.


Il design OO gestisce naturalmente la complessità OOP introduce naturalmente complessità inutili.
Miles Rout,

"Complessità non necessaria" mi ricorda i commenti inestimabili dei colleghi come "oh, sono troppe classi". L'esperienza mi dice che vale la pena spremere il succo. Nella migliore delle ipotesi vedo praticamente intere classi con metodi lunghi 1-3 righe e con ogni classe che fa la sua parte la complessità di un dato metodo è minimizzata: un singolo LOC breve che confronta due raccolte e restituisce i duplicati - ovviamente c'è del codice dietro. Non confondere "molto codice" con codice complesso. In ogni caso, niente è gratuito.
radarbob,

1
Un sacco di codice "semplice" che interagisce in modo complicato è IN MODO peggiore di una piccola quantità di codice complesso.
Miglia in rotta

1
La piccola quantità di codice complesso ha contenuto complessità . C'è complessità, ma solo lì. Non fuoriesce. Quando hai un sacco di pezzi individualmente semplici che lavorano insieme in un modo davvero complesso e difficile da capire, questo è molto più confuso perché non ci sono bordi o muri per la complessità.
Miles Rout,

3

Perché non confrontare le tue esigenze con 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 è disponibile la nuova nonlocaldichiarazione che consente di modificare le variabili nella funzione padre.

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


Grazie. E sai qualcosa nella libreria standard che suona come quello che ho nella domanda?
iCanLearn,

Troverei difficile citare qualcosa usando le funzioni nidificate, anzi non trovo da nonlocalnessuna parte nelle librerie python attualmente installate. Forse puoi prendere il cuore da textwrap.pycui ha una TextWrapperclasse, ma anche una def wrap(text)funzione che crea semplicemente TextWrapper un'istanza, chiama il .wrap()metodo su di essa e ritorna. Quindi usa una classe, ma aggiungi alcune funzioni utili.
Meuh
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.