Gestire le intersezioni di funzioni


11

Di recente ho assistito a sempre più problemi simili a quelli spiegati in questo articolo sulle intersezioni di funzioni. Un altro termine sarebbe linee di prodotto, anche se tendo ad attribuirle a prodotti effettivamente diversi, mentre di solito incontro questi problemi sotto forma di possibili configurazioni di prodotto.

L'idea di base di questo tipo di problema è semplice: aggiungi una funzionalità a un prodotto, ma in qualche modo le cose si complicano a causa di una combinazione di altre funzionalità esistenti. Alla fine, il QA trova un problema con una rara combinazione di funzionalità che nessuno ha mai pensato prima e che cosa avrebbe dovuto essere un semplice bugfix potrebbe persino trasformarsi in richiedere importanti modifiche alla progettazione.

Le dimensioni di questo problema di intersezione di caratteristiche sono di una complessità strabiliante. Supponiamo che la versione corrente del software abbia Nfunzionalità e tu aggiunga una nuova funzionalità. Semplifichiamo anche le cose dicendo che ciascuna delle funzionalità può essere attivata o disattivata solo, quindi hai già 2^(N+1)possibili combinazioni di funzionalità da considerare. A causa della mancanza di migliori termini di formulazione / ricerca, mi riferisco all'esistenza di queste combinazioni come problema di intersezione di caratteristiche . (Punti bonus per una risposta che includono riferimenti a un termine più definito.)

Ora la domanda con cui ho difficoltà è come affrontare questo problema di complessità ad ogni livello del processo di sviluppo. Per ovvi motivi di costo, non è pratico fino al punto di essere utopistici, voler affrontare ogni combinazione individualmente. Dopotutto, proviamo a stare lontani dagli algoritmi di complessità esponenziale per una buona ragione, ma trasformare lo stesso processo di sviluppo in un mostro di dimensioni esponenziali è destinato a portare a un totale fallimento.

Quindi, come ottenere il miglior risultato in modo sistematico che non esplode alcun budget ed è completo in un modo decente, utile e professionalmente accettabile.

  • Specifica: quando si specifica una nuova funzione, come si fa a garantire che funzioni bene con tutti gli altri bambini?

    Vedo che si potrebbe esaminare sistematicamente ogni funzionalità esistente in combinazione con la nuova funzionalità, ma sarebbe in isolamento dalle altre funzionalità. Data la natura complessa di alcune caratteristiche, questa visione isolata è spesso già così coinvolta che ha bisogno di un approccio strutturato tutto in sé, per non parlare del 2^(N-1)fattore causato dalle altre caratteristiche che uno ha ignorato volontariamente.

  • Implementazione: quando si implementa una funzionalità, come assicurarsi che il codice interagisca / intersechi correttamente in tutti i casi.

    Ancora una volta, mi chiedo la pura complessità. Conosco varie tecniche per ridurre il potenziale di errore di due funzioni che si intersecano, ma nessuna che si ridimensionerebbe in modo ragionevole. Presumo però che una buona strategia durante la specifica dovrebbe tenere a bada il problema durante l'implementazione.

  • Verifica: quando provi una funzione, come gestisci il fatto che puoi testare solo una frazione di questo spazio di intersezione della funzione?

    È abbastanza difficile sapere che testare una singola funzione in isolamento non garantisce nulla vicino al codice privo di errori, ma quando lo si riduce a una frazione di 2^-Nesso sembra che centinaia di test non coprano nemmeno una singola goccia d'acqua in tutti gli oceani messi insieme . Ancor peggio, gli errori più problematici sono quelli che derivano dall'intersezione di funzionalità, che non ci si potrebbe aspettare di portare a problemi - ma come si fa a testare questi se non ci si aspetta un'intersezione così forte?

Anche se vorrei sapere come gli altri affrontano questo problema, sono principalmente interessato alla letteratura o agli articoli che analizzano l'argomento in modo più approfondito. Quindi, se segui personalmente una determinata strategia, sarebbe bello includere nella tua risposta fonti corrispondenti.


6
Un'architettura applicativa progettata in modo ragionevole può accogliere nuove funzionalità senza capovolgere il mondo; l'esperienza è il grande livellatore qui. Detto questo, un'architettura del genere non è sempre facile da ottenere al primo tentativo, e talvolta è necessario apportare modifiche difficili. Il problema del test non è necessariamente il pantano che si capisce, se si sa come incapsulare correttamente le caratteristiche e le funzionalità e coprirle con test unitari adeguati.
Robert Harvey,

Risposte:


6

Sapevamo già matematicamente che la verifica di un programma è impossibile a tempo finito nel caso più generale, a causa del problema di arresto. Quindi questo tipo di problema non è nuovo.

In pratica, un buon design può fornire il disaccoppiamento in modo tale che il numero di funzioni intersecanti sia molto inferiore a 2 ^ N, sebbene sembri certamente essere superiore a N anche in sistemi ben progettati.

Per quanto riguarda le fonti, mi sembra che quasi tutti i libri o blog sulla progettazione di software stiano effettivamente cercando di ridurre il più possibile 2 ^ N, anche se non conosco nessuno che risolva il problema negli stessi termini di te fare.

Per un esempio di come il design possa essere d'aiuto in questo, nell'articolo citato alcune delle intersezioni tra le funzionalità sono avvenute perché la replica e l'indicizzazione sono state entrambe attivate dall'eTag. Se avessero avuto a disposizione un altro canale di comunicazione per segnalare la necessità di ciascuno di questi separatamente, probabilmente avrebbero potuto controllare l'ordine degli eventi più facilmente e avere meno problemi.

