Un costruttore che convalida i suoi argomenti viola SRP?


66

Sto cercando di aderire il più possibile al principio della responsabilità singola (SRP) e mi sono abituato a un certo modello (per SRP sui metodi) facendo molto affidamento sui delegati. Mi piacerebbe sapere se questo approccio è valido o se ci sono problemi gravi con esso.

Ad esempio, per controllare l'input di un costruttore, potrei introdurre il seguente metodo (l' Streaminput è casuale, potrebbe essere qualsiasi cosa)

private void CheckInput(Stream stream)
{
    if(stream == null)
    {
        throw new ArgumentNullException();
    }

    if(!stream.CanWrite)
    {
        throw new ArgumentException();
    }
}

Questo metodo (probabilmente) fa più di una cosa

  • Controlla gli ingressi
  • Genera diverse eccezioni

Per aderire all'SRP ho quindi modificato la logica in

private void CheckInput(Stream stream, 
                        params (Predicate<Stream> predicate, Action action)[] inputCheckers)
{
    foreach(var inputChecker in inputCheckers)
    {
        if(inputChecker.predicate(stream))
        {
            inputChecker.action();
        }
    }
}

Che presumibilmente fa solo una cosa (vero?): Controlla l'input. Per l'effettivo controllo degli input e il lancio delle eccezioni ho introdotto metodi simili

bool StreamIsNull(Stream s)
{
    return s == null;
}

bool StreamIsReadonly(Stream s)
{
    return !s.CanWrite;
}

void Throw<TException>() where TException : Exception, new()
{
    throw new TException();
}

e può chiamare CheckInputcome

CheckInput(stream,
    (this.StreamIsNull, this.Throw<ArgumentNullException>),
    (this.StreamIsReadonly, this.Throw<ArgumentException>))

È forse qualcosa di meglio della prima opzione o introduco complessità non necessarie? Esiste un modo in cui posso ancora migliorare questo modello, se possibile?


26
Potrei sostenere che CheckInputsta ancora facendo più cose: sta iterando su un array e chiamando una funzione predicata e chiamando una funzione action. Non è quindi una violazione dell'SRP?
Bart van Ingen Schenau,

8
Sì, questo è il punto che stavo cercando di chiarire.
Bart van Ingen Schenau,

135
è importante ricordare che è il principio della singola responsabilità ; non il principio della singola azione . Ha una responsabilità: verificare che il flusso sia definito e scrivibile.
David Arno,

40
Tieni presente che il punto centrale di questi principi del software è rendere il codice più leggibile e gestibile. CheckInput originale è molto più facile da leggere e gestire rispetto alla versione refactored. In effetti, se mai mi fossi imbattuto nel tuo ultimo metodo CheckInput in una base di codice, avrei scartato tutto e riscritto per abbinarlo a quello che avevi originariamente.
17 del 26

17
Questi "principi" sono praticamente inutili perché puoi semplicemente definire la "singola responsabilità" in qualunque modo tu voglia andare avanti con qualunque fosse la tua idea originale. Ma se provi ad applicarli rigidamente, immagino che finirai con questo tipo di codice che, a dire il vero, è difficile da capire.
Casey,

Risposte:


150

SRP è forse il principio software più frainteso.

Un'applicazione software è costruita da moduli, che sono costruiti da moduli, che sono costruiti da ...

In fondo, una singola funzione come CheckInputconterrà solo un po 'di logica, ma mentre si sale verso l'alto, ogni modulo successivo incapsula sempre più logica e questo è normale .

