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

45

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

Recentemente, ho intrapreso la revisione di alcune delle classi che ho progettato per il progetto.

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

  • VideoCompressor (con un metodo compress che accetta un video di input di tipo RawVideo e restituisce un video di output di tipo CompressedVideo ).
  • VideoSplitter (con un metodo split che accetta un video di input di tipo RawVideo e restituisce un vettore di 2 video di output, ciascuno di tipo RawVideo ).
  • VideoIndexer (con un metodo index che riprende un video di input di tipo RawVideo e restituisce un indice video di tipo VideoIndex ).

Mi trovo a istanziare ogni classe solo per effettuare chiamate come VideoCompressor.compress(...) , VideoSplitter.split(...) , VideoIndexer.index(...) .

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

Questo è effettivamente un problema?

    
posta yjwong 29.01.2014 - 08:16
fonte

9 risposte

78

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

    
risposta data 29.01.2014 - 08:37
fonte
24

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

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

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

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. Che si tratti di schemi di osservatori o di dipendenze o quant'altro. Qui dipende ancora dalla lingua. In linguaggi con funzioni di prima classe (C ++ ( std::function o parametro template), C # (delegato), Python, Perl, Ruby ( proc ), Lisp, Haskell, ...) questi pattern usano tipi di funzione e don ' Ho bisogno di lezioni. Java non ha (ancora, nella versione 8) ha i tipi di funzione, quindi usi le interfacce a metodo singolo e le corrispondenti classi di metodo singolo.

Naturalmente non sto sostenendo di scrivere un'unica grande funzione. Dovrebbe avere subroutine private, ma possono essere private del file di implementazione (namespace statico o anonimo a livello di file in C ++) o in una classe helper privata che viene solo istanziata all'interno della funzione pubblica (Per memorizzare i dati o no? ).

    
risposta data 29.01.2014 - 13:25
fonte
13

Potrebbero esserci motivi per estrarre un determinato metodo in una classe dedicata. Uno di questi motivi è consentire l'iniezione delle dipendenze.

Immagina di avere una classe chiamata VideoExporter che, 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 in questo modo:

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

Un'alternativa cattiva potrebbe avere un VideoExporter che ha molti metodi pubblici e fa tutto il lavoro, compresa la compressione. Sarebbe presto diventato un incubo di manutenzione, rendendo difficile aggiungere supporto per altri formati video.

    
risposta data 29.01.2014 - 08:39
fonte
6

Questo è un segno che si desidera passare funzioni come argomenti ad altre funzioni. Immagino che il tuo linguaggio (Java?) Non lo supporti; se questo è il caso, non è tanto una mancanza nel tuo design quanto una mancanza nella tua lingua di scelta. Questo è uno dei maggiori problemi con le lingue che insistono sul fatto che tutto debba essere una classe.

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

    
risposta data 29.01.2014 - 20:02
fonte
5

So di essere in ritardo per la festa, ma sembra che nessuno abbia mancato di segnalarlo:

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

Il modello di strategia viene utilizzato quando esistono diverse 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 di dipendenza per fornire la strategia concreta per tu.

Ad esempio in questo caso potresti avere interface VideoCompressor e poi avere diverse implementazioni alternative per esempio class H264Compressor implements VideoCompressor e class XVidCompressor implements VideoCompressor . Da OP non è chiaro che sia implicata un'interfaccia, anche se non c'è, può essere semplicemente che l'autore originale abbia lasciato la porta aperta per implementare un 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 l'iniezione di dipendenza e il modello di strategia correttamente. Invece di istanziarlo dove ne hai bisogno, la classe contenente dovrebbe avere un membro con l'oggetto strategia. E questo membro dovrebbe essere iniettato, per esempio nel costruttore.

In molti casi il modello di strategia genera classi di interfaccia (come stai visualizzando) con un solo metodo doStuff(...) .

    
risposta data 19.02.2016 - 16:17
fonte
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, ciò avrebbe probabilmente più senso dal punto di vista del DDD.

D'altro canto, se la suddivisione, la compressione e l'indicizzazione non fossero correlati in alcun modo, li terrei come componenti separati.

    
risposta data 30.01.2014 - 10:55
fonte
0

È un problema: stai lavorando dall'aspetto funzionale del design, piuttosto che dai dati. Ciò che in realtà hai sono 3 funzioni autonome che sono state OO-ified.

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

