Come applicare SRP in un ciclo in cui è necessario fare due cose?

2

Esiste un modo per applicare il principio di responsabilità singola a una funzione in cui è necessario che due elementi si ripetano in un ciclo per non dover ripetere l'iterazione due volte?

Ad esempio, supponiamo di avere una funzione come la seguente (in pseudo-Python):

def process_stuff(list):
    for item in list:
        process_one_way(item)
        process_another_way(item)

In questa funzione, i dati devono essere elaborati due volte in due modi completamente separati (in particolare, un esempio con cui ho lavorato di recente coinvolge sia la creazione di alcuni oggetti dai dati nell'elenco, sia la generazione di JSON separati per un altro scopo). Sento che questo viola l'SRP perché questa funzione sta facendo due cose completamente separate con i dati. Un modo naturale per separarli sarebbe:

def process_stuff(list):
    process_one_way(list)
    process_another_way(list)

def process_one_way(list):
    for item in list:
        # process

def process_another_way(list):
    for item in list:
        # process

... ma ciò comporta la ripetizione di iterazioni attraverso l'elenco due volte, il che sembra uno spreco. C'è un modo migliore per separare questi due compiti o sto fraintendendo qual è l'intenzione dell'SRP?

    
posta charlieshades 01.08.2017 - 06:51
fonte

6 risposte

11

L'SRP non dice che una funzione dovrebbe fare solo una cosa. Se così fosse, non potresti scrivere un'applicazione o un sistema che fa più di una cosa!

Quello che dovresti considerare è: le due operazioni saranno sempre eseguite sulle stesse collezioni allo stesso tempo? Quindi appartengono allo stesso ciclo. Prendi questo esempio:

def end_of_year_stuff(customers):
    for customer in customers:
        send_christmas_card(customer)
        calculate_for_financial_statement(customer)

Qui è un po 'per caso queste due cose accadono allo stesso tempo. La società potrebbe cambiare il suo anno finanziario senza che il Natale venga spostato, o il Natale potrebbe essere annullato senza che ciò influisca sull'anno finanziario. Oppure potresti decidere di escludere i clienti in Agrabah dalla cartolina di Natale, ma devono comunque essere presi in considerazione per le relazioni finanziarie. In breve, le due preoccupazioni potrebbero cambiare in modo indipendente e quindi appartenere a cicli separati.

Ora prendi questo esempio:

   def roll_logs(logfiles):
       for logfile in logfiles:
          compress_file(logfile)
          move_to_backup_drive(logfile)

In questo caso le due operazioni appartengono insieme nello stesso ciclo, dal momento che si desidera assicurarsi che queste due cose avvengano insieme.

La cosa importante da capire è che SRP dipende dal significato e dallo scopo delle funzioni e delle operazioni coinvolte. Quindi, utilizzando i nomi "semant-free" come " process_stuff " e " process_one_way " nell'esempio, è impossibile dire qualcosa sul fatto che il codice non rispetti SRP o meno.

    
risposta data 01.08.2017 - 14:14
fonte
6

Direi che il codice fornito è molto vicino all'SRP, molto più vicino della maggior parte del codice di produzione che ho visto (triste, molto triste;)). La cosa che stai facendo con i dati è in fase di elaborazione. Ci saranno sempre metodi che richiedono più di un passaggio e continuerebbero ad aderire all'SRP. D'altra parte potresti argomentare che il tuo metodo fa davvero due cose: elaborare l'elenco e che elabora un elemento dall'elenco.

Potresti, tuttavia, migliorare la tua soluzione. Basta estrarre la parte che elabora l'oggetto. Questo metodo fa una cosa: elaborare l'oggetto. Ignorando se fosse un passo o due. E ora il tuo metodo esterno fa solo una cosa: elaborare l'elenco.

def process_stuff(list):
    for item in list:
        process_item(item)

def process_item(item):
    process_one_way(item)
    process_another_way(item)
    
risposta data 01.08.2017 - 07:15
fonte
1

Hai già le preoccupazioni separate in diverse funzioni. Man mano che vai più in alto nello stack delle chiamate troverai funzioni / classi / componenti più grandi che rappresentano operazioni su un livello di astrazione più alto. Il punto è nascondere i dettagli di implementazione mantenendo singoli concetti su ogni livello. Non finire con funzioni che hanno solo una linea.

Quello che vorresti considerare è se avrebbe mai senso eseguire l'unica operazione separata dall'altra operazione. Se è così, dovresti davvero dividerli. L'iterazione non è probabilmente costosa rispetto alle operazioni ripetute, per motivi di prestazioni probabilmente non ha senso inserirli in un unico ciclo.

    
risposta data 01.08.2017 - 07:27
fonte
1

Questo dipende da come si separano le responsabilità.

Se è significativo avere due cicli separati che hanno altri usi indipendenti, crearli e combinarli quando necessario:

def peel_potatoes(potatoes):
  for potato in potatoes:
    peel(potato)

def dice_vegetables(vegetables):
  for veg in vegetables:
    slice_and_dice(veg)

def prepare_potatoes(potatoes):
  peel_potatoes(potatoes)
  dice_vegetables(potatoes)

Se non hanno i casi separati precedenti, crea una procedura che completa il passaggio, quindi applicalo in loop:

def dress_up(person):
  put_on_pants(person)
  put_on_shirt(person)

for person in people:
  dress_up(person)

Di solito un design pulito vale la pena di essere pagato con un piccolo successo nelle prestazioni; con i loop come sopra, non è nemmeno il caso.

(Un caso più avanzato, quando l'elaborazione di un singolo elemento può essere eseguita in modo asincrono, di solito comporta le code combinate in modo simile.)

    
risposta data 01.08.2017 - 17:29
fonte
0

Essere una singola responsabilità si basa su prospettiva e scopo. Non abbiamo intenzione di rompere tutto fino al ribaltamento di un particolare bit.

Nel tuo esempio, il tuo ciclo potrebbe essere una singola transazione, che richiede di separare le funzioni da eseguire in un particolare ordine ed eventualmente avere la necessità di riportarle tutte indietro se l'intera transazione non può essere eseguita correttamente per un singolo articolo o l'intero elenco.

Un trasferimento deve essere trattato come una singola azione dal punto di vista della banca. Se non ci sono abbastanza soldi nell'Account A, non puoi mettere soldi nell'Account B. Ogni account vede il risultato individualmente.

Le banche devono elaborare i pagamenti degli interessi su tutti i conti, in modo che possano assicurarsi che abbiano fondi sufficienti per coprire tutti loro. Di nuovo, dal punto di vista della banca, si tratta di un singolo processo, ma ogni account ha il proprio deposito. Qualcuno sta per premere un pulsante e farlo accadere.

    
risposta data 01.08.2017 - 13:58
fonte
0

Potrebbe esserci un modo più semplice di pensarci.

def mapc(lis, fun)
   for item in lis
      fun(item)

A quel punto, puoi definire un particolare processore dell'articolo, ad esempio foo (), quindi

def foo(item)
   do_one_thing(item)
   do_other_thing(item)

mapc(my_table, foo)

Nota 1: il nome "mapc" deriva da LISP nei tempi antichi. Viene scelto per il suo valore mnemonico.

Nota 2: aspettatevi di dover spiegare questo ad alcuni dei vostri colleghi. Non hanno letto Abelson & La "Struttura e interpretazione dei programmi per computer" di Sussman e l'idea di separare il ciclo dall'elaborazione fatta nel loop potrebbero essere del tutto estranei a loro.

    
risposta data 01.08.2017 - 17:45
fonte

Leggi altre domande sui tag