Classe che non rappresenta nulla - è corretta?


42

Sto solo progettando la mia applicazione e non sono sicuro di aver compreso correttamente SOLID e OOP. Le classi dovrebbero fare 1 cosa e farlo bene, ma d'altra parte dovrebbero rappresentare oggetti reali con cui lavoriamo.

Nel mio caso eseguo un'estrazione di funzionalità su un set di dati e quindi eseguo un'analisi di apprendimento automatico. Presumo che potrei creare tre classi

  1. FeatureExtractor
  2. DataSet
  3. Analizzatore

Ma la classe FeatureExtractor non rappresenta nulla, fa qualcosa che lo rende più una routine che una classe. Avrà solo una funzione che verrà utilizzata: extract_features ()

È corretto creare classi che non rappresentano una cosa ma fanno una cosa sola?

EDIT: non sono sicuro che sia importante, ma sto usando Python

E se extract_features () sarebbe simile a questo: vale la pena creare una classe speciale per contenere quel metodo?

def extract_features(df):
    extr = PhrasesExtractor()
    extr.build_vocabulary(df["Text"].tolist())

    sent = SentimentAnalyser()
    sent.load()

    df = add_features(df, extr.features)
    df = mark_features(df, extr.extract_features)
    df = drop_infrequent_features(df)
    df = another_processing1(df)
    df = another_processing2(df)
    df = another_processing3(df)
    df = set_sentiment(df, sent.get_sentiment)
    return df

13
Sembra perfettamente perfetto come funzione. Considerare le tre cose che hai elencato come moduli è ok, e potresti volerle mettere in file diversi, ma ciò non significa che debbano essere classi.
Bergi,

31
Tieni presente che è abbastanza comune e accettabile utilizzare approcci non OO in Python.
jpmc26

13
Potresti essere interessato a Domain Driven Design. Le "classi dovrebbero rappresentare oggetti dal mondo reale" è in realtà falsa ... dovrebbero rappresentare oggetti nel dominio . Il dominio è spesso fortemente legato al mondo reale, ma a seconda dell'applicazione alcune cose possono o non possono essere considerate oggetti, o alcune cose che "in realtà" sono separate possono finire collegate o identiche all'interno del dominio dell'applicazione.
Bakuriu,

1
Man mano che acquisisci familiarità con OOP, penso che scoprirai che le classi raramente corrispondono una a una con entità del mondo reale. Ad esempio, ecco un saggio che sostiene che cercare di stipare tutte le funzionalità associate a un'entità del mondo reale in una singola classe è molto spesso un anti-modello: programmatore.97things.oreilly.com/wiki/index.php/…
Kevin - Ripristina Monica il

1
"dovrebbero rappresentare oggetti reali con cui lavoriamo". non necessariamente. Molte lingue hanno una classe stream che rappresenta un flusso di byte, che è un concetto astratto piuttosto che un "oggetto reale". Tecnicamente un file system non è neanche un "oggetto reale", è solo un concetto, ma a volte ci sono classi che rappresentano un file system o una parte di esso.
Pharap,

Risposte:


96

Le lezioni dovrebbero fare 1 cosa e farlo bene

Sì, questo è generalmente un buon approccio.

ma d'altra parte dovrebbero rappresentare l'oggetto reale con cui lavoriamo.

No, questo è un malinteso comune IMHO. L'accesso di un buon principiante a OOP è spesso "iniziare con oggetti che rappresentano cose del mondo reale" , è vero.

Tuttavia, non dovresti smettere con questo !

Le classi possono (e dovrebbero) essere utilizzate per strutturare il programma in vari modi. Modellare oggetti dal mondo reale è un aspetto di questo, ma non l'unico. La creazione di moduli o componenti per un'attività specifica è un altro caso d'uso sensato per le classi. Un "estrattore di funzioni" è probabilmente un tale modulo, e anche se contiene solo un metodo pubblicoextract_features() , sarei sorpreso se se non contenga anche molti metodi privati ​​e forse qualche stato condiviso. Quindi avere una classe FeatureExtractorintrodurrà un luogo naturale per questi metodi privati.

