Come mantenere versioni diverse e personalizzate dello stesso software per più client


46

abbiamo più clienti con esigenze diverse. Sebbene il nostro software sia modulare in una certa misura, è quasi certo che dobbiamo adeguare la logica di business di ogni modulo qua e là un po 'per ogni cliente. I cambiamenti sono probabilmente troppo piccoli per giustificare la suddivisione del modulo in un modulo distinto (fisico) per ogni client, temo problemi con la build, un caos di collegamento. Tuttavia, tali modifiche sono troppo grandi e troppe per configurarle tramite switch in alcuni file di configurazione, poiché ciò comporterebbe problemi durante la distribuzione e probabilmente molti problemi di supporto, specialmente con amministratori di tipo armeggiare.

Vorrei che il sistema di creazione creasse più build, una per ciascun client, in cui le modifiche sono contenute in una versione specializzata del singolo modulo fisico in questione. Quindi ho alcune domande:

Consiglieresti di lasciare che il sistema di creazione crei più build? Come devo memorizzare le diverse personalizzazioni nel controllo del codice sorgente, in particolare svn?


4
Funziona #ifdefper te?
oh,

6
Le direttive del preprocessore possono diventare rapidamente molto complicate e rendere il codice più difficile da leggere e più difficile da eseguire il debug.
Falcon,

1
Dovresti includere dettagli sulla piattaforma attuale e sul tipo di applicazione (desktop / web) per risposte migliori. La personalizzazione in un'app C ++ desktop è completamente diversa da un'app Web PHP.
GrandmasterB

2
@Falcon: da quando hai scelto nel 2011 una risposta di cui ho molti dubbi, puoi dirci se hai avuto qualche esperienza con l'utilizzo di SVN nel modo suggerito nel mezzo? Le mie obiezioni sono fondate male?
Doc Brown,

2
@Doc Brown: la ramificazione e la fusione sono state noiose e complicate. Allora, abbiamo utilizzato un sistema di plug-in con plug-in specifici per client o "patch" che hanno modificato il comportamento o le configurazioni. Avrai un certo sovraccarico, ma può essere gestito con Dependendy Injection.
Falcon,

Risposte:


7

Ciò di cui hai bisogno è un'organizzazione del codice simile alla ramificazione delle caratteristiche . Nel tuo scenario specifico, questo dovrebbe essere chiamato branching specifico del client perché probabilmente utilizzerai anche il branching delle funzionalità mentre sviluppi nuovi elementi (o risolvi i bug).

http://svnbook.red-bean.com/en/1.5/svn.branchmerge.commonpatterns.html

L'idea è di avere un trunk di codice con i rami delle nuove funzionalità uniti. Intendo tutte le funzionalità non specifiche del cliente.

Poi ci sono anche i tuoi rami specifiche per il cliente , dove vengono uniti anche i rami stesse caratteristiche come richiesto (cherry-picking se si vuole).

Questi rami dei clienti sembrano simili ai rami delle caratteristiche anche se non sono temporanei o di breve durata. Sono mantenuti per un periodo di tempo più lungo e sono principalmente uniti. Dovrebbe esserci il minor sviluppo possibile di diramazioni specifiche per client.

I rami specifici del cliente sono rami paralleli al tronco e sono attivi fintanto che il tronco stesso e non si fondono nemmeno ovunque.

            feature1
            ———————————.
                        \
