L'aggiunta di un tipo restituito a un metodo di aggiornamento viola il "Principio di responsabilità singola"?


37

Ho un metodo che aggiorna i dati dei dipendenti nel database. La Employeeclasse è immutabile, quindi "aggiornare" l'oggetto significa effettivamente istanziare un nuovo oggetto.

Voglio che il Updatemetodo restituisca una nuova istanza di Employeecon i dati aggiornati, ma da ora posso dire che la responsabilità del metodo è quella di aggiornare i dati dei dipendenti e recuperare dal database un nuovo Employeeoggetto , viola il principio di responsabilità singola ?

Il record DB stesso viene aggiornato. Quindi, un nuovo oggetto viene istanziato per rappresentare questo record.


5
Un piccolo pseudo codice potrebbe fare molto qui: esiste effettivamente un nuovo record DB creato o viene aggiornato il record DB stesso, ma nel codice "client" viene creato un nuovo oggetto perché la classe è modellata come immutabile?
Martin Ba,

Oltre a ciò che ha chiesto Martin Bra, perché restituisci un'istanza Employee durante l'aggiornamento del database? I valori dell'istanza Employee non sono cambiati nel metodo Update, quindi perché restituirlo (i chiamanti hanno già accesso all'istanza ...). Oppure il metodo di aggiornamento recupera anche valori (potenzialmente diversi) dal database?
Thorsal,

1
@Torsale: se le tue entità di dati sono immutabili, la restituzione di un'istanza con dati aggiornati è praticamente SOP, poiché altrimenti dovresti fare tu stesso l'istanza dei dati modificati.
mikołak,

21
Compare-and-swape test-and-setsono operazioni fondamentali nella teoria della programmazione multi-thread. Entrambi sono metodi di aggiornamento con un tipo restituito e non potrebbero funzionare diversamente. Questo interrompe concetti come la separazione da query di comandi o il principio di responsabilità singola? Sì, e questo è il punto . L'SRP non è una cosa universalmente buona e può infatti essere attivamente dannoso.
MSalters,

3
@MSalters: esattamente. La separazione comando / query afferma che dovrebbe essere possibile emettere query che possono essere riconosciute come idempotenti ed emettere comandi senza dover attendere una risposta, ma la lettura atomica di lettura-modifica-scrittura dovrebbe essere riconosciuta come terza categoria di operazioni.
supercat

Risposte:


16

Come con qualsiasi regola, penso che l'importante qui sia considerare lo scopo della regola, lo spirito e non impantanarsi nell'analizzare esattamente come la regola è stata formulata in alcuni libri di testo e come applicarla a questo caso. Non abbiamo bisogno di avvicinarci a questo come avvocati. Lo scopo delle regole è di aiutarci a scrivere programmi migliori. Non è come lo scopo di scrivere programmi è di mantenere le regole.

Lo scopo della regola della responsabilità singola è rendere i programmi più facili da comprendere e mantenere facendo in modo che ciascuna funzione esegua una cosa autonoma e coerente.

Ad esempio, una volta ho scritto una funzione che ho chiamato qualcosa come "checkOrderStatus", che ha determinato se un ordine era in sospeso, spedito, riordinato, qualunque cosa, e restituito un codice che indica quale. Quindi è arrivato un altro programmatore e ha modificato questa funzione per aggiornare anche la quantità disponibile al momento della spedizione dell'ordine. Ciò ha violato gravemente il principio della responsabilità unica. Un altro programmatore che legge quel codice in seguito vedrebbe il nome della funzione, vedrebbe come veniva usato il valore restituito e potrebbe non sospettare mai che abbia fatto un aggiornamento del database. Qualcuno che aveva bisogno di ottenere lo stato dell'ordine senza aggiornare la quantità disponibile sarebbe in una posizione scomoda: dovrebbe scrivere una nuova funzione che duplica la parte dello stato dell'ordine? Aggiungi un flag per dirgli se fare l'aggiornamento db? Eccetera.

