Dobbiamo distinguere due tipi di versioni :
- VCS come Git memorizza la cronologia di sviluppo del software.
- Queste versioni non hanno nulla a che fare con diverse edizioni o varianti del software.
Cercare di confondere entrambi è una ricetta per il dolore.
Specialmente con Git, un commit rappresenta uno stato specifico del software e non solo un gruppo di modifiche . Pertanto, se si dispone di un ramo base e di un ramo per una variante software, l'unione tra di essi implica sempre la riconciliazione dello stato corrente di entrambe le varianti. Questo è molto più lavoro che ri-applicare le stesse modifiche (che è possibile in Git con git cherry-pick
, ma che crea un nuovo commit completamente non correlato senza la cronologia del commit originale).
Per Git, una fork è effettivamente la stessa di una branch. La creazione di un repository biforcato separato non presenta particolari vantaggi tecnici.
Se le filiali Git non sono adatte a mantenere più varianti del software, a cosa servono? I rami sono effettivamente tag mobili che puntano a qualche commit. Ciò li rende adatti a denotare "la X più recente", dove X potrebbe essere "l'ultima versione stabile" (ad esempio per il ramo principale) o "la versione di sviluppo corrente" (se si dispone di un ramo di sviluppo). Inoltre, le filiali sono adatte per thread di sviluppo simultanei . Per esempio. il lavoro può procedere in modo indipendente su più rami di funzionalità fino a quando non vengono uniti.
Come possiamo mantenere più varianti / edizioni se non possiamo usare le filiali? Il trucco è che dovresti avere solo una variante del tuo codice sorgente , e invece generare edizioni attraverso un processo di sviluppo o tramite configurazione .
Ad esempio, potresti voler creare un'app con marchi diversi. Quindi crea più pacchetti di risorse per ciascun marchio. Quando crei la tua app, seleziona solo il pacchetto di risorse per la variante che stai attualmente sviluppando. Un "pacchetto di risorse" potrebbe essere semplice come due directory customer-1/
e customer-2/
che contengono ciascuna un'immagine banner.png
.
Se vuoi che le varianti si comportino diversamente, le cose diventano un po 'più complicate. Ora possiamo utilizzare attiva / disattiva e / o iniezione di dipendenza per configurare il comportamento del software. Questo può accadere in fase di costruzione o di esecuzione. Questi commutatori potrebbero essere condizionali o ifdef, ma l'utilizzo dell'iniezione di dipendenza con il modello di strategia può mantenere il codice semplice.
es. se si dispone di un'edizione standard e di un'edizione pro e alcune operazioni sono supportate solo nell'edizione professionale, il codice che esegue questa operazione si comporterà in modo diverso a seconda della configurazione: l'edizione professionale eseguirà semplicemente l'operazione, mentre l'edizione standard potrebbe nascondere l'interfaccia utente per tale operazione o sollevare un'eccezione o richiedere all'utente di eseguire l'aggiornamento.
Mantenendo una singola base di codice per tutte le varianti, il codice diventa un po 'più complesso perché presenta più punti di configurazione e più possibili percorsi di codice.
Tuttavia, ottieni molta più flessibilità e agilità per il tuo sviluppo:
- È possibile apportare grandi modifiche senza doverle unire noiosamente in altre varianti.
- In particolare, devi solo correggere i bug una volta per tutte le tue edizioni.
- Il comportamento comune deve essere testato solo una volta.
- Dato che hai una base di codice, vedrai le incompatibilità tra varie funzioni in precedenza e puoi correggerle, prima che sia troppo tardi.
L'ultima volta che ho lavorato su un repository "un ramo per cliente", questo ha comportato notevoli difficoltà. Molto lavoro che avrebbe avvantaggiato tutti i settori (come processi di build, test e correzioni di errori migliorati) ha dovuto essere efficacemente reimplementato per ciascun ramo perché erano leggermente divergenti. Man mano che le fusioni diventavano sempre più scomode, il codice veniva copiato e incollato tra i rami. Un ramo ha visto un refactoring estremamente utile che ha interessato tutti i moduli. Dopo la fusione di altri rami, il lavoro di refactoring doveva essere efficacemente ripetuto per tenere conto di piccole modifiche. Stimo che abbiamo sprecato circa il 10% - 30% del nostro sforzo attraverso questa configurazione, che in retrospettiva era molto più del tempo necessario per aggiungere funzionalità al nostro processo di compilazione - che alla fine dovevamo fare comunque.