SRP non si tratta di fare una singola azione atomica . Si tratta di avere un'unica responsabilità, anche se tale responsabilità richiede più azioni ... e alla fine riguarda manutenzione e testabilità :

  • promuove l'incapsulamento (evitando God Objects),
  • promuove la separazione delle preoccupazioni (evitando di increspare i cambiamenti nell'intera base di codice),
  • aiuta la testabilità restringendo la portata delle responsabilità.

Il fatto che CheckInputsia implementato con due controlli e solleva due diverse eccezioni è in qualche misura irrilevante .

CheckInputha una responsabilità limitata: garantire che l'input sia conforme ai requisiti. Sì, ci sono più requisiti, ma ciò non significa che ci siano più responsabilità. Sì, potresti dividere i controlli, ma come sarebbe d'aiuto? Ad un certo punto i controlli devono essere elencati in qualche modo.

Confrontiamo:

Constructor(Stream stream) {
    CheckInput(stream);
    // ...
}

contro:

Constructor(Stream stream) {
    CheckInput(stream,
        (this.StreamIsNull, this.Throw<ArgumentNullException>),
        (this.StreamIsReadonly, this.Throw<ArgumentException>));
    // ...
}

Ora, CheckInputfa di meno ... ma il suo chiamante fa di più!

Hai spostato l'elenco dei requisiti da CheckInput, dove sono incapsulati, a Constructordove sono visibili.

È un bel cambiamento? Dipende:

  • Se CheckInputviene chiamato solo lì: è discutibile, da un lato rende visibili i requisiti, dall'altro ingombra il codice;
  • Se CheckInputviene chiamato più volte con gli stessi requisiti , viola il DRY e si verifica un problema di incapsulamento.

È importante rendersi conto che una singola responsabilità può implicare molto lavoro. Il "cervello" di un'auto a guida autonoma ha una sola responsabilità:

Guidare l'auto verso la sua destinazione.

È una singola responsabilità, ma richiede il coordinamento di una tonnellata di sensori e attori, prendendo molte decisioni e ha persino requisiti contrastanti 1 ...

... tuttavia, è tutto incapsulato. Quindi al cliente non importa.

1 sicurezza dei passeggeri, sicurezza degli altri, rispetto delle normative, ...


2
Penso che il modo in cui stai usando la parola "incapsulamento" e i suoi derivati ​​sia confuso. A parte questo, ottima risposta!
Fabio dice di reintegrare Monica il

4
Sono d'accordo con la tua risposta, ma l'argomento del cervello di auto a guida autonoma spesso induce le persone a interrompere la SRP. Come hai detto, sono moduli fatti di moduli fatti di moduli. È possibile identificare lo scopo dell'intero sistema, ma tale sistema dovrebbe essere distrutto da solo. Puoi risolvere quasi tutti i problemi.
Sava B.

13
@SavaB .: Certo, ma il principio rimane lo stesso. Un modulo dovrebbe avere un'unica responsabilità, sebbene di portata più ampia rispetto ai suoi componenti.
Matthieu M.,

3
@ user949300 Okay, che ne dici di "guidare". In realtà, "guidare" è la responsabilità e "in sicurezza" e "legalmente" sono requisiti su come adempiere a tale responsabilità. E spesso elenchiamo i requisiti quando dichiariamo una responsabilità.
Brian McCutchon,

1
"SRP è forse il principio software più frainteso." Come evidenziato da questa risposta :)
Michael,

41

Citando lo zio Bob sull'SRP ( https://8thlight.com/blog/uncle-bob/2014/05/08/SingleReponsibilityPrinciple.html ):

Il Single Responsibility Principle (SRP) afferma che ogni modulo software dovrebbe avere una e una sola ragione per cambiare.

... Questo principio riguarda le persone.

... Quando si scrive un modulo software, si desidera assicurarsi che, quando vengono richieste modifiche, tali modifiche possono provenire solo da una singola persona, o piuttosto da un singolo gruppo strettamente accoppiato di persone che rappresentano un'unica funzione aziendale definita in modo restrittivo.

... Questo è il motivo per cui non inseriamo SQL nei JSP. Questo è il motivo per cui non generiamo HTML nei moduli che calcolano i risultati. Questo è il motivo per cui le regole aziendali non dovrebbero conoscere lo schema del database. Questo è il motivo per cui separiamo le preoccupazioni.

Spiega che i moduli software devono affrontare le preoccupazioni specifiche delle parti interessate. Pertanto, rispondendo alla tua domanda:

È forse qualcosa di meglio della prima opzione o introduco complessità non necessarie? Esiste un modo in cui posso ancora migliorare questo modello, se possibile?