D'altra parte, non vorrei precisare ciò che costituisce "due cose". Di recente ho scritto una funzione che invia le informazioni sui clienti dal nostro sistema al sistema del nostro cliente. Tale funzione esegue una riformattazione dei dati per soddisfare le loro esigenze. Ad esempio, abbiamo alcuni campi che potrebbero essere nulli nel nostro database, ma non consentono valori nulli, quindi dobbiamo inserire del testo fittizio, "non specificato" o dimentico le parole esatte. Probabilmente questa funzione sta facendo due cose: riformattare i dati e inviarli. Ma ho deliberatamente messo questo in una singola funzione piuttosto che avere "riformattare" e "inviare" perché non voglio mai, mai inviare senza riformattare. Non voglio che qualcuno scriva una nuova chiamata e non si renda conto che deve chiamare il riformattatore e poi inviarlo.

Nel tuo caso, aggiorna il database e restituisci un'immagine del record scritto come due cose che potrebbero benissimo andare insieme logicamente e inevitabilmente. Non conosco i dettagli della tua applicazione, quindi non posso dire in modo definitivo se questa è una buona idea o meno, ma sembra plausibile.

Se stai creando un oggetto in memoria che contiene tutti i dati per il record, eseguendo le chiamate al database per scrivere questo e quindi restituendo l'oggetto, questo ha molto senso. Hai l'oggetto tra le mani. Perché non restituirlo? Se non restituissi l'oggetto, come lo otterrebbe il chiamante? Dovrebbe leggere il database per ottenere l'oggetto che hai appena scritto? Sembra piuttosto inefficiente. Come avrebbe trovato il disco? Conosci la chiave primaria? Se qualcuno dichiara che è "legale" per la funzione di scrittura restituire la chiave primaria in modo da poter rileggere il record, perché non restituire l'intero record in modo da non doverlo fare? Qual è la differenza?

D'altra parte, se la creazione dell'oggetto è un mucchio di lavoro abbastanza distinto dalla scrittura del record del database e un chiamante potrebbe voler scrivere, ma non creare l'oggetto, ciò potrebbe essere dispendioso. Se un chiamante potrebbe desiderare l'oggetto ma non fare la scrittura, allora dovresti fornire un altro modo per ottenere l'oggetto, il che potrebbe significare la scrittura di codice ridondante.

Ma penso che lo scenario 1 sia più probabile, quindi direi, probabilmente nessun problema.


Grazie per tutte le risposte, ma questa mi ha davvero aiutato di più.
Sipo,

Se non si desidera inviare senza riformattazione e non è necessario riformattare oltre a inviare i dati in un secondo momento, allora sono una cosa, non due.
Joker_vD

public function sendDataInClientFormat() { formatDataForClient(); sendDataToClient(); } private function formatDataForClient() {...} private function sendDataToClient() {...}
CJ Dennis,

@CJDennis Certo. E infatti è così che l'ho fatto: una funzione per eseguire la formattazione, una funzione per eseguire l'invio effettivo, e c'erano altre due funzioni che non entrerò qui. Quindi una funzione di livello superiore per chiamarli tutti nella sequenza corretta. Si potrebbe dire che "formattare e inviare" è un'operazione logica e quindi potrebbe essere correttamente combinata in una funzione. Se insisti sul fatto che sono due, ok, allora la cosa razionale da fare è avere una funzione di livello superiore che faccia tutto.
Jay,

67

Il concetto di SRP è quello di impedire ai moduli di fare 2 cose diverse quando lo fanno individualmente per una migliore manutenzione e meno spaghetti nel tempo. Come dice l'SRP "un motivo per cambiare".

Nel tuo caso, se avevi una routine che "aggiorna e restituisce l'oggetto aggiornato", stai ancora cambiando l'oggetto una volta - dandogli 1 motivo per cambiare. Il fatto che tu ritorni l'oggetto indietro non è né qui né lì, stai ancora operando su quel singolo oggetto. Il codice ha la responsabilità di una e una sola cosa.

L'SRP non riguarda davvero il tentativo di ridurre tutte le operazioni a una singola chiamata, ma di ridurre ciò su cui si sta operando e il modo in cui ci si sta operando. Quindi un singolo aggiornamento che restituisce l'oggetto aggiornato va bene.


