Come può il mio team evitare errori frequenti dopo il refactoring?


20

Per darti un piccolo esempio: lavoro per un'azienda con circa dodici sviluppatori di Ruby on Rails (+/- stagisti). Il lavoro a distanza è comune. Il nostro prodotto è composto da due parti: un nucleo piuttosto grasso e sottile fino ai grandi progetti dei clienti costruiti su di esso. I progetti dei clienti di solito espandono il nucleo. La sovrascrittura delle funzionalità principali non avviene. Potrei aggiungere che il nucleo ha alcune parti piuttosto brutte che hanno urgente bisogno di rifatturazioni. Ci sono specifiche, ma principalmente per i progetti dei clienti. La parte peggiore del core non è testata (non come dovrebbe essere ...).

Gli sviluppatori sono divisi in due team, lavorando con uno o due PO per ogni sprint. Di solito, un progetto cliente è strettamente associato a uno dei team e degli OP.

Ora il nostro problema: piuttosto frequentemente, ci spezziamo a vicenda. Qualcuno del team A espande o trasforma la funzione principale Y, causando errori imprevisti per uno dei progetti dei clienti del team B. Per lo più, i cambiamenti non sono annunciati sulle squadre, quindi i bug colpiscono quasi sempre inaspettati. La squadra B, incluso l'OP, ha ritenuto che la funzione Y fosse stabile e non l'ha testata prima di rilasciarla, ignara delle modifiche.

Come sbarazzarsi di quei problemi? Che tipo di "tecnica di annuncio" mi puoi consigliare?


34
La risposta ovvia è TDD .
mouviciel,

1
Come mai affermate che "sovrascrittura delle caratteristiche chiave non succede", e poi il problema è che non accada? Distingui nella tua squadra tra "core" e "caratteristiche chiave", e come lo fai?
Sto

4
@mouvciel Questo e non usare la digitazione dinamica , ma quel particolare consiglio arriva un po 'troppo tardi in questo caso.
Doval,

3
Usa un linguaggio fortemente tipizzato come OCaml.
Gaius,

@logc Forse non sono stato chiaro, scusa. Non sovrascriviamo una funzionalità di base come la libreria di filtri stessa, ma aggiungiamo nuovi filtri alle classi che utilizziamo nei progetti dei nostri clienti. Uno scenario comune può essere che i cambiamenti nella libreria dei filtri distruggano quei filtri aggiunti nel progetto del cliente.
SDD64,

Risposte:


24

Consiglierei di leggere Lavorando in modo efficace con il codice legacy di Michael C. Feathers . Spiega che hai davvero bisogno di test automatizzati, come puoi facilmente aggiungerli, se non li hai già, e quale "odore di codice" refactoring in che modo.

Oltre a ciò, un altro problema fondamentale nella tua situazione sembra una mancanza di comunicazione tra le due squadre. Quanto sono grandi queste squadre? Stanno lavorando su diversi arretrati?

È quasi sempre una cattiva pratica dividere i team in base alla propria architettura. Ad esempio un team principale e un team non core. Invece, creerei team su domini funzionali, ma tra componenti.


In "The Mythical Man-Month" ho letto che la struttura del codice di solito segue la struttura del team / organizzazione. Pertanto, questa non è in realtà una "cattiva pratica", ma solo il modo in cui le cose vanno di solito.
Marcel,

Penso che in " Dinamica dello sviluppo del software ", il responsabile di Visual C ++ raccomanda vivamente di avere team di funzionalità; Non ho letto "The Mythical Man-Month", @Marcel, ma AFAIK elenca le cattive pratiche del settore ...
logc,

Marcel, è vero che questo è il modo in cui le cose vanno o vanno di solito, ma sempre più team lo fanno in modo diverso, ad esempio i team di funzionalità. La presenza di team basati su componenti comporta una mancanza di comunicazione quando si lavora su funzionalità tra componenti. Oltre a ciò, quasi sempre si tradurrà in discussioni di architettura non basate sullo scopo di una buona architettura, ma su persone che cercano di spingere le responsabilità verso altri team / componenti. Quindi, otterrai la situazione descritta dall'autore di questa domanda. Vedi anche mountaingoatsoftware.com/blog/the-benefits-of-feature-teams .
Tohnmeister,

Bene, per quanto ho capito il PO, ha affermato che le squadre non sono divise in una squadra principale e non centrale. I team sono suddivisi "per cliente", che è essenzialmente "per dominio funzionale". E questo è parte del problema: poiché a tutti i team è consentito cambiare il nucleo comune, i cambiamenti da una squadra influiscono sull'altra.
Doc Brown,

