Come può una classe avere più metodi senza violare il principio della singola responsabilità


64

Il principio della responsabilità singola è definito su Wikipedia come

Il principio di responsabilità singola è un principio di programmazione informatica che afferma che ogni modulo, classe o funzione dovrebbe avere la responsabilità su una singola parte della funzionalità fornita dal software e che la responsabilità dovrebbe essere interamente incapsulata dalla classe

Se una classe dovrebbe avere una sola responsabilità, come può avere più di 1 metodo? Ogni metodo non avrebbe una responsabilità diversa, il che significherebbe che la classe avrebbe più di 1 responsabilità.

Ogni esempio che ho visto dimostrare il principio di responsabilità singola utilizza una classe di esempio che ha un solo metodo. Potrebbe essere utile vedere un esempio o avere una spiegazione di una classe con più metodi che possono ancora essere considerati avere una responsabilità.


11
Perché un downvote? Sembra una domanda ideale per SE.SE; la persona ha studiato l'argomento e si è impegnata a chiarire la questione. Merita invece voti positivi.
Arseni Mourzenko,

19
Il downvote è probabilmente dovuto al fatto che si tratta di una domanda che è stata posta e che ha già risposto più volte, ad esempio consultare softwareengineering.stackexchange.com/questions/345018/… . Secondo me, non aggiunge nuovi aspetti sostanziali.
Hans-Martin Mosner,


9
Questo è semplicemente reductio ad absurdum. Se a ogni classe fosse letteralmente consentito un solo metodo, allora non c'è letteralmente modo che un programma sarebbe mai in grado di fare più di una cosa.
Darrel Hoffman,

6
@DarrelHoffman Non è vero. Se ogni classe fosse una funzione con solo un metodo "call ()", allora hai praticamente emulato una semplice programmazione procedurale con programmazione orientata agli oggetti. Puoi ancora fare tutto ciò che altrimenti potresti fare, dato che il metodo "call ()" di una classe può chiamare i metodi "call ()" di molte altre classi.
Vaelus,

Risposte:


29

La singola responsabilità potrebbe non essere qualcosa che una singola funzione può svolgere.

 class Location { 
     public int getX() { 
         return x;
     } 
     public int getY() { 
         return y; 
     } 
 }

Questa classe può violare il principio della singola responsabilità. Non perché ha due funzioni, ma se il codice è getX()e getY()deve soddisfare diversi stakeholder che potrebbero richiedere cambiamenti. Se il vicepresidente Mr. X invia un promemoria secondo cui tutti i numeri devono essere espressi come numeri in virgola mobile e il direttore contabile, la signora Y, insiste sul fatto che tutti i numeri che le sue recensioni di reparto devono rimanere numeri interi, indipendentemente da ciò che il signor X pensa bene, quindi questa classe dovrebbe avere una singola idea di chi è responsabile perché le cose stanno per diventare confuse.

Se fosse stato seguito SRP, sarebbe chiaro se la classe Location contribuisse alle cose a cui il sig. X e il suo gruppo sono esposti. Chiarisci di cosa è responsabile la classe e sai quale direttiva ha un impatto su questa classe. Se hanno entrambi un impatto su questa classe, allora è stata progettata male per minimizzare l'impatto del cambiamento. "Una classe dovrebbe avere solo un motivo per cambiare" non significa che l'intera classe possa fare solo una piccola cosa. Significa che non dovrei essere in grado di guardare la classe e dire che sia il signor X che la signora Y hanno interesse per questa classe.

A parte cose del genere. No, più metodi vanno bene. Dagli semplicemente un nome che chiarisca quali metodi appartengono alla classe e quali no.

L'SRP di zio Bob riguarda più la legge di Conway che la legge di Curly . Lo zio Bob sostiene l'applicazione della legge di Curly (fai una cosa) a funzioni e non a classi. SRP mette in guardia contro il mescolare le ragioni per cambiare insieme. La legge di Conway afferma che il sistema seguirà il modo in cui scorre l'informazione di un'organizzazione. Ciò porta a seguire SRP perché non ti importa di ciò di cui non hai mai sentito parlare.