7
In alternativa, non si tratta di "cercare di ridurre tutte le operazioni a una singola chiamata", ma di ridurre il numero di casi diversi a cui pensare quando si utilizza l'elemento in questione.
jpmc26,

46

Come sempre, questa è una domanda di laurea. L'SRP dovrebbe impedirti di scrivere un metodo che recupera un record da un database esterno, esegue una rapida trasformazione di Fourier su di esso e aggiorna un registro delle statistiche globali con il risultato. Penso che quasi tutti sarebbero d'accordo sul fatto che queste cose dovrebbero essere fatte con metodi diversi. Postulare una singola responsabilità per ciascun metodo è semplicemente il modo più economico e memorabile per fare questo punto.

All'altra estremità dello spettro ci sono metodi che forniscono informazioni sullo stato di un oggetto. Il tipico isActiveha dato queste informazioni come unica responsabilità. Probabilmente tutti concordano sul fatto che questo va bene.

Ora, alcuni estendono il principio così lontano che considerano la restituzione di una bandiera di successo una responsabilità diversa dall'esecuzione dell'azione il cui successo è stato segnalato. Sotto un'interpretazione estremamente rigorosa, questo è vero, ma poiché l'alternativa sarebbe quella di chiamare un secondo metodo per ottenere lo stato di successo, il che complica il chiamante, molti programmatori stanno perfettamente bene restituendo codici di successo da un metodo con effetti collaterali.

Restituire il nuovo oggetto è un ulteriore passo avanti verso molteplici responsabilità. Richiedere al chiamante di effettuare una seconda chiamata per un intero oggetto è leggermente più ragionevole che richiedere una seconda chiamata solo per vedere se la prima ha avuto successo o meno. Tuttavia, molti programmatori prenderebbero in considerazione la possibilità di restituire perfettamente il risultato di un aggiornamento. Sebbene ciò possa essere interpretato come due responsabilità leggermente diverse, non è certo uno degli abusi eclatanti che ha ispirato il principio all'inizio.


15
Se devi chiamare un secondo metodo per vedere se il primo ha avuto successo, non dovresti chiamare un terzo metodo per ottenere il risultato del secondo? Quindi se davvero vuoi il risultato, beh ...

3
Posso aggiungere che un'applicazione troppo zelante dell'SRP porta a una miriade di piccole classi, che è un onere in sé. Quanto è grande l'onere per l'ambiente, il compilatore, gli strumenti IDE / helper ecc.
Erik Alapää,

8

Viola il principio di responsabilità singola?

Non necessariamente. Semmai, ciò viola il principio della separazione comando-query .

La responsabilità è

aggiorna i dati dei dipendenti

con la comprensione implicita che questa responsabilità include lo stato dell'operazione; ad esempio, se l'operazione ha esito negativo, viene generata un'eccezione, se ha esito positivo, viene restituito il dipendente di aggiornamento, ecc.

Ancora una volta, questa è tutta una questione di grado e giudizio soggettivo.


E che dire della separazione tra query e comandi?

Bene, questo principio esiste, ma in realtà restituire il risultato di un aggiornamento è molto comune.

(1) Java Set#add(E)aggiunge l'elemento e restituisce la sua inclusione precedente nel set.

if (visited.add(nodeToVisit)) {
    // perform operation once
}

Questo è più efficiente dell'alternativa CQS, che potrebbe dover eseguire due ricerche.

if (!visited.contains(nodeToVisit)) {
    visited.add(nodeToVisit);
    // perform operation once
}

(2) Compare-and-swap , fetch-and-add e test-and-set sono primitivi comuni che consentono l'esistenza di una programmazione concorrente. Questi schemi appaiono spesso, dalle istruzioni della CPU di basso livello alle raccolte simultanee di alto livello.


2

La singola responsabilità riguarda una classe che non ha bisogno di cambiare per più di una ragione.

Ad esempio, un dipendente ha un elenco di numeri di telefono. Quando cambia il modo in cui gestisci i numeri di telefono (potresti aggiungere codici di chiamata nazionali) che non dovrebbe cambiare affatto la classe dei dipendenti.

