Le classi con un solo metodo (pubblico) sono un problema?


51

Attualmente sto lavorando a un progetto software che esegue compressione e indicizzazione su filmati di videosorveglianza. La compressione funziona dividendo gli oggetti di sfondo e di primo piano, quindi salvando lo sfondo come immagine statica e il primo piano come uno sprite.

Di recente, ho iniziato a rivedere alcune delle classi che ho progettato per il progetto.

Ho notato che ci sono molte classi che hanno un solo metodo pubblico. Alcune di queste classi sono:

  • VideoCompressor (con un compressmetodo che accetta un video di input di tipo RawVideoe restituisce un video di output di tipo CompressedVideo).
  • VideoSplitter (con un splitmetodo che accetta un video di input di tipo RawVideoe restituisce un vettore di 2 video di output, ciascuno di tipo RawVideo).
  • VideoIndexer (con un indexmetodo che accetta un video di input di tipo RawVideoe restituisce un indice video di tipo VideoIndex).

Mi ritrovo a un'istanza di ogni classe solo per fare chiamate come VideoCompressor.compress(...), VideoSplitter.split(...), VideoIndexer.index(...).

In apparenza, penso che i nomi delle classi siano sufficientemente descrittivi della loro funzione prevista, e in realtà sono nomi. Di conseguenza, anche i loro metodi sono verbi.

Questo è davvero un problema?


14
Dipende dalla lingua. In un linguaggio multi-paradigma come C ++ o Python, queste classi non hanno affari esistenti: i loro "metodi" dovrebbero essere funzioni libere.

3
@delnan: anche in C ++, in genere usi le classi per creare moduli, anche se non hai bisogno delle "capacità OO" complete. In effetti, esiste l'alternativa di usare gli spazi dei nomi invece di raggruppare "funzioni libere" in un modulo, ma non ne vedo alcun vantaggio.
Doc Brown,

5
@DocBrown: C ++ ha spazi dei nomi per questo. In effetti, nel moderno C ++ si usano spesso funzioni gratuite per i metodi, perché possono essere (staticamente) sovraccarichi per tutti gli argomenti mentre i metodi possono essere sovraccaricati solo per l'invocante.
Jan Hudec,

2
@DocBrown: è un nocciolo della domanda. I moduli che usano spazi dei nomi con un solo metodo "esterno" vanno benissimo quando la funzione è sufficientemente complessa. Perché gli spazi dei nomi non fingono di rappresentare nulla e non fingono di essere orientati agli oggetti. Le classi lo fanno, ma classi come questa sono in realtà solo spazi dei nomi. Ovviamente in Java non puoi avere una funzione, quindi questo è il risultato. Peccato per Java.
Jan Hudec,

2
Correlato (quasi un duplicato): programmers.stackexchange.com/q/175070/33843
Heinzi

Risposte:


87

No, questo non è un problema, al contrario. È un segno di modularità e chiara responsabilità della classe. L'interfaccia lean è facile da capire dal punto di vista di un utente di quella classe e incoraggerà un accoppiamento lento. Questo ha molti vantaggi ma quasi nessun inconveniente. Vorrei che più componenti fossero progettati in questo modo!


6
Questa è la risposta corretta e l'ho votata a fondo, ma potresti migliorarla spiegando alcuni dei vantaggi. Penso che ciò che l'istinto dell'OP gli sta dicendo sia un problema, è qualcos'altro ... cioè che VideoCompressor è davvero un'interfaccia. Mp4VideoCompressor è una classe e dovrebbe essere intercambiabile con un altro VideoCompressor senza copiare il codice per un VideoSplitter in una nuova classe.
pdr

1
Scusa ma ti sbagli. Una classe con un singolo metodo dovrebbe essere solo una funzione autonoma.
Miglia rotta

3
@MilesRout: il tuo commento mostra che hai frainteso la domanda: l'OP ha scritto di una classe con un metodo pubblico, non con un metodo (e in effetti intendeva una classe con più metodi privati, come ci ha detto qui in un commento).
Doc Brown,

2
"Una classe con un singolo metodo dovrebbe essere solo una funzione autonoma." Un presupposto grossolano su quale sia quel metodo. Ho una lezione con un metodo pubblico. DIETRO ci sono più metodi in quella classe più altre 5 classi che non hanno assolutamente idea del client. L'eccellente applicazione di SRP produrrà un'interfaccia pulita e semplice della struttura di classe a strati, con quella semplicità che si manifesta fino in cima.
radarbob,