Nota a margine: in linguaggi come Python che supportano un concetto di modulo separato si può anche usare un modulo FeatureExtractorper questo, ma nel contesto di questa domanda, questa è IMHO una differenza trascurabile.

Inoltre, un "estrattore di funzionalità" può essere immaginato come "una persona o un bot che estrae funzionalità". Questa è una "cosa" astratta, forse non una cosa che troverai nel mondo reale, ma il nome stesso è un'astrazione utile, che dà a tutti un'idea di quale sia la responsabilità di quella classe. Quindi non sono d'accordo sul fatto che questa classe non "rappresenti nulla".


33
Mi piace soprattutto che tu abbia menzionato la questione dello stato interno. Trovo spesso che sia questo il fattore decisivo per stabilire se faccio qualcosa in una classe o in una funzione in Python.
David Z,

17
Sembri raccomandare la "classite". Non fare una lezione per questo, è un antipattern in stile Java. Se è una funzione, rendila una funzione. Lo stato interno è irrilevante: le funzioni possono averlo (tramite chiusure).
Konrad Rudolph,

25
@KonradRudolph: sembra che tu abbia perso che non si tratta di fare una funzione. Si tratta di un pezzo di codice che richiede diverse funzioni, un nome comune e forse uno stato condiviso. L'uso di un modulo per questo potrebbe essere sensato nelle lingue con un concetto di modulo a parte le classi.
Doc Brown,

8
@DocBrown Non dovrò essere d'accordo. È una classe con un metodo pubblico. Per quanto riguarda le API, questo è indistinguibile da una funzione. Vedi la mia risposta per i dettagli. L'uso di classi a metodo singolo può essere giustificato se è necessario creare un tipo specifico per rappresentare uno stato. Ma questo non è il caso qui (e anche allora, a volte le funzioni possono essere utilizzate).
Konrad Rudolph,