@DocBrown Hai ragione. Ogni squadra può cambiare il nucleo. Naturalmente, questi cambiamenti dovrebbero essere utili per ogni progetto. Tuttavia, funzionano su diversi backlog. Ne abbiamo uno per ogni cliente e uno per il core.
SDD64,

41

La parte peggiore del core non è testata (come dovrebbe essere ...).

Questo è il problema. Il refactoring efficiente dipende fortemente dalla suite di test automatizzati. Se non li hai, iniziano a comparire i problemi che stai descrivendo. Ciò è particolarmente importante se si utilizza un linguaggio dinamico come Ruby, in cui non esiste un compilatore per rilevare errori di base relativi al passaggio di parametri ai metodi.


10
Quello e il refactoring a piccoli passi e l'impegno molto frequente.
Stefan Billiet,

1
Probabilmente ci sono risme di consigli che potrebbero aggiungere consigli qui, ma tutto si riduce a questo punto. Qualunque sia la battuta "come dovrebbe essere" dei PO che mostrano che sanno che è un problema in sé, l'impatto dei test con script sul refactoring è immenso: se un passaggio è diventato un fallimento, il refactoring non ha funzionato. Se tutti i passaggi rimangono passaggi, il refactoring potrebbe aver funzionato (spostare i passaggi non riesce sarebbe ovviamente un vantaggio, ma mantenere tutti i passaggi come passaggi è più importante di un guadagno netto; una modifica che interrompe un test e corregge cinque potrebbe essere un miglioramento, ma non un refactoring)
Jon Hanna,

Ti ho dato un "+1", ma penso che i "test automatici" non siano l'unico approccio per risolverlo. Un QA manuale migliore, ma sistematico, magari da parte di un team addetto al QA potrebbe risolvere anche i problemi di qualità (e probabilmente ha senso avere test sia automatici che manuali).
Doc Brown,

Un buon punto, ma se i progetti core e dei clienti sono moduli separati (e per di più in un linguaggio dinamico come Ruby), il core può cambiare sia un test che la sua implementazione associata , e rompere un modulo dipendente senza fallire i propri test.
logc,

Come altri hanno commentato. TDD. Probabilmente riconosci già che dovresti avere unit test per la maggior parte del codice possibile. Mentre scrivere test unitari per il solo fatto è uno spreco di risorse, quando inizi a refactoring qualsiasi componente dovresti iniziare con una scrittura di test approfondita prima di toccare il codice principale.
jb510,

5

Le precedenti risposte che indicano migliori test unitari sono buone, ma ritengo che potrebbero esserci problemi più fondamentali da affrontare. Sono necessarie interfacce chiare per accedere al codice principale dal codice per i progetti del cliente. In questo modo, se si esegue il refactoring del codice principale senza alterare il comportamento osservato attraverso le interfacce , il codice dell'altro team non si interromperà. Ciò renderà molto più facile sapere cosa può essere refactored "in modo sicuro" e che cosa ha bisogno di una, eventualmente rottura dell'interfaccia, riprogettazione.


Spot on. Test più automatizzati non porteranno altro che vantaggi e vale la pena farlo, ma qui non risolverà il problema principale che è un errore nel comunicare i cambiamenti fondamentali. Il disaccoppiamento avvolgendo le interfacce attorno a funzionalità importanti sarà un enorme miglioramento.
Bob Tway,

5

Altre risposte hanno messo in evidenza punti importanti (più unit test, feature team, interfacce pulite per i componenti principali), ma c'è un punto che trovo mancante, ovvero il versioning.

Se congeli il comportamento del tuo core eseguendo una release 1 e inserisci quella release in un sistema di gestione di artefatti privato 2 , qualsiasi progetto del cliente può dichiarare la sua dipendenza dalla versione core X e non verrà interrotto dalla prossima versione X + 1 .

La "politica di annuncio" si limita quindi a disporre di un file CHANGES insieme a ciascuna versione o a una riunione del team per annunciare tutte le funzionalità di ogni nuova versione principale.

Inoltre, penso che sia necessario definire meglio ciò che è "core" e quale sottoinsieme è "chiave". Sembri (correttamente) evitare di apportare molte modifiche ai "componenti chiave", ma permetti frequenti modifiche a "core". Per fare affidamento su qualcosa, devi mantenerlo stabile; se qualcosa non è stabile, non chiamarlo core. Forse potrei suggerire di chiamarlo componenti "helper"?