2
@Andy: la domanda era "è un problema" e non "è conforme a una definizione OO specifica"? Inoltre, non c'è assolutamente alcun segno nella domanda che OP significa classi senza stato.
Doc Brown,

26

Non è più orientato agli oggetti. Poiché quelle classi non rappresentano nulla, sono solo vasi per le funzioni.

Ciò non significa che sia sbagliato. Se la funzionalità è sufficientemente complessa o quando è generica (ovvero gli argomenti sono interfacce, non tipi finali concreti), ha senso mettere quella funzionalità in un modulo separato .

Da lì dipende dalla tua lingua. Se la lingua ha funzioni gratuite, dovrebbero essere moduli che esportano funzioni. Perché fingere che sia una classe quando non lo è. Se la lingua non ha funzioni gratuite come ad esempio Java, allora si creano classi con un singolo metodo pubblico. Bene, questo mostra solo i limiti del design orientato agli oggetti. A volte funzionale è semplicemente la migliore corrispondenza.

C'è un caso in cui potresti aver bisogno di una classe con un singolo metodo pubblico perché deve implementare l'interfaccia con un singolo metodo pubblico. Sia per modello di osservatore o iniezione di dipendenza o altro. Anche in questo caso dipende dalla lingua. Nei linguaggi con funzioni di prima classe (C ++ ( std::functiono parametro modello), C # (delegato), Python, Perl, Ruby ( proc), Lisp, Haskell, ...) questi schemi usano tipi di funzione e non hanno bisogno di classi. Java non ha (ancora, sarà nella versione 8) i tipi di funzione, quindi usi interfacce a metodo singolo e corrispondenti classi a metodo singolo.

Ovviamente non sto sostenendo di scrivere un'unica enorme funzione. Dovrebbe avere subroutine private, ma possono essere private nel file di implementazione (spazio dei nomi statico o anonimo a livello di file in C ++) o in una classe di supporto privata che viene istanziata solo all'interno della funzione pubblica ( per archiviare o meno i dati? ).


12
"Perché fingere che sia una lezione quando non lo è." Avere un oggetto invece di una funzione libera consente lo stato e il sottotipo. Ciò potrebbe rivelarsi prezioso in un mondo in cui i requisiti cambiano. Prima o poi è necessario giocare con le impostazioni di compressione video o fornire algoritmi di compressione alternativi. Prima di ciò la parola "classe" ci dice semplicemente che questa funzione appartiene a un modulo software facilmente estensibile e intercambiabile con una chiara responsabilità. Non è quello a cui OO punta veramente?
VIENI DAL

1
Bene, se ha solo un metodo pubblico (e non implica alcun tipo di protezione), non può davvero essere esteso. Naturalmente impacchettare i parametri di compressione potrebbe avere senso nel qual caso diventa oggetto funzione (alcune lingue hanno un supporto separato per quelle, altre no).
Jan Hudec,

3
Questa è una connessione fondamentale tra oggetti e funzioni: una funzione è isomorfa a un oggetto con un solo metodo e senza campi. Una chiusura è isomorfa a un oggetto con un solo metodo e alcuni campi. Una funzione di selezione che restituisce una delle numerose funzioni che si chiudono tutte sullo stesso insieme di variabili è isomorfa a un oggetto. (In effetti, è così che gli oggetti vengono codificati in JavaScript, tranne per il fatto che si utilizza una struttura di dati del dizionario anziché una funzione di selezione.)
Jörg W Mittag,

4
"Non è più orientato agli oggetti. Dato che quelle classi non rappresentano nulla, sono solo vasi per le funzioni." - Questo è sbagliato. Direi che questo approccio è PIÙ orientato agli oggetti. OO non significa che rappresenti un oggetto del mondo reale. Una grande quantità di programmazione si occupa di concetti astratti, ma ciò significa che non è possibile applicare un approccio OO per risolverlo? Sicuramente no. Si tratta più di modularità e astrazione. Ogni oggetto ha una sola responsabilità. Ciò rende semplice avvolgere la testa attorno al programma e apportare modifiche. Spazio insufficiente per elencare i numerosi vantaggi di OOP.
Despertar,

2
@Phoshi: Sì, ho capito. Non ho mai sostenuto che un approccio funzionale non avrebbe funzionato altrettanto bene. Tuttavia, questo non è chiaramente l'argomento. Un compressore video o un transmogrifier video o qualsiasi altra cosa sia ancora un candidato perfettamente valido per un oggetto.
VIENI DAL

13

Ci possono essere motivi per estrarre un determinato metodo in una classe dedicata. Uno di questi motivi è consentire l'iniezione di dipendenza.

Immagina di avere una classe chiamata VideoExporterche, alla fine, dovrebbe essere in grado di comprimere un video. Un modo pulito sarebbe avere un'interfaccia:

interface IVideoCompressor
{
    Stream compress(Video video);
}

che sarebbe implementato in questo modo:

class MpegVideoCompressor : IVideoCompressor
{
    // ...
}

class FlashVideoCompressor : IVideoCompressor
{
    // ...
}

e usato così:

class VideoExporter
{
    // ...
    void export(Destination destination, IVideoCompressor compressor)
    {
        // ...
        destination = compressor(this.source);
        // ...
    }
    // ...
}

Una cattiva alternativa sarebbe quella di avere un VideoExportermetodo che ha molti metodi pubblici e fa tutto il lavoro, compresa la compressione. Diventerebbe rapidamente un incubo per la manutenzione, rendendo difficile aggiungere supporto per altri formati video.


2
La tua risposta non distingue tra metodi pubblici e privati. Potrebbe essere inteso come una raccomandazione per mettere tutto il codice di una classe in un solo metodo, ma immagino che non sia quello che intendevi?
Doc Brown,

2
@DocBrown: i metodi privati ​​non sono rilevanti per questa domanda. Possono essere messi in classe helper interna o altro.
Jan Hudec,

2
@JanHudec: attualmente il testo è "Una cattiva alternativa sarebbe quella di avere un VideoExporter che abbia molti metodi" - ma dovrebbe essere "Una cattiva alternativa sarebbe avere un VideoExporter che abbia molti metodi pubblici ".
Doc Brown,

@DocBrown: accetta qui.
Jan Hudec,

2
@DocBrown: grazie per le tue osservazioni. Ho modificato la mia risposta. Inizialmente, ho pensato che si supponesse che la domanda (e quindi la mia risposta) riguardasse solo metodi pubblici. Sembra che non sia così ovvio.
Arseni Mourzenko,

6

Questo è un segno che si desidera passare funzioni come argomenti ad altre funzioni. Immagino che la tua lingua (Java?) Non la supporti; in tal caso, non è tanto un fallimento nel tuo design quanto è un difetto nella tua lingua preferita. Questo è uno dei maggiori problemi con le lingue che insistono sul fatto che tutto deve essere una classe.

Se in realtà non stai passando queste funzioni false, allora vuoi solo una funzione libera / statica.


1
Da Java 8, hai lambda, quindi puoi praticamente passare le funzioni.
Silviu Burcea,

Punto giusto, ma che non sarà ufficialmente rilasciato per un po 'di più e alcuni posti di lavoro sono lenti a passare alle versioni più recenti.
Doval,

La data di uscita è a marzo. Inoltre, le build EAP erano molto popolari per JDK 8 :)
Silviu Burcea,