6
@DocBrown Sto iniziando a capire cosa intendi: stai parlando delle funzioni che vengono chiamate dall'interno extract_features? Ho solo supposto che fossero funzioni pubbliche da un altro posto. Abbastanza giusto, sono d'accordo che se questi sono privati, dovrebbero probabilmente entrare in un modulo (ma comunque: non una classe, a meno che non condividano lo stato), insieme a extract_features. (Detto questo, puoi ovviamente dichiararli localmente all'interno di quella funzione.)
Konrad Rudolph,

44

Doc Brown è perfetto: le classi non devono rappresentare oggetti del mondo reale. Devono solo essere utili . Le classi sono fondamentalmente solo tipi aggiuntivi, e cosa corrisponde into stringcorrisponde nel mondo reale? Sono descrizioni astratte, non cose concrete e tangibili.

Detto questo, il tuo caso è speciale. Secondo la tua descrizione:

E se extract_features () sarebbe simile a questo: vale la pena creare una classe speciale per contenere quel metodo?

Hai assolutamente ragione: se il tuo codice è come mostrato, è inutile trasformarlo in una classe. C'è un famoso discorso che sostiene che tali usi delle classi in Python sono odori di codice e che spesso sono sufficienti funzioni semplici. Il tuo caso ne è un perfetto esempio.

L'uso eccessivo delle classi è dovuto al fatto che OOP divenne mainstream con Java negli anni '90. Purtroppo a quel tempo a Java mancavano diverse funzionalità del linguaggio moderno (come le chiusure), il che significa che molti concetti erano difficili o impossibili da esprimere senza l'uso di classi. Ad esempio, fino a poco tempo fa in Java non era possibile disporre di metodi con stato (ovvero chiusure). Invece, dovevi scrivere una classe per portare lo stato e che esponesse un singolo metodo (chiamato qualcosa di simile invoke).

Sfortunatamente questo stile di programmazione è diventato popolare ben oltre Java (in parte a causa di un libro di ingegneria software influente che sarebbe altrimenti molto utile), anche in linguaggi che non richiedono tali soluzioni alternative.

In Python, le classi sono ovviamente uno strumento molto importante e dovrebbero essere usate liberamente. Ma non sono l' unico strumento e non c'è motivo di usarli dove non hanno senso. È un'idea sbagliata comune che le funzioni libere non abbiano spazio in OOP.


Sarebbe utile aggiungere che se le funzioni chiamate nell'esempio sono in effetti funzioni private, incapsularle in un modulo o in una classe sarebbe del tutto appropriato. Altrimenti, sono completamente d'accordo.
Leliel,

11
Utile anche ricordare che "OOP" non significa "scrivere un mucchio di classi". Le funzioni sono oggetti in Python, quindi non è necessario considerarlo come "non OOP". Piuttosto, sta semplicemente riutilizzando i tipi / le classi incorporati e il "riutilizzo" è uno dei santi graal nella programmazione. Avvolgere questo in una classe impedirebbe il riutilizzo, poiché nulla sarebbe compatibile con questa nuova API (a meno che non __call__sia definito, nel qual caso basta usare una funzione!)
Warbo

3
Anche in tema di "classi contro funzioni indipendenti": eev.ee/blog/2013/03/03/…
Joker_vD

1
Non è così, anche se in Python le "funzioni libere" sono anche oggetti con un tipo che espongono un metodo __call__()? È davvero così diverso da un'istanza di classe interna anonima? Sintatticamente, certo ma da un design linguistico, sembra una distinzione meno significativa di quella che presenti qui.
JimmyJames,

1
@JimmyJames Right. Il punto è che offrono le stesse funzionalità per lo scopo specifico ma sono più semplici da usare.
Konrad Rudolph,

36

Sto solo progettando la mia applicazione e non sono sicuro di aver compreso correttamente SOLID e OOP.

Sono stato in questo oltre 20 anni e non ne sono nemmeno sicuro.

Le lezioni dovrebbero fare 1 cosa e farlo bene

Difficile sbagliare qui.

dovrebbero rappresentare oggetti reali con cui lavoriamo.

Oh veramente? Lasciate che vi presenti alla classe singolo più popolare e di successo di tutti i tempi: String. Lo usiamo per il testo. E l'oggetto del mondo reale che rappresenta è questo:

Il pescatore tiene 10 pesci sospesi da una corda

Perché no, non tutti i programmatori sono ossessionati dalla pesca. Qui stiamo usando qualcosa chiamato metafora. Va bene creare modelli di cose che non esistono davvero. È l'idea che deve essere chiara. Stai creando immagini nella mente dei tuoi lettori. Quelle immagini non devono essere reali. Ho appena capito facilmente.

Un buon design OOP raggruppa i messaggi (metodi) attorno ai dati (stato) in modo che le reazioni a tali messaggi possano variare a seconda di tali dati. Se fare questo modella qualcosa del mondo reale, spiffy. Altrimenti, vabbè. Finché ha senso per il lettore, va bene.

Ora certo, potresti pensarlo in questo modo:

lettere festive sospese a una stringa leggere "LETS DO THINGS!"

ma se pensi che questo debba esistere nel mondo reale prima di poter usare la metafora, la tua carriera di programmatore coinvolgerà molte arti e mestieri.


1
Una bella serie di immagini ...
Zev Spitz,

A questo punto non sono sicuro che "stringa" sia persino metaforica. Ha solo un significato specifico nel dominio della programmazione, così come le parole come classe tablee column...
Kyralessa,

@Kyralessa tu etere insegni al neofita la metafora o lasci che sia magico per loro. Per favore, salvami dai programmatori che credono nella magia.
candied_orange

6

Attenzione! In nessun caso SOLID afferma che una classe dovrebbe "fare solo una cosa". In tal caso, le classi avrebbero sempre e solo un singolo metodo e non ci sarebbe davvero una differenza tra classi e funzioni.

SOLID afferma che una classe dovrebbe rappresentare un'unica responsabilità . Questi sono un po 'come le responsabilità delle persone in una squadra: l'autista, l'avvocato, il borseggiatore, il graphic designer ecc. Ognuna di queste persone può svolgere più attività (correlate), ma tutte relative a un'unica responsabilità.

Il punto è questo: se c'è un cambiamento nei requisiti, idealmente devi solo modificare una singola classe. Ciò semplifica la comprensione del codice, la modifica e la riduzione del rischio.

Non esiste una regola che un oggetto debba rappresentare "una cosa reale". Questa è solo una tradizione di culto del carico da quando OO è stato inizialmente inventato per l'uso nelle simulazioni. Ma il tuo programma non è una simulazione (poche applicazioni OO moderne lo sono), quindi questa regola non si applica. Finché ogni classe ha una responsabilità ben definita, dovresti stare bene.

Se una classe ha davvero un solo metodo e la classe non ha alcuno stato, potresti considerare di renderla una funzione autonoma. Questo va decisamente bene e segue i principi KISS e YAGNI: non è necessario creare una classe se è possibile risolverla con una funzione. D'altra parte, se hai motivo di credere che potresti aver bisogno di stato interno o implementazioni multiple, potresti anche renderlo una classe in anticipo. Dovrai usare il tuo miglior giudizio qui.


+1 per "non è necessario creare una classe se è possibile risolverla con una funzione". A volte qualcuno ha bisogno di dire la verità.
tchrist,

5

È corretto creare classi che non rappresentano una cosa ma fanno una cosa sola?

In generale va bene.

Senza una descrizione un po 'più specifica di ciò che la FeatureExtractorclasse dovrebbe fare esattamente è difficile da dire.

Ad ogni modo, anche se FeatureExtractorespone solo una extract_features()funzione pubblica , potrei pensare di configurarla con una Strategyclasse, che determina esattamente come dovrebbe essere eseguita l'estrazione.

Un altro esempio è una classe con una funzione Template .

E ci sono altri modelli di progettazione comportamentale , che si basano su modelli di classe.


Come hai aggiunto del codice per chiarimenti.

E se extract_features () sarebbe simile a questo: vale la pena creare una classe speciale per contenere quel metodo?

La linea

 sent = SentimentAnalyser()

comprende esattamente ciò che intendevo dire che potresti configurare una classe con una strategia .

Se hai un'interfaccia per quella SentimentAnalyserclasse, puoi passarla alla FeatureExtractorclasse nel suo punto di costruzione, anziché accoppiarla direttamente a quella specifica implementazione nella tua funzione.


2
Non vedo un motivo per aggiungere complessità (una FeatureExtractorclasse) solo per introdurre ancora più complessità (un'interfaccia per la SentimentAnalyserclasse). Se il disaccoppiamento è desiderabile, allora extract_featurespuò prendere la get_sentimentfunzione come argomento (la loadchiamata sembra essere indipendente dalla funzione e richiede solo i suoi effetti). Si noti inoltre che Python non ha / incoraggia interfacce.
Warbo,

1
@warbo - anche se si fornisce la funzione come argomento, rendendola una funzione, si limitano le potenziali implementazioni che si adatteranno al formato di una funzione, ma se è necessario gestire lo stato persistente tra una chiamata e il successivamente (ad es. a CacheingFeatureExtractoro a TimeSeriesDependentFeatureExtractor), un oggetto sarebbe molto più adatto. Solo perché non c'è bisogno di un oggetto attualmente non significa che non ci sarà mai.
Jules,

3
@Jules In primo luogo non ne avrai bisogno (YAGNI), in secondo luogo le funzioni di Python possono fare riferimento allo stato persistente (chiusure) se ne hai bisogno (non hai intenzione di farlo), in terzo luogo l'uso di una funzione non limita nulla poiché qualsiasi oggetto con un __call__il metodo sarà compatibile se ne hai bisogno (non hai intenzione di farlo), in quarto luogo aggiungendo un wrapper come FeatureExtractorse rendessi il codice incompatibile con tutti gli altri codici mai scritti (a meno che tu non fornisca un __call__metodo, nel qual caso una funzione sarebbe chiaramente più semplice )
Warbo,

0

Modelli e tutto il linguaggio / i concetti fantasiosi a parte: quello che ti sei imbattuto in un processo o in un processo batch .

Alla fine della giornata, anche un puro programma OOP deve essere in qualche modo guidato da qualcosa, per eseguire effettivamente il lavoro; ci deve essere un punto di ingresso in qualche modo. Nel modello MVC, ad esempio, l'ontroller "C" riceve gli eventi click ecc. Dalla GUI e quindi orchestra gli altri componenti. Nei classici strumenti da riga di comando, una funzione "principale" farebbe lo stesso.

È corretto creare classi che non rappresentano una cosa ma fanno una cosa sola?

La tua classe rappresenta un'entità che fa qualcosa e orchestra tutto il resto. Puoi nominarlo Controller , Job , Main o qualunque cosa ti venga in mente.

E se extract_features () sarebbe simile a questo: vale la pena creare una classe speciale per contenere quel metodo?

Dipende dalle circostanze (e non ho familiarità con il solito modo in Python). Se questo è solo un piccolo strumento da riga di comando one-shot, un metodo anziché una classe dovrebbe andare bene. La prima versione del tuo programma può sicuramente cavarsela con un metodo. Se, in seguito, ti accorgi di finire con dozzine di tali metodi, forse anche con variabili globali mescolate, allora è il momento di rifattorizzare in classi.


3
Si noti che in scenari come questo, chiamare "metodi" di procedure autonome può essere fonte di confusione. Molte lingue, incluso Python, le chiamano "funzioni". I "metodi" sono funzioni / procedure che sono legate a una particolare istanza di una classe, che è l'opposto del tuo uso del termine :)
Warbo

Abbastanza vero, @Warbo. Oppure potremmo chiamarli procedura o defun o sub o ...; e possono essere metodi di classe (sic) non correlati a un'istanza. Spero che il gentile lettore sarà in grado di sottrarre il significato desiderato. :)
AnoE

@Warbo è buono a sapersi! La maggior parte del materiale di apprendimento che ho incontrato afferma che i termini funzione e metodo sono intercambiabili e che si tratta semplicemente di una preferenza dipendente dalla lingua.
Dom

@Dom In generale una "funzione" ("pura") è una mappatura da valori di input a valori di output; una "procedura" è una funzione che può anche causare effetti (ad es. eliminazione di un file); entrambi vengono spediti staticamente (ovvero cercati nell'ambito lessicale). Un "metodo" è una funzione o (di solito) una procedura che viene dinamicamente inviata (cercata) da un valore (chiamato "oggetto"), che viene automaticamente associato a un argomento (implicito thiso esplicito self) del metodo. I metodi di un oggetto sono reciprocamente "apertamente" ricorsivi, quindi la sostituzione foofa sì che tutte le self.foochiamate utilizzino questa sostituzione.
Warbo,

0

Possiamo pensare a OOP come a modellare il comportamento di un sistema. Si noti che il sistema non deve esistere nel "mondo reale", sebbene a volte le metafore del mondo reale possano essere utili (ad esempio "condotte", "fabbriche", ecc.).

Se il nostro sistema desiderato è troppo complicato per modellare tutto in una volta, possiamo scomporlo in pezzi più piccoli e modellarli (il "dominio problematico"), che può comportare un ulteriore abbattimento, e così via fino a quando non arriviamo a pezzi il cui comportamento corrisponde (più o meno) quello di un oggetto linguaggio incorporato come un numero, una stringa, un elenco, ecc.

Una volta che abbiamo quei pezzi semplici, possiamo combinarli insieme per descrivere il comportamento di pezzi più grandi, che possiamo combinare insieme in pezzi ancora più grandi, e così via fino a quando non possiamo descrivere tutti i componenti del dominio necessari per un intero sistema.

È questa fase di "combinazione" in cui potremmo scrivere alcune classi. Scriviamo le classi quando non esiste un oggetto esistente che si comporti come vogliamo. Ad esempio, il nostro dominio potrebbe contenere "foos", raccolte di foos chiamate "bars" e raccolte di barre chiamate "bazs". Potremmo notare che i foo sono abbastanza semplici da modellare con le stringhe, quindi lo facciamo. Scopriamo che le barre richiedono che i loro contenuti obbediscano ad alcuni vincoli particolari che non corrispondono a nulla di ciò che Python fornisce, nel qual caso potremmo scrivere una nuova classe per far rispettare questo vincolo. Forse i baz non hanno tali peculiarità, quindi possiamo semplicemente rappresentarli con un elenco.

Nota che noi potremmo scrivere una nuova classe per ogni uno di quei componenti (Foos, bar e bazs), ma non hanno bisogno di se c'è già qualcosa con il comportamento corretto. In particolare, per una classe per essere utile deve 'offrire' qualcosa (dati, metodi, costanti, sottoclassi, ecc), quindi, anche se abbiamo molti strati di classi personalizzate ci dobbiamo finalmente usare qualche funzione integrata; per esempio, se scrivessimo una nuova classe per foos probabilmente conterrebbe solo una stringa, quindi perché non dimenticare la classe foo e fare in modo che la classe bar contenga quelle stringhe? Tieni presente che le classi sono anche un oggetto incorporato, sono solo particolarmente flessibili.

Una volta che abbiamo il nostro modello di dominio, possiamo prendere alcuni casi particolari di quei pezzi e disporli in una "simulazione" del sistema particolare che vogliamo modellare (ad esempio "un sistema di apprendimento automatico per ...").

Una volta che abbiamo questa simulazione, possiamo eseguirla e hey presto, abbiamo un sistema funzionante (simulazione di a) di machine learning per ... (o qualsiasi altra cosa stessimo modellando).


Ora, nella tua situazione particolare, stai cercando di modellare il comportamento di un componente "estrattore di funzioni". La domanda è: ci sono oggetti incorporati che si comportano come un "estrattore di funzioni" o dovrai scomporlo in cose più semplici? Sembra che gli estrattori di funzioni si comportino in modo molto simile agli oggetti funzione, quindi penso che andrebbe bene usarli come modello.


Una cosa da tenere a mente quando si imparano questi tipi di concetti è che linguaggi diversi possono fornire funzionalità e oggetti integrati diversi (e, naturalmente, alcuni non usano nemmeno la terminologia come "oggetti"!). Quindi le soluzioni che hanno un senso in una lingua potrebbero essere meno utili in un'altra (questo può anche applicarsi a versioni diverse della stessa lingua!).

Storicamente, gran parte della letteratura OOP (in particolare "modelli di progettazione") si è concentrata su Java, che è abbastanza diverso da Python. Ad esempio, le classi Java non sono oggetti, Java non aveva oggetti funzione fino a poco tempo fa, Java ha un rigoroso controllo del tipo (che incoraggia le interfacce e la sottoclasse) mentre Python incoraggia la tipizzazione duck, Java non ha oggetti modulo, numeri interi Java / galleggianti / etc. non sono oggetti, la meta-programmazione / introspezione in Java richiede "riflessione" e così via.

Non sto cercando di scegliere Java (come altro esempio, molta teoria OOP ruota attorno a Smalltalk, che è di nuovo molto diverso da Python), sto solo cercando di sottolineare che dobbiamo pensare molto attentamente al contesto e i vincoli in cui sono state sviluppate le soluzioni e se ciò corrisponde alla situazione in cui ci troviamo.

Nel tuo caso, un oggetto funzione sembra una buona scelta. Se ti stai chiedendo perché alcune linee guida di "best practice" non menzionino gli oggetti funzione come una possibile soluzione, potrebbe essere semplicemente perché quelle linee guida sono state scritte per le vecchie versioni di Java!


0

In termini pragmatici, quando ho una "cosa varia che fa qualcosa di importante e che dovrebbe essere separato", e che non ha una casa chiara, la inserisco in una Utilitiessezione e la uso come mia convenzione di denominazione. vale a dire. FeatureExtractionUtility.

Dimentica il numero di metodi in una classe; un singolo metodo oggi potrebbe dover diventare cinque metodi domani. Ciò che conta è una struttura organizzativa chiara e coerente, come un'area di utilità per varie raccolte di funzioni.

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.