Non avrei bisogno che la classe dei dipendenti sappia come si salva nel database, perché cambierebbe in seguito a cambiamenti nei dipendenti e cambiamenti nella modalità di archiviazione dei dati.

Simile non dovrebbe esserci un metodo CalculateSalary nella classe dipendente. Una proprietà salariale è ok, ma il calcolo delle tasse ecc. Dovrebbe essere fatto altrove.

Ma che il metodo Update restituisca ciò che ha appena aggiornato va bene.


2

WRT. il caso specifico:

Employee Update(Employee, name, password, etc) (attualmente utilizzo un Builder poiché ho molti parametri).

E sembra il Updatemetodo prende uno già esistente Employeecome primo parametro per identificare (?) Il dipendente esistente e una serie di parametri di cambiamento su questo dipendente.

Io non credo che questo potrebbe essere più pulito fatto. Vedo due casi:

(a) Employee contiene effettivamente un database / ID univoco mediante il quale può sempre essere identificato nel database. (Cioè, non è necessario impostare l'intero valore dei record per trovarlo nel DB.

In questo caso, preferirei un void Update(Employee obj)metodo, che trova semplicemente il record esistente per ID e quindi aggiorna i campi dall'oggetto passato. O forse unvoid Update(ID, EmployeeBuilder values)

Una variante di questo che ho trovato utile è avere solo un void Put(Employee obj)metodo che inserisce o aggiorna, a seconda che esista il record (per ID).

(b) La scheda completa esistente è necessario per DB di ricerca, in questo caso si potrebbe ancora fare più senso avere: void Update(Employee existing, Employee newData).

Per quanto posso vedere qui, direi davvero che la responsabilità di costruire un nuovo oggetto (o valori di sotto-oggetto) per archiviarlo e memorizzarlo effettivamente è ortogonale, quindi li separerei.

I requisiti concorrenti citati in altre risposte (set-and-recuperare / confrontare-scambiare atomici, ecc.) Non sono stati un problema nel codice DB su cui ho lavorato finora. Quando parlo con un DB, penso che questo dovrebbe essere gestito normalmente a livello di transazione, non a livello di singola istruzione. (Questo non vuol dire che potrebbe non esserci un progetto in cui un "atomico" Employee[existing data] Update(ID, Employee newData)non potrebbe avere senso, ma con DB accede ad esso non è qualcosa che normalmente vedo.)


1

Finora qui tutti parlano di lezioni. Ma pensaci dal punto di vista dell'interfaccia.

Se un metodo di interfaccia dichiara un tipo restituito e / o una promessa implicita sul valore restituito, ogni implementazione deve restituire l'oggetto aggiornato.

Quindi puoi chiedere:

  • Riesci a pensare a possibili implementazioni che non vogliono preoccuparsi di restituire un nuovo oggetto?
  • Riesci a pensare ai componenti che dipendono dall'interfaccia (facendo iniettare un'istanza), che non hanno bisogno dell'impiegato aggiornato come valore di ritorno?

Pensa anche agli oggetti finti per i test unitari. Ovviamente sarà più facile deridere senza il valore di ritorno. E i componenti dipendenti sono più facili da testare se le loro dipendenze iniettate hanno interfacce più semplici.

Sulla base di queste considerazioni è possibile prendere una decisione.

E se te ne pentirai in seguito, potresti comunque introdurre una seconda interfaccia, con adattatori tra i due. Naturalmente tali interfacce aggiuntive aumentano la complessità compositiva, quindi è tutto un compromesso.


0

Il tuo approccio va bene. L'immutabilità è una forte affermazione. L'unica cosa che vorrei chiedere è: c'è qualche altro posto in cui costruisci l'oggetto. Se il tuo oggetto non fosse immutabile, dovresti rispondere ad altre domande perché è stato introdotto "Stato". E il cambio di stato di un oggetto può avvenire per diversi motivi. Quindi dovresti conoscere i tuoi casi e non dovrebbero essere ridondanti o divisi.

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.