IMO, stai solo guardando un metodo, quando dovresti guardare a un livello superiore (livello di classe in questo caso). Forse dovremmo dare un'occhiata a ciò che la tua classe sta attualmente facendo (e questo richiede maggiori spiegazioni sul tuo scenario). Per ora, la tua classe sta ancora facendo la stessa cosa. Ad esempio, se domani ci sono delle richieste di modifica su alcune convalide (ad esempio: "ora lo stream può essere nullo"), allora devi ancora andare a questa classe e cambiare le cose al suo interno.


4
Migliore risposta. Per elaborare il PO, se i controlli di guardia provengono da due parti interessate / dipartimenti diversi, allora checkInputs()dovrebbero essere divisi, diciamo in checkMarketingInputs()e checkRegulatoryInputs(). Altrimenti va bene combinarli tutti in un unico metodo.
user949300

36

No, questa modifica non è informata dall'SRP.

Chiediti perché non c'è un segno di spunta nel tuo correttore per "l'oggetto passato è un flusso" . La risposta è ovvia: il linguaggio impedisce al chiamante di compilare un programma che passa in un flusso non.

Il sistema di tipi di C # non è sufficiente per soddisfare le tue esigenze; i tuoi controlli stanno implementando l'applicazione degli invarianti che non possono essere espressi oggi nel sistema dei tipi . Se ci fosse un modo per dire che il metodo prende un flusso scrivibile non annullabile, lo avresti scritto, ma non lo è, quindi hai fatto la cosa migliore successiva: hai imposto la restrizione del tipo in fase di esecuzione. Spero che tu l'abbia anche documentato, in modo che gli sviluppatori che usano il tuo metodo non debbano violarlo, falliscano i loro casi di test e quindi risolvano il problema.

Mettere i tipi su un metodo non è una violazione del principio di responsabilità singola; né il metodo fa rispettare le sue precondizioni o afferma le sue postcondizioni.


1
Inoltre, lasciare l'oggetto creato in uno stato valido è l'unica responsabilità che un costruttore ha fondamentalmente sempre. Se, come hai già detto, richiede controlli aggiuntivi che il runtime e / o il compilatore non possono fornire, allora non c'è davvero modo di aggirarlo.
SBI,

23

Non tutte le responsabilità sono uguali.

inserisci qui la descrizione dell'immagine

inserisci qui la descrizione dell'immagine

Ecco due cassetti. Entrambi hanno una responsabilità. Ognuno di essi ha nomi che ti consentono di sapere cosa appartiene a loro. Uno è il cassetto dell'argenteria. L'altro è il cassetto spazzatura.

Quindi qual è la differenza? Il cassetto dell'argenteria chiarisce ciò che non vi appartiene. Il cassetto spazzatura accetta comunque tutto ciò che si adatta. Estrarre i cucchiai dal cassetto dell'argenteria sembra molto sbagliato. Eppure mi viene difficile pensare a tutto ciò che ci mancherebbe se rimosso dal cassetto della spazzatura. La verità è che puoi affermare che qualcosa ha una singola responsabilità, ma quale pensi abbia la responsabilità singola più mirata?

Un oggetto con una sola responsabilità non significa che qui possa succedere solo una cosa. Le responsabilità possono nidificare. Ma quelle responsabilità di nidificazione dovrebbero avere senso, non dovrebbero sorprenderti quando le trovi qui e dovresti perderle se non ci fossero più.

Quindi quando offri

CheckInput(Stream stream);

Non mi preoccupo che sia il controllo dell'input sia il lancio di eccezioni. Sarei preoccupato se si stesse verificando sia l'input che il salvataggio dell'input. È una brutta sorpresa. Uno che non mi mancherebbe se non ci fosse più.


21

Quando ti allacci e scrivi uno strano codice per conformarti a un importante principio del software, di solito hai frainteso il principio (anche se a volte il principio è sbagliato). Come sottolinea l'eccellente risposta di Matthieu, l'intero significato di SRP dipende dalla definizione di "responsabilità".

I programmatori esperti vedono questi principi e li mettono in relazione con le memorie del codice che abbiamo rovinato; i programmatori meno esperti li vedono e potrebbero non avere nulla a cui collegarli affatto. È un'astrazione che fluttua nello spazio, tutto ghigno e nessun gatto. Quindi immaginano, e di solito va male. Prima di sviluppare il senso del cavallo di programmazione, la differenza tra il codice strano e complicato e il codice normale non è affatto evidente.