Sebbene, poiché i lambda Java 8 siano semplicemente un modo abbreviato per definire gli oggetti con un solo metodo pubblico, oltre a salvare un po 'di codice del boilerplate, qui non fanno alcuna differenza.
Jules,

5

So di essere in ritardo alla festa, ma come sembra che tutti abbiano perso di sottolineare questo:

Questo è un modello di design ben noto chiamato: Pattern di strategia .

Il modello di strategia viene utilizzato quando esistono diverse possibili strategie per risolvere un sotto-problema. In genere si definisce un'interfaccia che impone un contratto su tutte le implementazioni e quindi si utilizza una forma di Iniezione delle dipendenze per fornire la strategia concreta per l'utente.

Ad esempio in questo caso potresti avere interface VideoCompressore quindi avere diverse implementazioni alternative per esempio class H264Compressor implements VideoCompressore class XVidCompressor implements VideoCompressor. Dall'OP non è chiaro che sia coinvolta un'interfaccia, anche se non lo è, potrebbe semplicemente essere che l'autore originale abbia lasciato la porta aperta per implementare il modello di strategia, se necessario. Che di per sé è anche un buon design.

Il problema che OP si trova costantemente a istanziare le classi per chiamare un metodo è un problema con lei che non usa correttamente l'iniezione di dipendenza e il modello di strategia. Invece di istanziarlo dove serve, la classe contenente dovrebbe avere un membro con l'oggetto strategia. E questo membro dovrebbe essere iniettato, ad esempio nel costruttore.