EDIT : se segui le convenzioni nel sistema di versione semantica , qualsiasi modifica incompatibile nell'API del core deve essere contrassegnata da una modifica della versione principale . Cioè, quando si modifica il comportamento del nucleo esistente in precedenza o si rimuove qualcosa, non si aggiunge semplicemente qualcosa di nuovo. Con tale convenzione, gli sviluppatori sanno che l'aggiornamento dalla versione '1.1' a '1.2' è sicuro, ma passare da '1.X' a '2.0' è rischioso e deve essere attentamente esaminato.

1: Penso che questo sia chiamato un gioiello, nel mondo di Ruby
2: l'equivalente di Nexus in Java o PyPI in Python


Il "versioning" è importante, in effetti, ma quando si cerca di risolvere il problema descritto congelando il core prima di un rilascio, si finisce facilmente con la necessità di sofisticate ramificazioni e fusioni. Il ragionamento è che durante una fase di "rilascio build" del team A, A potrebbe dover cambiare il core (almeno per la correzione di bug), ma non accetterà le modifiche al core da altri team - quindi finirai con un ramo di il nucleo per squadra, da unire "più tardi", che è una forma di debito tecnico. Questo a volte va bene, ma spesso rimanda il problema descritto a un momento successivo.
Doc Brown,

@DocBrown: sono d'accordo con te, ma ho scritto supponendo che tutti gli sviluppatori siano cooperativi e cresciuti. Questo non vuol dire che non ho visto quello che descrivi . Ma una parte fondamentale per rendere affidabile un sistema è, beh, cercare la stabilità. Inoltre, se la squadra A deve cambiare X nel nucleo, e la squadra B deve cambiare X nel nucleo, allora forse X non appartiene al nucleo; Penso che sia l'altro mio punto. :)
logc,

@DocBrown Sì, abbiamo imparato a utilizzare un ramo del core per ogni progetto del cliente. Ciò ha causato altri problemi. Ad esempio, non ci piace "toccare" i sistemi dei clienti già distribuiti. Di conseguenza, dopo ogni distribuzione potrebbero verificarsi diversi salti di versione minori del core utilizzato.
SDD64,

@ SDD64: è esattamente quello che sto dicendo: non integrare le modifiche immediatamente in un core comune non è una soluzione a lungo termine. Ciò di cui hai bisogno è una migliore strategia di test per il tuo core, anche con test automatici e manuali.
Doc Brown,

1
Per la cronaca, non sto sostenendo un nucleo separato per ogni squadra, né negando la necessità di test, ma un test di base e la sua implementazione possono cambiare allo stesso tempo, come ho commentato prima . Solo un core congelato, contrassegnato da una stringa di rilascio o da un tag di commit, può essere invocato da un progetto che si basa su di esso (escludendo correzioni di bug e purché la politica di controllo delle versioni sia valida).
logc,

3

Come altre persone hanno detto, una buona suite di unit test non risolverà il problema: avrai problemi durante l'unione delle modifiche, anche se ogni suite di test del team supera.

Lo stesso vale per TDD. Non vedo come possa risolverlo.

La tua soluzione non è tecnica. Devi definire chiaramente i confini "fondamentali" e assegnare un ruolo di "cane da guardia" a qualcuno, che sia lo sviluppatore principale o l'architetto. Qualsiasi modifica al core deve passare attraverso questo watchdog. È responsabile di assicurarsi che ogni risultato di tutte le squadre si fonderà senza troppi danni collaterali.


Avevamo un "cane da guardia", dal momento che ha scritto la maggior parte del core. Purtroppo, era anche responsabile della maggior parte delle parti non testate. Era impersonato YAGNI ed è stato sostituito mezzo anno fa da altri due ragazzi. Facciamo ancora fatica a rimodellare quelle "parti oscure".
SDD64,

2
L'idea è quella di avere una suite di test unitari per il core , che fa parte del core , con contributi di tutti i team, non suite di test separate per ciascun team.
Doc Brown,

2
@ SDD64: sembra che tu confonda "Non hai ancora bisogno (ancora)" (che è una cosa molto buona) con "Non hai bisogno di ripulire il tuo codice (ancora)" - che è un'abitudine estremamente cattiva e IMHO al contrario.
Doc Brown,

La soluzione watchdog è davvero, davvero non ottimale, IMHO. È come costruire un singolo punto di errore nel tuo sistema e, soprattutto, molto lento, perché coinvolge una persona e una politica. Altrimenti, TDD può ovviamente aiutare con questo problema: ogni test di base è un esempio per gli sviluppatori di progetti dei clienti su come dovrebbe essere utilizzato il core corrente. Ma penso che tu abbia dato la tua risposta in buona fede ...
logc,