Questo non è un comandamento religioso a cui devi obbedire indipendentemente dalle conseguenze personali. È più una regola empirica intesa a formalizzare un elemento di programmazione del senso del cavallo e ad aiutarti a mantenere il tuo codice il più semplice e chiaro possibile. Se sta avendo l'effetto opposto, hai ragione a cercare un input esterno.

Nella programmazione, non puoi andare molto più in errore che cercare di dedurre il significato di un identificatore dai primi principi semplicemente fissandolo, e questo vale per gli identificatori che scrivono sulla programmazione tanto quanto gli identificatori nel codice reale.


14

Ruolo CheckInput

Innanzitutto, lasciatemi mettere in evidenza l'ovvio, CheckInput sta facendo una cosa, anche se sta controllando vari aspetti. Alla fine controlla l'input . Si potrebbe obiettare che non è una cosa se si ha a che fare con metodi chiamati DoSomething, ma penso che sia sicuro supporre che il controllo dell'input non sia troppo vago.

L'aggiunta di questo modello per i predicati potrebbe essere utile se non si desidera che la logica per il controllo dell'input venga inserita nella propria classe, ma questo modello sembra piuttosto dettagliato per ciò che si sta tentando di ottenere. Potrebbe essere molto più diretto passare semplicemente un'interfaccia IStreamValidatorcon un singolo metodo isValid(Stream)se questo è ciò che si desidera ottenere. Qualsiasi implementazione di classe IStreamValidatorpuò utilizzare predicati come StreamIsNullo StreamIsReadonlyse lo desiderano, ma tornando al punto centrale, è una modifica piuttosto ridicola da apportare nell'interesse di mantenere il principio della responsabilità unica.

Controllo sanitario

È mia idea che ci sia permesso tutti un "controllo di integrità" per assicurarti che tu abbia almeno a che fare con uno Stream non nullo e scrivibile, e questo controllo di base non sta in qualche modo rendendo la tua classe un validatore di flussi. Intendiamoci, sarebbe meglio lasciare controlli più sofisticati al di fuori della tua classe, ma è lì che viene tracciata la linea. Una volta che devi iniziare a cambiare lo stato del tuo flusso leggendolo o dedicando risorse alla convalida, hai iniziato a eseguire una convalida formale del tuo flusso e questo è ciò che dovrebbe essere inserito nella sua classe.

Conclusione

Penso che se stai applicando uno schema per organizzare meglio un aspetto della tua classe, merita di essere nella sua classe. Dal momento che uno schema non si adatta, dovresti anche chiederti se appartiene o meno alla sua stessa classe in primo luogo. Il mio pensiero è che, a meno che tu non creda che la convalida del flusso sarà probabilmente cambiata in futuro, e specialmente se ritieni che questa convalida possa anche essere di natura dinamica, lo schema che hai descritto è una buona idea, anche se potrebbe inizialmente banale. Altrimenti, non è necessario rendere arbitrariamente più complesso il programma. Consente di chiamare una vanga una vanga. La convalida è una cosa, ma verificare l'input nullo non è convalida, e quindi penso che tu possa essere sicuro di mantenerlo nella tua classe senza violare il principio della responsabilità singola.


4

Il principio enfaticamente non afferma che un pezzo di codice dovrebbe "fare solo una cosa".

La "responsabilità" in SRP dovrebbe essere compresa a livello di requisiti. La responsabilità del codice è di soddisfare i requisiti aziendali. SRP viene violato se un oggetto soddisfa più di un requisito aziendale indipendente . Indipendentemente da ciò significa che un requisito potrebbe cambiare mentre l'altro requisito rimane in vigore.

È ipotizzabile l'introduzione di un nuovo requisito aziendale, il che significa che questo particolare oggetto non deve essere verificato per la lettura, mentre un altro requisito aziendale richiede ancora che l'oggetto controlli per la lettura? No, perché i requisiti aziendali non specificano i dettagli di implementazione a quel livello.

Un esempio reale di una violazione di SRP sarebbe il codice come questo:

var message = "Your package will arrive before " + DateTime.Now.AddDays(14);