In molti casi il modello di strategia si traduce in classi di interfaccia (come si sta mostrando) con un solo doStuff(...)metodo.


1
public interface IVideoProcessor
{
   void Split();

   void Compress();

   void Index();
}

Quello che hai è modulare e va bene, ma se dovessi raggruppare queste responsabilità in IVideoProcessor, probabilmente avrebbe più senso dal punto di vista di DDD.

D'altra parte, se la divisione, la compressione e l'indicizzazione non fossero in alcun modo correlate, li terrei come componenti separati.


Poiché la ragione di ciascuna di queste funzioni che è necessario cambiare è in qualche modo diversa, direi che metterle insieme in questo modo viola SRP.
Jules,

0

È un problema: stai lavorando dall'aspetto funzionale del design, piuttosto che dai dati. Quello che hai effettivamente sono 3 funzioni autonome che sono state richieste da OO.

Ad esempio, hai una classe VideoCompressor. Perché stai lavorando con una classe progettata per comprimere video - perché non hai una classe Video con metodi su di essa per comprimere i dati (video) che contiene ogni oggetto di questo tipo?

Quando si progettano sistemi OO, è meglio creare classi che rappresentano oggetti, piuttosto che classi che rappresentano attività che è possibile applicare. Ai vecchi tempi, le classi venivano chiamate tipi: OO era un modo per estendere una lingua con il supporto per nuovi tipi di dati. Se pensi a OO in questo modo, ottieni un modo migliore di progettare le tue lezioni.

MODIFICARE:

lasciami provare a spiegarmi un po 'meglio, immagina una classe di stringhe che ha un metodo concat. Puoi implementare una cosa del genere in cui ogni oggetto istanziato dalla classe contenga i dati della stringa, così puoi dire

string mystring("Hello"); 
mystring.concat("World");

ma l'OP vuole che funzioni così:

string mystring();
string result = mystring.concat("Hello", "World");

ora ci sono luoghi in cui una classe può essere usata per contenere una raccolta di funzioni correlate, ma che non è OO, è un modo pratico di usare le funzionalità OO di una lingua per aiutare a gestire meglio la tua base di codice, ma non è assolutamente possibile di "OO Design". L'oggetto in questi casi è totalmente artificiale, semplicemente usato così perché il linguaggio non offre niente di meglio per gestire questo tipo di problema. per esempio. In linguaggi come C # useresti una classe statica per fornire questa funzionalità: riutilizza il meccanismo di classe, ma non è più necessario creare un'istanza di un oggetto solo per chiamare i metodi su di esso. Ti ritrovi con metodi come quelli string.IsNullOrEmpty(mystring)che penso siano poveri rispetto a mystring.isNullOrEmpty().

Quindi, se qualcuno chiede "come faccio a progettare le mie classi", consiglio di pensare ai dati che la classe conterrà piuttosto che alle funzioni che contiene. Se scegli "una classe è un mucchio di metodi", finisci per scrivere il codice di stile "migliore C". (che non è necessariamente una cosa negativa se stai migliorando il codice C) ma non ti darà il miglior programma progettato da OO.


9
-1, non sono assolutamente d'accordo. La progettazione OO per principianti funziona solo con oggetti dati come a Videoe tende a sovraccaricare tali classi con funzionalità, che spesso termina in codice disordinato con> 10K LOC per classe. Il design OO avanzato suddivide la funzionalità in unità più piccole come una VideoCompressor(e consente a una Videoclasse di essere semplicemente una classe di dati o una facciata per VideoCompressor).
Doc Brown,

5
@DocBrown: a quel punto tuttavia non è più un design orientato agli oggetti, poiché VideoCompressornon rappresenta un oggetto . Non c'è niente di sbagliato in questo, mostra solo il limite del design orientato agli oggetti.
Jan Hudec,

3
ah ma OO per principianti in cui 1 funzione viene trasformata in una classe non è affatto OO, e incoraggia solo l'enorme disaccoppiamento che finisce in un migliaio di file di classi che nessuno può mantenere. Penso che sia meglio pensare a una classe come un "wrapper di dati" e non un "wrapper di funzionalità", che darà a un principiante una migliore comprensione di come pensare ai programmi OO.
gbjbaanb,