"Un modulo dovrebbe essere responsabile nei confronti di un solo attore"

Robert C Martin - Architettura pulita

Le persone continuano a desiderare che SRP riguardi tutti i motivi per limitare l'ambito. Esistono più motivi per limitare l'ambito rispetto a SRP. Limito ulteriormente l'ambito insistendo sul fatto che la classe è un'astrazione che può prendere un nome che assicura che guardarti dentro non ti sorprenda .

Puoi applicare la legge di Curly alle lezioni. Sei fuori da ciò di cui parla lo zio Bob, ma puoi farlo. Dove sbagli è quando inizi a pensare che significhi una funzione. È come pensare che una famiglia dovrebbe avere un solo figlio. Avere più di un figlio non gli impedisce di essere una famiglia.

Se applichi la legge di Curly a una classe, tutto nella classe dovrebbe riguardare un'unica idea unificante. Quell'idea può essere ampia. L'idea potrebbe essere la persistenza. Se alcune funzioni dell'utilità di registrazione sono presenti, sono chiaramente fuori posto. Non importa se il signor X è l'unico a cui importa questo codice.

Il principio classico da applicare qui si chiama Separazione delle preoccupazioni . Se separi tutte le tue preoccupazioni, si potrebbe sostenere che ciò che rimane in ogni posto è una preoccupazione. Questo è ciò che abbiamo chiamato questa idea prima che il film City Slickers del 1991 ci presentasse al personaggio di Curly.

Questo va bene. È solo che ciò che lo zio Bob definisce una responsabilità non è un problema. Una responsabilità nei suoi confronti non è qualcosa su cui ti concentri. È qualcosa che può costringerti a cambiare. Puoi concentrarti su una preoccupazione e creare comunque codice responsabile di diversi gruppi di persone con diversi programmi.

Forse non ti interessa. Belle. Pensare che la volontà di "fare una cosa" risolverà tutti i tuoi problemi di progettazione mostra una mancanza di immaginazione di ciò che "una cosa" può finire per essere. Un altro motivo per limitare l'ambito è l'organizzazione. Puoi annidare molte "cose" dentro altre "cose" fino a quando non avrai un cassetto spazzatura pieno di tutto. Ne ho già parlato prima

Naturalmente il classico motivo OOP per limitare l'ambito è che la classe ha campi privati ​​al suo interno e piuttosto che usare getter per condividere quei dati, mettiamo tutti i metodi che necessitano di quei dati nella classe dove possono usare i dati in privato. Molti lo trovano troppo restrittivo per essere utilizzato come limitatore di ambito perché non tutti i metodi che appartengono insieme utilizzano esattamente gli stessi campi. Mi piace garantire che qualunque idea che abbia riunito i dati sia la stessa idea che ha riunito i metodi.

Il modo funzionale di vedere questo è quello a.f(x)e a.g(x)sono semplicemente f a (x) e g a (x). Non due funzioni ma un continuum di coppie di funzioni che variano insieme. Il anon hanno nemmeno bisogno di avere i dati in esso. Potrebbe essere semplicemente il modo in cui sai quale fe quale gimplementazione utilizzerai. Le funzioni che cambiano insieme appartengono insieme. È un buon vecchio polimorfismo.

SRP è solo uno dei tanti motivi per limitare l'ambito di applicazione. È buono. Ma non l'unico.


25
Penso che questa risposta sia fonte di confusione per qualcuno che cerca di capire l'SRP. La battaglia tra Mr. President e Mrs Director non è risolta con mezzi tecnici e il suo utilizzo per giustificare una decisione ingegneristica non ha senso. Legge di Conway in azione.
whatsisname