Questo codice è molto semplice, ma è comunque ipotizzabile che il testo cambierà indipendentemente dalla data di consegna prevista, poiché questi sono decisi da diverse parti dell'azienda.


Una classe diversa per praticamente ogni esigenza suona come un incubo empio.
whatsisname

@whatsisname: Quindi forse l'SRP non fa per te. Nessun principio di progettazione si applica a tutti i tipi e dimensioni di progetti. (Ma sappiate che stiamo solo parlando di requisiti indipendenti (cioè che possono cambiare in modo indipendente), e non di qualsiasi requisito da allora, dipenderà solo da quanto sono definiti a grana fine.)
JacquesB,

Penso che sia più che l'SRP richieda un elemento di giudizio situazionale che è difficile da descrivere in un'unica frase accattivante.
whatsisname

@whatsisname: sono totalmente d'accordo.
Jacques B

+1 per SRP viene violato se un oggetto soddisfa più di un requisito aziendale indipendente. Indipendentemente da ciò significa che un requisito potrebbe cambiare mentre l'altro requisito rimane in vigore
Juzer Ali

3

Mi piace il punto della risposta di @ EricLippert :

Chiediti perché non è presente alcun segno di spunta nel tuo correttore per l'oggetto passato in un flusso . La risposta è ovvia: il linguaggio impedisce al chiamante di compilare un programma che passa in un flusso non.

Il sistema di tipi di C # non è sufficiente per soddisfare le tue esigenze; i tuoi controlli stanno implementando l'applicazione degli invarianti che non possono essere espressi oggi nel sistema dei tipi . Se ci fosse un modo per dire che il metodo prende un flusso scrivibile non annullabile, lo avresti scritto, ma non lo è, quindi hai fatto la cosa migliore successiva: hai imposto la restrizione del tipo in fase di esecuzione. Spero che tu l'abbia anche documentato, in modo che gli sviluppatori che usano il tuo metodo non debbano violarlo, falliscano i loro casi di test e quindi risolvano il problema.

EricLippert ha ragione sul fatto che si tratta di un problema per il sistema di tipi. E poiché si desidera utilizzare il principio della responsabilità singola (SRP), in pratica è necessario che il sistema dei tipi sia responsabile di questo lavoro.

In realtà è possibile fare questo in C #. Siamo in grado di catturare valori letterali nullin fase di compilazione, quindi catturare valori non letterali nullin fase di esecuzione. Non è buono come un controllo completo in fase di compilazione, ma è un miglioramento rigoroso rispetto al non intercettare mai in fase di compilazione.

Allora, sai come ha fatto C # Nullable<T>? Facciamo il contrario e facciamo un NonNullable<T>:

public struct NonNullable<T> where T : class
{
    public T Value { get; private set; }
    public NonNullable(T value)
    {
        if (value == null) { throw new NullArgumentException(); }
        this.Value = value;
    }
    //  Ease-of-use:
    public static implicit operator T(NonNullable<T> value) { return value.Value; }
    public static implicit operator NonNullable<T>(T value) { return new NonNullable<T>(value); }

    //  Hack-ish overloads that prevent null-literals from being implicitly converted into NonNullable<T>'s.
    public static implicit operator NonNullable<T>(Tuple<T> value) { return new NonNullable<T>(value.Item1); }
    public static implicit operator NonNullable<T>(Tuple<T, T> value) { return new NonNullable<T>(value.Item1); }
}

Ora, invece di scrivere

public void Foo(Stream stream)
{
  if (stream == null) { throw new NullArgumentException(); }

  // ...method code...
}

, Scrivi e basta:

public void Foo(NonNullable<Stream> stream)
{
  // ...method code...
}

Quindi, ci sono tre casi d'uso:

  1. L'utente chiama Foo()con un valore nullo Stream:

    Stream stream = new Stream();
    Foo(stream);

    Questo è il caso d'uso desiderato e funziona con o senza NonNullable<>.

  2. L'utente chiama Foo()con un null Stream:

    Stream stream = null;
    Foo(stream);

    Questo è un errore di chiamata. Qui NonNullable<>aiuta a informare l'utente che non dovrebbero farlo, ma in realtà non li ferma. In entrambi i casi, ciò si traduce in un tempo di esecuzione NullArgumentException.

  3. L'utente chiama Foo()con null:

    Foo(null);

    nullnon si convertirà implicitamente in a NonNullable<>, quindi l'utente riceve un errore nell'IDE prima del runtime. Questo sta delegando il controllo nullo al sistema di tipi, proprio come l'SRP consiglierebbe.