4
@DocBrown ha effettivamente evidenziato con precisione la mia preoccupazione; In effetti ho una Videoclasse, ma la metodologia di compressione non è affatto banale, quindi vorrei scomporre il compressmetodo in più altri metodi privati. Questo è parte del motivo della mia domanda, dopo aver letto Non creare classi di verbi
yjwong

2
Penso che potrebbe essere giusto avere una Videoclasse, ma sarebbe composta da una VideoCompressor, una VideoSplittere altre classi correlate, che, in buona forma OO, dovrebbero essere classi individuali altamente coesive.
Eric King,

-1

L' ISP (principio di segregazione dell'interfaccia) afferma che nessun client dovrebbe essere costretto a dipendere da metodi che non utilizza. I vantaggi sono molteplici e chiari. Il tuo approccio rispetta totalmente l'ISP, e va bene.

Un approccio diverso anche nel rispetto dell'ISP è, ad esempio, creare un'interfaccia per ciascun metodo (o insieme di metodi con una coesione elevata) e quindi avere un'unica classe che implementa tutte quelle interfacce. Se questa è una soluzione migliore dipende dallo scenario. Il vantaggio di questo è che, quando si utilizza l'iniezione di dipendenza, si potrebbe avere un client con diversi collaboratori (uno per ogni interfaccia) ma alla fine tutti i collaboratori punteranno alla stessa istanza di oggetto.

A proposito, hai detto

Mi ritrovo a creare un'istanza per ogni classe

, queste classi sembrano essere servizi (e quindi apolidi). Hai pensato di farli diventare dei singoli?


1
I single sono, di solito, antipattern. Vedi Singletons: Risolvere problemi che non sapevi di non avere dal 1995 .
Jan Hudec,

3
Per il resto, mentre il principio di separazione dell'interfaccia è un buon modello, questa domanda riguarda le implementazioni, non le interfacce, quindi non sono sicuro che sia pertinente.
Jan Hudec,

3
Non entrerò per discutere se il singleton è uno schema o un antipasto. Ho suggerito una possibile soluzione (ha anche un nome) al suo problema, sta a lui decidere se si adatta o meno.
Diegomtassis,

Per quanto riguarda se l'ISP è pertinente o no, beh, l'oggetto della domanda è "le classi con un solo metodo sono un problema?", L'opposto è una classe con molti metodi, che diventa un problema nel momento in cui si crea un il cliente dipende direttamente da questo ... esattamente l'esempio xerox di Robert Martin che gli è occorso per formulare l'ISP.
Diegomtassis,

-1

Sì, c'è un problema. Ma non grave. Voglio dire che puoi strutturare il tuo codice in questo modo e non accadrà nulla di brutto, è mantenibile. Ma ci sono alcuni punti deboli in questo tipo di strutturazione. Ad esempio, considera se la rappresentazione del tuo video (nel tuo caso è raggruppato nella classe RawVideo) cambia, dovrai aggiornare tutte le tue classi operative. Oppure considera che potresti avere più rappresentazioni di video che variano in fase di esecuzione. Quindi dovrai abbinare la rappresentazione a una particolare classe "operativa". Inoltre soggettivamente è fastidioso trascinare una dipendenza per ogni operazione che si desidera eseguire su smth. e per aggiornare l'elenco delle dipendenze trasmesse ogni volta che decidi che l'operazione non è più necessaria o hai bisogno di nuove operazioni.

Inoltre, questa è in realtà una violazione di SRP. Alcune persone trattano SRP come una guida per dividere le responsabilità (e lo portano lontano trattando ciascuna operazione come una responsabilità distinta) ma dimenticano che SRP è anche una guida per raggruppare le responsabilità. E secondo le responsabilità dell'SRP che cambiano per lo stesso motivo dovrebbero essere raggruppate in modo tale che se il cambiamento si verifica, è localizzato nel minor numero possibile di classi / moduli. Per quanto riguarda le grandi classi, non è un problema avere più algoritmi nella stessa classe, purché tali algoritmi siano correlati (cioè condividono alcune conoscenze che non dovrebbero essere conosciute al di fuori di quella classe). Il problema sono le grandi classi che hanno algoritmi che non sono in alcun modo correlati e cambiano / variano per diversi motivi.

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.