Durante la progettazione di sistemi OO, è meglio creare classi che rappresentano oggetti, piuttosto che classi che rappresentano attività che è possibile applicare. In passato, le classi venivano chiamate tipi: OO era un modo per estendere una lingua con supporto per nuovi tipi di dati. Se pensi a OO in questo modo, ottieni un modo migliore di progettare le tue lezioni.

EDIT:

fammi provare a spiegarmi un po 'meglio, immagina una classe di stringhe che abbia un metodo concat. Puoi implementare una cosa del genere in cui ogni oggetto istanziato dalla classe contiene i dati della stringa, quindi puoi dire

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

ma l'OP vuole che funzioni in questo modo:

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

ora ci sono posti in cui una classe può essere usata per contenere una collezione di funzioni correlate, ma non è OO, è un modo pratico di utilizzare le funzionalità OO di una lingua per aiutare a gestire meglio il tuo codebase, ma non è ogni tipo di "OO Design". L'oggetto in questi casi è totalmente artificiale, semplicemente usato in questo modo perché la lingua non offre nulla di meglio per gestire questo tipo di problema. per esempio. In linguaggi come C # si usa 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. Finisci con metodi come string.IsNullOrEmpty(mystring) che ritengo sia scarso rispetto a mystring.isNullOrEmpty() .

Quindi, se qualcuno sta chiedendo "come faccio a progettare le mie classi", consiglio di pensare ai dati che la classe conterrà invece delle funzioni che contiene. Se si va per "una classe è un mucchio di metodi", si finisce per scrivere un codice di stile "migliore C". (che non è necessariamente una brutta cosa se stai migliorando il codice C) ma non ti darà il miglior programma progettato da OO.

    
risposta data 29.01.2014 - 12:10
fonte
-1

Il ISP (principio di separazione delle interfacce) dice che nessun client dovrebbe essere costretto a dipendere da metodi che non usa. I benefici sono molteplici e chiari. Il tuo approccio rispetta totalmente l'ISP, e va bene.

Un approccio diverso anche rispetto all'ISP è, ad esempio, creare un'interfaccia per ogni metodo (o un insieme di metodi con un'elevata coesione) e quindi avere una singola classe che implementa tutte quelle interfacce. Che questa sia o meno una soluzione migliore dipende dallo scenario. Il vantaggio di ciò è che, quando si utilizza l'integrazione delle dipendenze, si potrebbe avere un client con diversi collaboratori (uno per ogni interfaccia) ma alla fine tutti i collaboratori punteranno alla stessa istanza dell'oggetto.

A proposito, hai detto

I find myself instantiating each class

, queste classi sembrano essere servizi (e quindi apolidi). Hai pensato di renderli singleton?

    
risposta data 29.01.2014 - 15:21
fonte
-1

Sì, c'è un problema. Ma non severo. 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, se la rappresentazione del tuo video (nel tuo caso è raggruppata in classe RawVideo) consideri le modifiche, dovrai aggiornare tutte le tue classi operative. Oppure considera che potresti avere più rappresentazioni di video che variano in runtime. Quindi dovrai abbinare la rappresentazione a una particolare classe di "operazione". Anche soggettivamente è fastidioso trascinare una dipendenza per ogni operazione che vuoi fare su smth. e per aggiornare la lista delle dipendenze passate ogni volta che decidi che l'operazione non è più necessaria o hai bisogno di una nuova operazione.

Anche questa è in realtà una violazione di SRP. Alcune persone considerano la SRP come una guida per dividere le responsabilità (e portarla a termine trattando ciascuna operazione con una responsabilità distinta) ma dimenticano che l'SRP è anche una guida per raggruppare le responsabilità. E in base alle responsabilità SRP che i cambiamenti per lo stesso motivo dovrebbero essere raggruppati in modo tale che, se il cambiamento accade, sia localizzato nel minor numero possibile di classi / moduli. Per quanto riguarda le grandi classi, non è un problema avere più algoritmi nella stessa classe finché tali algoritmi sono correlati (cioè condividere 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 correlati in alcun modo e cambiano / variano per ragioni diverse.

    
risposta data 14.12.2017 - 17:15
fonte

Leggi altre domande sui tag