8
@whatsisname Al contrario. L'SRP era esplicitamente destinato a essere applicato alle parti interessate. Non ha nulla a che fare con il design tecnico. Potresti non essere d'accordo con questo approccio, ma è così che SRP è stato originariamente definito da Zio Bob, e ha dovuto ribadirlo più e più volte perché per qualche ragione, le persone non sembrano essere in grado di capire questa semplice nozione (mente, sia è effettivamente utile è una domanda completamente ortogonale).
Luaan,

La legge di Curly, come descritta da Tim Ottinger, sottolinea che una variabile dovrebbe costantemente significare una cosa. Per me, SRP è un po 'più forte di così; una classe può concettualmente rappresentare "una cosa", ma violare SRP se due driver esterni di cambiamento trattano alcuni aspetti di quella "cosa" in modi diversi o sono preoccupati per due aspetti diversi. Il problema è di modellistica; hai scelto di modellare qualcosa come una singola classe, ma c'è qualcosa nel dominio che rende problematica quella scelta (le cose iniziano a mettersi in cammino man mano che la base di codice si evolve).
Filip Milovanović,

2
@ FilipMilovanović La somiglianza che vedo tra Conway's Law e SRP nel modo in cui lo zio Bob ha spiegato che SRP nel suo libro di Clean Architecture deriva dal presupposto che l'organizzazione abbia un organigramma aciclico pulito. Questa è una vecchia idea Anche la Bibbia ha una citazione qui: "Nessun uomo può servire due padroni".
candied_orange

1
@TKK lo sto mettendo in relazione (non equiparandolo) alla legge di Conways e non alla legge di Curly. Sto confutando l'idea che SRP sia la legge di Curly principalmente perché lo zio Bob lo ha detto lui stesso nel suo libro di Clean Architecture.
candied_orange

48

La chiave qui è portata o, se preferite, granularità . Una parte della funzionalità rappresentata da una classe può essere ulteriormente separata in parti della funzionalità, ciascuna parte essendo un metodo.

Ecco un esempio Immagina di dover creare un CSV da una sequenza. Se si desidera essere conformi a RFC 4180, ci vorrebbe un po 'di tempo per implementare l'algoritmo e gestire tutti i casi limite.

Farlo in un unico metodo comporterebbe un codice che non sarà particolarmente leggibile e, soprattutto, il metodo farebbe diverse cose contemporaneamente. Pertanto, lo suddividerai in diversi metodi; ad esempio, uno di essi potrebbe essere responsabile della generazione dell'intestazione, ovvero la prima riga del CSV, mentre un altro metodo convertirà un valore di qualsiasi tipo nella sua rappresentazione di stringa adatta al formato CSV, mentre un altro determinerebbe se un il valore deve essere racchiuso tra virgolette doppie.

Tali metodi hanno la propria responsabilità. Il metodo che controlla se è necessario aggiungere doppie virgolette o meno ha il suo e il metodo che genera l'intestazione ne ha uno. Questo è SRP applicato ai metodi .

Ora, tutti questi metodi hanno un obiettivo in comune, ovvero prendere una sequenza e generare il CSV. Questa è la sola responsabilità della classe .


Pablo H ha commentato:

Un bell'esempio, ma ritengo che non risponda ancora perché SRP consente a una classe di avere più di un metodo pubblico.

Infatti. L'esempio CSV che ho dato ha idealmente un metodo pubblico e tutti gli altri metodi sono privati. Un esempio migliore sarebbe quello di una coda, implementata da una Queueclasse. Questa classe conterrebbe sostanzialmente due metodi: push(chiamato anche enqueue) e pop(chiamato anche dequeue).

  • La responsabilità di Queue.pushè aggiungere un oggetto alla coda della coda.

  • La responsabilità di Queue.popè rimuovere un oggetto dalla testa della coda e gestire il caso in cui la coda è vuota.

  • La responsabilità della Queueclasse è fornire una logica di coda.