O forse no. Non so nulla di RavenDB. L'architettura non può prevenire i problemi di intersezione delle caratteristiche se le caratteristiche sono davvero inspiegabilmente intrecciate e non possiamo mai sapere in anticipo che non desidereremo una caratteristica che presenta davvero il caso peggiore di intersezione 2 ^ N. Ma l'architettura può almeno limitare le intersezioni a causa di problemi di implementazione.

Anche se mi sbaglio su RavenDB ed eTags (e lo sto usando solo per ragioni di discussione - sono persone intelligenti e probabilmente hanno capito bene), dovrebbe essere chiaro come l'architettura possa aiutare. La maggior parte dei modelli di cui parlano le persone sono progettate esplicitamente con l'obiettivo di ridurre il numero di modifiche al codice richieste dalle funzionalità nuove o modificabili. Ciò risale a molto tempo fa - ad esempio "Modelli di progettazione, Elementi di software riutilizzabile orientato agli oggetti", afferma l'introduzione "Ogni modello di progettazione consente a un aspetto dell'architettura di variare indipendentemente da altri aspetti, rendendo così un sistema più robusto rispetto a un particolare tipo di modificare".

Il mio punto è che si può avere un senso della Big O delle intersezioni di funzionalità in pratica, osservando cosa succede in pratica. Nella ricerca di questa risposta, ho scoperto che la maggior parte delle analisi dei punti funzione / sforzo di sviluppo (cioè - produttività) hanno riscontrato una crescita inferiore allo sforzo lineare del progetto per punto funzione o leggermente superiore alla crescita lineare. Che ho trovato un po 'sorprendente. Questo ha avuto un esempio piuttosto leggibile.

Questo (e studi simili, alcuni dei quali usano punti di funzione anziché linee di codice) non dimostrano che l'intersezione delle caratteristiche non si verifica e causa problemi, ma sembra una prova ragionevole che non è devastante nella pratica.


0

Questa non sarà la risposta migliore in alcun modo, ma ho pensato ad alcune cose che si intersecano con i punti della tua domanda, quindi ho pensato di menzionarle:

Supporto strutturale

Dal poco che ho visto, quando le funzionalità sono difettose e / o non si adattano bene agli altri, ciò è in gran parte dovuto al supporto scarso fornito dalla struttura / struttura di base del programma per gestirle / coordinarle. Trascorrere più tempo a completare e completare il nucleo, penso, dovrebbe facilitare l'aggiunta di nuove funzionalità.

Una cosa che ho trovato per essere comune nelle applicazioni dove lavoro è che la struttura di un programma è stato configurato per gestire uno di un tipo di oggetto o di un processo, ma un sacco di estensioni che abbiamo fatto o vogliono fare avere a che fare con la gestione di molti di un tipo. Se questo fosse stato preso maggiormente in considerazione all'inizio della progettazione dell'applicazione, avrebbe aiutato ad aggiungere queste funzionalità in un secondo momento.

Questo diventa piuttosto critico quando si aggiunge il supporto per più X che coinvolgono codice threaded / asincrono / guidato da eventi perché quella roba può andare male abbastanza rapidamente - Ho avuto la gioia di eseguire il debug di una serie di problemi relativi a questo.

È probabilmente difficile giustificare questo tipo di sforzo in anticipo, specialmente per prototipi o progetti una tantum - anche se alcuni di questi prototipi o one-off continuano ad essere riutilizzati o come (la base del) sistema finale, nel senso che la spesa sarebbe valsa la pena a lungo termine.

Design

Quando si progetta il nucleo di un programma, iniziare con un approccio dall'alto verso il basso può aiutare a trasformare le cose in blocchi gestibili e consente di avvolgere la testa attorno al dominio problematico; successivamente penso che dovrebbe essere usato un approccio dal basso verso l'alto : ciò contribuirà a rendere le cose più piccole, più flessibili e migliori da aggiungere in seguito. (Come menzionato nel link, fare le cose in questo modo rende più piccole le implementazioni delle funzionalità, il che significa meno conflitti / bug.)

Se ti concentri sui blocchi costitutivi del sistema e ti assicuri che tutti interagiscano bene, anche tutto ciò che è stato costruito li userà probabilmente e si integrerà meglio con il resto del sistema.

Quando viene aggiunta una nuova funzionalità, penso che potrebbe essere presa una strada simile nel progettarla come è stata presa progettando il resto del framework: decomporlo e poi andare dal basso verso l'alto. Se è possibile riutilizzare uno qualsiasi dei blocchi originali dal framework nell'implementazione della funzione, ciò sarebbe sicuramente utile; una volta terminato puoi aggiungere tutti i nuovi blocchi che ottieni dalla funzionalità a quelli già presenti nel framework principale, testandoli con il set originale di blocchi - in questo modo saranno compatibili con il resto del sistema e utilizzabili in futuro anche funzioni.

Semplificare!

Ho preso una posizione minimalista sul design negli ultimi tempi, iniziando con la semplificazione del problema, quindi semplificando la soluzione. Se è possibile dedicare del tempo per un secondo, semplificando l'iterazione del progetto su un progetto, è possibile che sia molto utile quando si aggiungono elementi in seguito.

Comunque, questo è il mio 2c.

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.