@DocBrown: Okay, forse le nostre comprensioni differiscono. Le caratteristiche principali, scritte da lui, sono eccessivamente complicate per soddisfare anche le possibilità più strane. Molti di loro non li abbiamo mai incontrati. La complessità ci rallenta per riformattarli, dall'altra parte.
SDD64,

2

Come soluzione più a lungo termine, è necessaria anche una comunicazione migliore e più tempestiva tra i team. Ognuno dei team che utilizzerà mai, ad esempio, la funzione principale Y, deve essere coinvolto nella costruzione delle prove previste per la funzione. Questa pianificazione, di per sé, metterà in evidenza i diversi casi d'uso inerenti alla caratteristica Y tra i due team. Una volta che il modo in cui la funzionalità dovrebbe funzionare è inchiodato e le prove sono implementate e concordate, è necessario un ulteriore cambiamento nel tuo schema di implementazione. Il team che rilascia la funzione è necessario per eseguire il testcase, non il team che sta per usarlo. L'eventuale compito che dovrebbe causare collisioni è l'aggiunta di un nuovo testcase da parte di entrambi i team. Quando un membro del team pensa a un nuovo aspetto della funzione che non è stato testato, dovrebbero essere liberi di aggiungere un testcase che hanno verificato passando nella propria sandbox. In questo modo, le uniche collisioni che si verificheranno saranno a livello di intento e dovrebbero essere inchiodate prima che la funzione refactored venga rilasciata in natura.


2

Sebbene ogni sistema necessiti di suite di test efficaci (il che significa, tra le altre cose, automazione), e mentre questi test, se usati in modo efficace, rileveranno questi conflitti prima di quanto non siano ora, questo non affronta i problemi sottostanti.

La domanda rivela almeno due problemi sottostanti: la pratica di modificare il "core" al fine di soddisfare i requisiti per i singoli clienti e l'incapacità dei team di comunicare e coordinare il loro intento di apportare modifiche. Nessuna di queste sono cause alla radice e dovrai capire perché questo è stato fatto prima di poterlo risolvere.

Una delle prime cose da determinare è se sia gli sviluppatori che i gestori si rendono conto che qui c'è un problema. Se almeno alcuni lo fanno, allora devi scoprire perché o pensano di non poter fare nulla al riguardo o scelgono di non farlo. Per coloro che non lo fanno, potresti provare ad aumentare la loro capacità di anticipare in che modo le loro azioni attuali possono creare problemi futuri o sostituirli con persone che possono. Fino a quando non avrai una forza lavoro consapevole di come stanno andando le cose, è improbabile che tu sia in grado di risolvere il problema (e forse nemmeno allora, almeno a breve termine).

Potrebbe essere difficile analizzare il problema in termini astratti, almeno inizialmente, quindi concentrati su un incidente specifico che ha provocato un problema e prova a determinare come è successo. Poiché è probabile che le persone coinvolte siano difensive, sarà necessario essere vigili per giustificazioni egoistiche e post-hoc al fine di scoprire cosa sta realmente accadendo.

C'è una possibilità che esito a menzionare perché è così improbabile: i requisiti dei clienti sono così disparati che non c'è comunanza sufficiente per giustificare il codice core condiviso. Se è così, allora in realtà hai più prodotti separati e dovresti gestirli come tali e non creare un accoppiamento artificiale tra di loro.


Prima di migrare il nostro prodotto da Java a RoR, in realtà abbiamo fatto come mi hai suggerito. Avevamo un core Java per tutti i clienti, ma le loro esigenze lo hanno "rotto" un giorno e abbiamo dovuto dividerlo. Durante quella situazione, abbiamo riscontrato problemi come: 'Amico, il cliente Y ha una caratteristica così bella. Peccato che non possiamo portarlo al cliente Z, perché il loro nucleo è incompatibile '. Con Rails, vogliamo rigorosamente optare per una politica "un core per tutti". In tal caso, offriamo ancora cambiamenti drastici, ma questi distaccano il cliente da ulteriori aggiornamenti.
SDD64,

Chiamare TDD non mi sembra abbastanza. Quindi, oltre alla scissione del suggerimento principale, mi piace di più la tua risposta. Purtroppo, il core non è perfettamente testato, ma ciò non risolverebbe tutti i nostri problemi. L'aggiunta di nuove funzionalità di base per un cliente può sembrare perfettamente soddisfacente e persino dare una build verde, per loro, perché solo le specifiche di base sono condivise tra i clienti. Non si nota, cosa succede a ogni possibile cliente. Quindi, mi piace il tuo suggerimento per scoprire i problemi e parlare di ciò che li ha causati.
SDD64,

1