trunk                    \
================================================== · · ·
      \ client1            \
       `========================================== · · ·
        \ client2            \
         `======================================== · · ·
              \ client2-specific feature   /
               `——————————————————————————´

7
+1 per la diramazione delle funzioni. Puoi usare un ramo anche per ogni cliente. Vorrei suggerire solo una cosa: utilizzare un VCS distribuito (hg, git, bzr) invece di SVN / CVS per ottenere questo risultato;)
Herberth Amaral

43
-1. Non è a questo che servono i "rami delle caratteristiche"; contraddice la loro definizione di "rami temporanei che si fondono quando lo sviluppo di una funzione è terminato".
P:

14
Devi ancora mantenere tutti quei delta nel controllo del codice sorgente e tenerli allineati, per sempre. A breve termine potrebbe funzionare. A lungo termine potrebbe essere terribile.
quick_now

4
-1, questo non è un problema di denominazione: l'utilizzo di rami diversi per client diversi è solo un'altra forma di duplicazione del codice. Questo è IMHO un anti-pattern, un esempio di come non farlo.
Doc Brown,

7
Robert, penso di aver capito abbastanza bene quello che mi hai suggerito, anche prima della modifica, ma penso che questo sia un approccio orribile. Supponiamo che tu abbia N client, ogni volta che aggiungi una nuova funzionalità principale al trunk, sembra che SCM renderà più semplice la propagazione della nuova funzionalità agli N rami. Ma l'utilizzo di filiali in questo modo rende troppo facile evitare una chiara separazione delle modifiche specifiche del cliente. Di conseguenza, ora hai N possibilità di ottenere un conflitto di unione per ogni modifica nel trunk. Inoltre, ora devi eseguire N test di integrazione anziché uno.
Doc Brown,

38

Non farlo con i rami SCM. Rendi il codice comune un progetto separato che produce una libreria o un artefatto scheletro di progetto. Ogni progetto del cliente è un progetto separato che quindi dipende da quello comune come dipendenza.

Questo è più semplice se l'app di base comune utilizza un framework di iniezione delle dipendenze come Spring, in modo da poter iniettare facilmente diverse varianti di sostituzione di oggetti in ciascun progetto del cliente in quanto sono richieste funzionalità personalizzate. Anche se non hai già un framework DI, aggiungerne uno e farlo in questo modo potrebbe essere la scelta meno dolorosa.


Un altro approccio per mantenere le cose semplici è usare l'ereditarietà (e un singolo progetto) invece di usare l'iniezione delle dipendenze, mantenendo sia il codice comune che le personalizzazioni nello stesso progetto (usando l'ereditarietà, le classi parziali o gli eventi). Con questo design, i rami client funzionerebbero bene (come descritto nella risposta selezionata) e si potrebbero sempre aggiornare i rami client aggiornando il codice comune.
drizin,

Se non mi manca qualcosa, ciò che suggerisci è che se hai 100 clienti, creerai (100 x NumberOfChangedProjects ) e utilizzerai DI per gestirli? Se è così, starei sicuramente lontano da questo tipo di soluzione poiché la manutenibilità sarebbe terribile ..
fatto il

13

Archivia il software per tutti i client in un singolo ramo. Non è necessario divergere tra le modifiche apportate a clienti diversi. Molto probabilmente, vorrai che il tuo software sia della migliore qualità per tutti i client e la correzione di bug per la tua infrastruttura di base dovrebbe interessare tutti senza un inutile overhead di fusione, che potrebbe anche introdurre più bug.

Modularizza il codice comune e implementa il codice che differisce in file diversi o proteggilo con definizioni diverse. Fai in modo che il tuo sistema di generazione abbia target specifici per ogni client, ogni target compilando una versione con solo il codice relativo a un client. Se il tuo codice è C, ad esempio, potresti voler proteggere le funzionalità per diversi client con " #ifdef" o qualunque meccanismo la tua lingua abbia per la gestione della configurazione della build e preparare un insieme di definizioni corrispondenti alla quantità di funzionalità per cui un cliente ha pagato.

Se non ti piacciono gli ifdefs, usa "interfacce e implementazioni", "funzioni", "file oggetto" o qualsiasi altro strumento fornito dalla tua lingua per memorizzare cose diverse in un unico posto.

Se distribuisci le fonti ai tuoi clienti, è meglio fare in modo che i tuoi script di compilazione abbiano "target di distribuzione delle fonti" speciali. Una volta invocato un tale obiettivo, crea una versione speciale dei sorgenti del software, li copia in una cartella separata in modo da poterli spedire e non li compila.


8

Come molti hanno affermato: fattorizza correttamente il tuo codice e personalizzalo in base al codice comune — questo migliorerà significativamente la manutenibilità. Sia che tu stia usando un linguaggio / sistema orientato agli oggetti o meno, questo è possibile (anche se è un po 'più difficile da fare in C di qualcosa che è veramente orientato agli oggetti). Questo è precisamente il tipo di problema che l'ereditarietà e l'incapsulamento aiutano a risolvere!


5

Molto attentamente

La funzione Branching è un'opzione ma trovo che sia piuttosto pesante. Inoltre semplifica le modifiche profonde che possono portare a un biforcimento definitivo della tua applicazione se non tenuto sotto controllo. Idealmente, vuoi aumentare il più possibile le personalizzazioni nel tentativo di mantenere la tua base di codice più comune e generica possibile.

Ecco come lo farei, anche se non so se sia applicabile alla tua base di codice senza pesanti modifiche e ripensamenti. Avevo un progetto simile in cui le funzionalità di base erano le stesse ma ogni cliente richiedeva un set di funzionalità molto specifico. Ho creato un insieme di moduli e contenitori che poi assemblo attraverso la configurazione (alla IoC).

quindi per ogni cliente ho creato un progetto che sostanzialmente contiene le configurazioni e lo script di creazione per creare un'installazione completamente configurata per il loro sito. Occasionalmente inserisco anche alcuni componenti su misura per questo client. Ma questo è raro e quando possibile cerco di renderlo in una forma più generica e spingerlo verso il basso in modo che altri progetti possano usarli.

Il risultato finale è che ho il livello di personalizzazione di cui avevo bisogno, ho script di installazione personalizzati in modo che quando arrivo al sito del cliente non sembro sto tweekando il sistema tutto il tempo e come un bonus MOLTO significativo aggiunto che ottengo per poter creare test di regressione collegati direttamente alla build. In questo modo ogni volta che ricevo un bug specifico per il cliente posso scrivere un test che affermerà il sistema mentre viene distribuito e quindi può fare TDD anche a quel livello.

quindi in breve:

  1. Sistema fortemente modulare con una struttura di progetto piatta.
  2. Crea un progetto per ciascun profilo di configurazione (cliente, sebbene più di uno possa condividere un profilo)
  3. Assemblare i set di funzionalità richiesti come un prodotto diverso e trattarlo come tale.

Se eseguito correttamente, l'assemblaggio del prodotto deve contenere tutti tranne alcuni file di configurazione.

Dopo averlo usato per un po 'ho finito per creare meta-pacchetti che assemblano i sistemi maggiormente utilizzati o essenziali come unità centrale e usano questo meta-pacchetto per assemblee dei clienti. Dopo alcuni anni ho finito per avere una grande cassetta degli attrezzi che avrei potuto assemblare molto rapidamente per creare soluzioni per i clienti. Attualmente sto esaminando Spring Roo e vedo se non riesco a spingere ulteriormente l'idea sperando che un giorno possa creare una prima bozza del sistema direttamente con il cliente nella nostra prima intervista ... Immagino che potresti chiamarla User Driven Sviluppo ;-).

Spero che questo abbia aiutato


3

I cambiamenti sono probabilmente troppo piccoli per giustificare la suddivisione del modulo in un modulo distinto (fisico) per ogni client, temo problemi con la build, un caos di collegamento.

IMO, non possono essere troppo piccoli. Se possibile, prenderei in considerazione il codice specifico del cliente usando il modello di strategia praticamente ovunque. Ciò ridurrà la quantità di codice che deve essere ramificato e ridurrà l'unione richiesta per mantenere il codice generale sincronizzato per tutti i client. Semplificherà anche i test ... puoi testare il codice generale usando strategie predefinite e testare le classi specifiche del cliente separatamente.

Se i tuoi moduli sono codificati in modo tale che l'utilizzo della politica X1 nel modulo A richieda l'uso della politica X2 nel modulo B, pensa al refactoring in modo tale che X1 e X2 possano essere combinati in una singola classe politica.


1

È possibile utilizzare SCM per mantenere le filiali. Mantenere il ramo principale incontaminato / pulito dal codice personalizzato del client. Fai lo sviluppo principale su questo ramo. Per ogni versione personalizzata dell'applicazione mantenere rami separati. Qualsiasi buon strumento SCM farà davvero bene con l'unione dei rami (mi viene in mente Git). Tutti gli aggiornamenti nel ramo master devono essere uniti nei rami personalizzati, ma il codice specifico del client può rimanere nel proprio ramo.


Tuttavia, se possibile, prova a progettare il sistema in modo modulare e configurabile. Il rovescio della medaglia di questi rami personalizzati può essere che si allontana troppo dal core / master.


1

Se stai scrivendo in semplice C, ecco un modo piuttosto brutto per farlo.

  • Codice comune (ad es. Unità "frangulator.c")

  • codice specifico del cliente, pezzi piccoli e usati solo per ogni cliente.

  • nel codice dell'unità principale, usa #ifdef e #include per fare qualcosa del genere

#ifdef CLIENT = CLIENTA
#include "frangulator_client_a.c"
#finisci se

Utilizzare questo come modello ripetutamente in tutte le unità di codice che richiedono una personalizzazione specifica del client.

Questo è MOLTO brutto e porta ad alcuni altri problemi, ma è anche semplice e puoi confrontare facilmente i file specifici del cliente con l'altro.

Significa anche che tutti i pezzi specifici del cliente sono chiaramente visibili (ciascuno nel proprio file) in ogni momento, e c'è una chiara relazione tra il file di codice principale e la parte del file specifica del cliente.

Se diventi davvero intelligente, puoi impostare i makefile per creare la definizione client corretta, quindi qualcosa del tipo:

fare clienta

costruirà per client_a e "make clientb" farà per client_b e così via.

(e "make" senza target fornito può emettere un avviso o una descrizione dell'uso.)

Ho usato un'idea simile prima, ci vuole un po 'di tempo per installarlo ma può essere molto efficace. Nel mio caso un albero sorgente ha costruito circa 120 prodotti diversi.


0

In git, il modo in cui lo farei è avere un ramo master con tutto il codice comune e rami per ogni client. Ogni volta che viene apportata una modifica al codice principale, è necessario modificare tutte le diramazioni specifiche del client in cima al master, in modo da disporre di una serie di patch mobili per i client che vengono applicate in cima alla linea di base corrente.

Ogni volta che stai apportando una modifica per un client e noti un bug che dovrebbe essere incluso in altri rami, puoi selezionarlo nel master o negli altri rami che richiedono la correzione (anche se di rami client diversi condividono il codice , dovresti probabilmente averli entrambi ramificati da un ramo comune dal master).

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.