1
Un bell'esempio, ma ritengo che non risponda ancora perché SRP consente a una classe di avere più di un metodo pubblico .
Pablo H,

1
@PabloH: giusto. Ho aggiunto un altro esempio in cui una classe ha due metodi.
Arseni Mourzenko,

30

Una funzione è una funzione.

Una responsabilità è una responsabilità.

Un meccanico ha la responsabilità di riparare le auto, che implicano la diagnostica, alcune semplici attività di manutenzione, alcuni lavori di riparazione effettivi, alcune delegazioni di compiti ad altri, ecc.

Una classe di container (elenco, array, dizionario, mappa, ecc.) Ha la responsabilità di archiviare oggetti, il che comporta la loro memorizzazione, consentendo l'inserimento, fornendo accesso, un qualche tipo di ordinamento, ecc.

Una singola responsabilità non significa che ci sia pochissimo codice / funzionalità, significa che qualunque funzionalità esiste "appartiene insieme" sotto la stessa responsabilità.


2
Concorrere. @Aulis Ronkainen - per legare le due risposte. E per le responsabilità annidate, usando la tua analogia meccanica, un garage ha la responsabilità della manutenzione dei veicoli. diversi meccanici nel garage hanno la responsabilità di diverse parti dell'auto, ma ciascuno di questi meccanici lavora insieme in coesione
wolfsshield

2
@wolfsshield, d'accordo. Il meccanico che fa solo una cosa è inutile, ma il meccanico che ha una sola responsabilità non lo è (almeno necessariamente). Sebbene le analogie della vita reale non siano sempre le migliori per descrivere concetti astratti di OOP, è importante distinguere queste differenze. Credo che non capire la differenza sia ciò che crea la confusione in primo luogo.
Aulis Ronkainen,

3
@AulisRonkainen Anche se sembra, odora e sembra un'analogia, ho intenzione di usare il meccanico per evidenziare il significato specifico del termine Responsabilità in SRP. Sono completamente d'accordo con la tua risposta.
Peter,

20

La singola responsabilità non significa necessariamente che faccia solo una cosa.

Prendi ad esempio una classe di servizio utente:

class UserService {
    public User Get(int id) { /* ... */ }
    public User[] List() { /* ... */ }

    public bool Create(User u) { /* ... */ }
    public bool Exists(int id) { /* ... */ }
    public bool Update(User u) { /* ... */ }
}

Questa classe ha più metodi ma la sua responsabilità è chiara. Fornisce l'accesso ai record dell'utente nell'archivio dati. Le sue sole dipendenze sono il modello utente e l'archivio dati. È liberamente accoppiato e altamente coeso, che è davvero ciò che SRP sta cercando di farti pensare.

SRP non deve essere confuso con il "Principio di segregazione dell'interfaccia" (vedere SOLID ). Il principio di segregazione dell'interfaccia (ISP) afferma che interfacce più piccole e leggere sono preferibili a interfacce più grandi e più generalizzate. Go fa un uso intensivo dell'ISP in tutta la sua libreria standard:

// Interface to read bytes from a stream
type Reader interface {
    Read(p []byte) (n int, err error)
}

// Interface to write bytes to a stream
type Writer interface {
    Write(p []byte) (n int, err error)
}

// Interface to convert an object into JSON
type Marshaler interface {
    MarshalJSON() ([]byte, error)
}

SRP e ISP sono sicuramente correlati, ma l'uno non implica l'altro. L'ISP è a livello di interfaccia e SRP a livello di classe. Se una classe implementa diverse interfacce semplici, potrebbe non avere più una sola responsabilità.

Grazie a Luaan per aver sottolineato la differenza tra ISP e SRP.


3
In realtà, stai descrivendo il principio di segregazione dell'interfaccia (l'io in SOLID). SRP è una bestia abbastanza diversa.
Luaan,