Sappiamo tutti che i test unitari sono la strada da percorrere. Ma sappiamo anche che è realisticamente retrodiffondere questi elementi al nucleo è difficile.

Una tecnica specifica che potrebbe essere utile durante l'estensione della funzionalità consiste nel provare temporaneamente e localmente a verificare che la funzionalità esistente non sia stata modificata. Questo può essere fatto in questo modo:

Pseudo codice originale:

def someFunction
   do original stuff
   return result
end

Codice di prova sul posto temporaneo:

def someFunctionNew
   new do stuff
   return result
end

def someFunctionOld
   do original stuff
   return result
end

def someFunction
   oldResult = someFunctionOld
   newResult = someFunctionNew
   check oldResult = newResult
   return newResult
end

Esegui questa versione attraverso qualsiasi test a livello di sistema esistente. Se tutto va bene, sai che non hai rotto le cose e puoi quindi procedere con la rimozione del vecchio codice. Si noti che quando si controlla la corrispondenza dei risultati vecchi e nuovi, è possibile anche aggiungere codice per analizzare le differenze per acquisire casi che si sa che dovrebbero essere diversi a causa di una modifica prevista come una correzione di bug.


1

"Principalmente, i cambiamenti non sono annunciati sui team, quindi i bug colpiscono quasi sempre inaspettatamente"

Problemi di comunicazione a nessuno? Che dire di (oltre a quello che tutti gli altri hanno già sottolineato, che dovresti eseguire test rigorosi) per assicurarti che ci sia una comunicazione adeguata? Che le persone siano consapevoli che l'interfaccia su cui stanno scrivendo cambierà nella prossima versione e quali saranno tali cambiamenti?
E dare loro l'accesso a almeno un'interfaccia fittizia (con implementazione vuota) il più presto possibile durante lo sviluppo in modo che possano iniziare a scrivere il proprio codice.

Senza tutto ciò, i test unitari non faranno molto se non sottolineare durante le fasi finali che c'è qualcosa che va fuori di testa tra le parti del sistema. Lo vuoi sapere, ma lo vuoi sapere presto, molto presto e avere le squadre che parlano tra loro, coordinare gli sforzi e avere effettivamente accesso frequente al lavoro svolto dall'altra squadra (quindi si impegna regolarmente, non uno massiccio commettere dopo alcune settimane o mesi, 1-2 giorni prima della consegna).
Il tuo bug NON è nel codice, certamente non nel codice dell'altra squadra che non sapeva che stavi scherzando con l'interfaccia contro cui stanno scrivendo. Il tuo bug è nel tuo processo di sviluppo, la mancanza di comunicazione e collaborazione tra le persone. Solo perché sei seduto in stanze diverse non significa che dovresti isolarti dagli altri ragazzi.


1

In primo luogo, hai un problema di comunicazione (probabilmente anche collegato a un problema di team building ), quindi penso che una soluzione al tuo caso dovrebbe essere focalizzata su ... beh, sulla comunicazione, piuttosto che sulle tecniche di sviluppo.

Dò per scontato che non è possibile congelare o fork il modulo principale quando si avvia un progetto del cliente (altrimenti è necessario semplicemente integrare nei programmi della propria società alcuni progetti non relativi al cliente che mirano ad aggiornare il modulo principale).

Quindi ci resta il problema di provare a migliorare la comunicazione tra i team. Questo può essere affrontato in due modi:

  • con gli esseri umani. Ciò significa che la tua azienda designa qualcuno come l' architetto del modulo principale (o qualunque linguaggio che sia adatto al top management) che sarà responsabile della qualità e della disponibilità del codice. Questa persona incarnerà il nucleo. Pertanto, sarà condivisa da tutte le squadre e assicurerà una corretta sincronizzazione tra di loro. Inoltre, dovrebbe anche agire come revisore del codice impegnato nel modulo principale per mantenere la sua coerenza;
  • con strumenti e flussi di lavoro. Imponendo l' integrazione continua sul core, renderai il codice core stesso il mezzo di comunicazione. Ciò richiederà prima qualche sforzo (con l'aggiunta di suite di test automatizzate su di esso), ma poi i rapporti CI notturni saranno un aggiornamento di stato grossolano del modulo principale.

Puoi trovare ulteriori informazioni su CI come processo di comunicazione qui .

Infine, avrai ancora un problema con la mancanza di lavoro di squadra a livello aziendale. Non sono un grande fan degli eventi di team building, ma questo sembra un caso in cui sarebbero utili. Hai incontri a livello di sviluppatore su base regolare? Puoi invitare persone di altri team alle retrospettive del tuo progetto? O forse hai qualche birra del venerdì sera a volte?

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.