Puoi estendere questo metodo per affermare anche altre cose sui tuoi argomenti. Ad esempio, poiché si desidera un flusso scrivibile, è possibile definire un struct WriteableStream<T> where T:Streamcontrollo per entrambi nulle stream.CanWritenel costruttore. Questo sarebbe ancora un controllo del tipo di runtime, ma:

  1. Decora il tipo con il WriteableStreamqualificatore, segnalando la necessità ai chiamanti.

  2. Esegue il controllo in un'unica posizione nel codice, quindi non è necessario ripetere il controllo e throw InvalidArgumentExceptionogni volta.

  3. Si conforma meglio all'SRP spingendo i compiti di verifica del tipo sul sistema di tipi (come esteso dai decoratori generici).


3

Il tuo approccio è attualmente procedurale. Stai rompendo l' Streamoggetto e convalidandolo dall'esterno. Non farlo: rompe l'incapsulamento. Lascia che Streamsia responsabile della propria convalida. Non possiamo cercare di applicare l'SRP finché non avremo alcune classi a cui applicarlo.

Ecco una Streamche esegue un'azione solo se supera la convalida:

class Stream
{
    public void someAction()
    {
        if(!stream.canWrite)
        {
            throw new ArgumentException();
        }

        System.out.println("My action");
    }
}

Ma ora stiamo violando SRP! "Una classe dovrebbe avere solo un motivo per cambiare." Abbiamo un mix di 1) validazione e 2) logica effettiva. Abbiamo due motivi per cui potrebbe essere necessario cambiare.

Possiamo risolverlo con validatori decoratori . Innanzitutto, dobbiamo convertire la nostra Streamin un'interfaccia e implementarla come una classe concreta.

interface Stream
{
    void someAction();
}

class DefaultStream implements Stream
{
    @Override
    public void someAction()
    {
        System.out.println("My action");
    }
}

Ora possiamo scrivere un decoratore che avvolge a Stream, esegue la validazione e difende il dato Streamper la logica effettiva dell'azione.

class WritableStream implements Stream
{
    private final Stream stream;

    public WritableStream(final Stream stream)
    {
        this.stream = stream;
    }

    @Override
    public void someAction()
    {
        if(!stream.canWrite)
        {
            throw new ArgumentException();
        }
        stream.someAction();
    }
}

Ora possiamo comporli come preferiamo:

final Stream myStream = new WritableStream(
    new DefaultStream()
);

Desideri una convalida aggiuntiva? Aggiungi un altro decoratore.


1

Il lavoro di una classe è fornire un servizio che soddisfi un contratto . Una classe ha sempre un contratto: un insieme di requisiti per usarlo, e promette che fa riguardo al suo stato e ai risultati a condizione che i requisiti siano soddisfatti. Il presente contratto può essere esplicito, attraverso documentazione e / o asserzioni, o implicito, ma esiste sempre.

Parte del contratto della tua classe è che il chiamante fornisce al costruttore alcuni argomenti che non devono essere nulli. L'attuazione del contratto è responsabilità della classe, quindi verificare che il chiamante abbia soddisfatto la sua parte del contratto può essere facilmente considerato rientrante nell'ambito della responsabilità della classe.

L'idea che una classe attui un contratto è dovuta a Bertrand Meyer , il progettista del linguaggio di programmazione Eiffel e dell'idea del design per contratto . La lingua Eiffel rende le specifiche e il controllo della parte contrattuale della lingua.


0

Come è stato sottolineato in altre risposte, SRP è spesso frainteso. Non si tratta di avere un codice atomico che svolge una sola funzione. Si tratta di assicurarsi che i tuoi oggetti e metodi facciano solo una cosa e che l'unica cosa sia fatta in un solo posto.

Vediamo un esempio scarso nello pseudo codice.