A parte questo, quale convenzione di codifica stai usando qui? Mi aspetterei gli oggetti UserService e Userad essere UpperCamelCase, ma i metodi Create , Existse Updateavrei fatto lowerCamelCase.
KlaymenDK,

1
@KlaymenDK Hai ragione, la maiuscola è solo un'abitudine dall'uso di Go (maiuscola = esportata / pubblica, minuscola = privata)
Jesse

@Luaan Grazie per averlo sottolineato, chiarirò la mia risposta
Jesse il

1
@KlaymenDK Molte lingue usano PascalCase sia per i metodi che per le classi. C # per esempio.
Omegastick,

15

C'è uno chef in un ristorante. La sua unica responsabilità è cucinare. Eppure può cucinare bistecche, patate, broccoli e centinaia di altre cose. Assumeresti uno chef per piatto nel tuo menu? O uno chef per ogni componente di ogni piatto? O uno chef in grado di soddisfare la sua unica responsabilità: cucinare?

Se chiedi a quello chef di occuparsi anche delle buste paga, allora violerai SRP.


4

Contro-esempio: memorizzazione dello stato mutabile.

Supponiamo di avere la classe più semplice di sempre, il cui unico compito è quello di archiviarne una int.

public class State {
    private int i;


    public State(int i) { this.i = i; }
}

Se tu fossi limitato a 1 solo metodo, potresti avere un setState(), o un getState(), a meno che tu non rompa l'incapsulamento e lo renda ipubblico.

  • Un setter è inutile senza un getter (non potresti mai leggere le informazioni)
  • Un getter è inutile senza un setter (non puoi mai mutare le informazioni).

Quindi, chiaramente, questa singola responsabilità richiede di avere almeno 2 metodi su questa classe. QED.


4

Stai fraintendendo il principio della responsabilità singola.

La singola responsabilità non equivale a un singolo metodo. Significano cose diverse. Nello sviluppo del software parliamo di coesione . Le funzioni (metodi) che hanno un'elevata coesione "appartengono" insieme e possono essere considerate come eseguire una singola responsabilità.

Spetta allo sviluppatore progettare il sistema in modo da soddisfare il principio della singola responsabilità. Si può vedere questo come una tecnica di astrazione ed è quindi a volte una questione di opinione. L'implementazione del principio della responsabilità singola rende il codice principalmente più facile da testare e più facile da capire la sua architettura e design.


2

È spesso utile (in qualsiasi lingua, ma soprattutto nelle lingue OO) guardare le cose e organizzarle dal punto di vista dei dati piuttosto che dalle funzioni.

Pertanto, considera la responsabilità di una classe di mantenere l'integrità e fornire aiuto per utilizzare correttamente i dati di sua proprietà. Chiaramente questo è più facile da fare se tutto il codice è in una classe, piuttosto che distribuito su più classi. L'aggiunta di due punti viene eseguita in modo più affidabile e il codice viene gestito più facilmente, con un Point add(Point p)metodo nella Pointclasse piuttosto che averlo altrove.

E in particolare, la classe non dovrebbe esporre nulla che possa provocare dati incoerenti o errati. Ad esempio, se un Pointdeve trovarsi all'interno di un piano da (0,0) a (127,127), il costruttore e tutti i metodi che modificano o producono un nuovo Pointhanno la responsabilità di verificare i valori forniti e di rifiutare qualsiasi modifica che violi questo Requisiti. (Spesso qualcosa come un Pointsarebbe immutabile, e garantire che non ci siano modi per modificare un Pointdopo che è stato costruito sarebbe anche una responsabilità della classe)

Si noti che la stratificazione qui è perfettamente accettabile. Potresti avere una Pointclasse per trattare i singoli punti e una Polygonclasse per trattare un insieme di Points; questi hanno ancora responsabilità distinte, perché Polygoni delegati ogni responsabilità per trattare con qualsiasi cosa solo a che fare con una Point(come ad esempio garantire un punto ha sia un xe un yvalue) per la Pointcategoria.

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.