class Math
    private int a;
    private int b;
    def constructor(int x, int y) 
        if(x != null)
          a = x
        else if(x < 0)
          a = abs(x)
        else if (x == -1)
          throw "Some Silly Error"
        else
          a = 0
        end
        if(y != null)
           b = y
        else if(y < 0)
           b = abs(y)
        else if(y == -1)
           throw "Some Silly Error"
        else
         b = 0
        end
    end
    def add()
        return a + b
    end
    def sub()
        return b - a
    end
end

Nel nostro esempio piuttosto assurdo, la "responsabilità" del costruttore Math # è di rendere utilizzabile l'oggetto matematico. Lo fa sanificando prima l'input, quindi assicurandosi che i valori non siano -1.

Questo è SRP valido perché il costruttore sta facendo solo una cosa. Sta preparando l'oggetto Math. Tuttavia non è molto mantenibile. Viola il secco.

Quindi facciamo un altro passaggio

class Math
    private int a;
    private int b;
    def constructor(int x, int y)
        cleanX(x)
        cleanY(y)
    end
    def cleanX(int x)
        if(x != null)
          a = x
        else if(x < 0)
          a = abs(x)
        else if (x == -1)
          throw "Some Silly Error"
        else
          a = 0
        end
   end
   def cleanY(int y)
        if(y != null)
           b = y
        else if(y < 0)
           b = abs(y)
        else if(y == -1)
           throw "Some Silly Error"
        else
         b = 0
        end
    end
    def add()
        return a + b
    end
    def sub()
        return b - a
    end
end

In questo passaggio siamo migliorati un po 'con DRY, ma abbiamo ancora modi per andare con DRY. SRP d'altra parte sembra un po 'fuori. Ora abbiamo due funzioni con lo stesso lavoro. Sia cleanX che cleanY sterilizzano l'input.

Diamo un altro tentativo

class Math
    private int a;
    private int b;
    def constructor(int x, int y)
        a = clean(x)
        b = clean(y)
    end
    def clean(int i)
        if(i != null)
          return i
        else if(i < 0)
          return abs(i)
        else if (i == -1)
          throw "Some Silly Error"
        else
          return 0
        end
    end
    def add()
        return a + b
    end
    def sub()
        return b - a
    end
end

Ora finalmente sono stati meglio con DRY e SRP sembra essere d'accordo. Abbiamo solo un posto che svolge il compito di "sanificazione".

In teoria, il codice è più gestibile e migliore, ma quando andiamo a correggere il bug e a rafforzare il codice, dobbiamo solo farlo in un unico posto.

class Math
    private int a;
    private int b;
    def constructor(int x, int y)
        a = clean(x)
        b = clean(y)
    end
    def clean(int i)
        if(i == null)
          return 0
        else if (i == -1)
          throw "Some Silly Error"
        else
          return abs(i)
        end
    end
    def add()
        return a + b
    end
    def sub()
        return b - a
    end
end

Nella maggior parte dei casi del mondo reale gli oggetti sarebbero più complessi e SRP verrebbe applicato su un mucchio di oggetti. Ad esempio l'età può appartenere a Padre, Madre, Figlio, Figlia, quindi invece di avere 4 classi che determinano l'età dalla data di nascita hai una classe Persona che lo fa e le 4 classi ereditano da quello. Ma spero che questo esempio aiuti a spiegare. SRP non riguarda le azioni atomiche, ma il completamento di un "lavoro".


-3

Parlando di SRP, lo zio Bob non ama i controlli null sparsi ovunque. In generale, come squadra, dovresti evitare di utilizzare parametri nulli per i costruttori ogni volta che è possibile. Quando pubblichi il tuo codice al di fuori del tuo team, le cose potrebbero cambiare.

L'applicazione della non annullabilità dei parametri del costruttore senza prima assicurare la coesione della classe in questione provoca un gonfiamento nel codice chiamante, in particolare i test.

Se vuoi davvero far rispettare tali contratti, considera l'utilizzo Debug.Asserto qualcosa di simile per ridurre il disordine:

public AClassThatDefinitelyNeedsAWritableStream(Stream stream)
{
   Assert.That(stream.CanWrite, "Put crucial information here, and not inane bloat.");

   // Go on normal operation.
}
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.