Potresti mantenere filiali separate per ogni cliente e unire le modifiche dal tuo trunk, proprio come dbb sta dicendo. Tuttavia, esiste un buon modo per farlo e un modo sbagliato in cui tutto ciò diventa complicato e rapidamente fuori controllo se esistono molte funzionalità simili condivise tra più clienti.
Il modo più semplice per gestire le varianti, ciascuna per ogni cliente , è attraverso ramificazione per astrazione anziché. In poche parole: crea la gestione della configurazione nell'applicazione / sistema che attiva e disattiva la funzionalità richiesta dai clienti. In questo modo non devi preoccuparti di unire il controllo del codice sorgente. Può essere complesso come un sistema plug-in, ma puoi farlo semplicemente avendo una funzione nel tuo programma che passa attraverso un file di configurazione e abilita la funzionalità di cui hai bisogno.
Come costruirlo dipende da quale ambiente lo stai sviluppando. Java ha una classe Proprietà che può farlo per te. In C # puoi cercare qui per maggiori informazioni. Sebbene sia possibile codificare in modo rigido queste proprietà con una switch-case o una mappa hash e gestire le differenze di questa classe di configurazione tramite il controllo del codice sorgente, nel caso in cui non si desideri che i clienti riconfigurino facilmente il software.
Esempi in Java:
Leggi un file di configurazione con java.util.Properties
Fonte:
public class ReadConfig {
private static final CONFIG_FILEPATH = "config.txt";
private Properties config;
public ReadConfig() {
config = new Properties();
config.load(CONFIG_FILEPATH);
}
public boolean isEnabled(String functionality) {
String s = config.getProperty(functionality);
return Boolean.getBoolean(s);
}
}
Contenuto di config.txt
(che può essere gestito per ogni cliente):
Eat_Sandwich: true
Make_Sandwich: false
Esempio di utilizzo:
public static void main(String[] args) {
ReadConfig myConfig = new ReadConfig();
if(myConfig.isEnabled("Eat_Sandwich")) {
System.out.println("I can eat the sandwich");
}
if(myConfig.isEnabled("Make_Sandwich")) {
System.out.println("I can make the sandwich");
}
}
// Will output: I can eat the sandwich
Codice hard della configurazione
Origine (che può essere gestita per ogni cliente):
public class HardCodedConfig {
private HashMap<String, Boolean> config;
public HardCodedConfig() {
config = new HashMap<String, Boolean>();
// Add functionalities here:
register("Make_Sandwich", true);
register("Eat_Sandwich", false);
}
private void register(String functionality, boolean enabled) {
config.put(functionality, enabled);
}
private boolean isEnabled(String functionality) {
return config.get(functionality);
}
}
Utilizzo:
public static void main(String[] args) {
HardCodedConfig myConfig = new HardCodedConfig();
if(myConfig.isEnabled("Eat_Sandwich")) {
System.out.println("I can eat the sandwich");
}
if(myConfig.isEnabled("Make_Sandwich")) {
System.out.println("I can make the sandwich");
}
}
// Will output: I can make the sandwich
EDIT:
Dato che l'OP vuole qualcosa di un po 'più avanzato rispetto all'attivazione delle funzionalità, è possibile farlo attraverso il caricamento delle librerie. Ma dovresti fare attenzione ad aggiungere librerie esterne (come dll e jar) nel repository di origine, dato che ti farebbe un lavoro extra di pulizia della casa da fare. Quindi se hai file che possono essere generati tramite script di compilazione (usi un build server , giusto?), Allora non includile nel controllo del codice sorgente.
Invece, tieni traccia delle diverse DLL che devi compilare e utilizzare tramite gli script di configurazione configurabili, in modo da poter creare l'applicazione da zero insieme ai programmi di installazione per ciascun cliente.
Considera anche l'utilizzo di modelli come il Pattern di strategia , per separare diverse implementazioni della stessa funzionalità. Nel tuo esempio, calcolare gli sconti, può essere fatto creando un'interfaccia e / o una classe astratta. Ecco una semplice implementazione in C #:
public interface IDiscountStrategy {
/**
* Calculates the discount from amount
*/
public decimal calculateDiscount(decimal amount);
}
public class DefaultDiscountStrategy : IDiscountStrategy {
public decimal _percentage;
public DefaultDiscountStrategy(decimal percentage) {
_percentage = percentage;
}
public decimal calculateDiscount(decimal amount) {
return amount * _percentage;
}
}
L'utilizzo è chiamare il metodo calculateDiscount
su DiscountStrategy che è stato caricato.
Nella tua libreria dll separata per un cliente specifico hai il seguente che viene usato invece quando l'applicazione carica:
public class WeirdCustomerDiscountStrategy : IDiscountStrategy {
public decimal calculateDiscount(decimal amount) {
DayOfWeek dayOfWeek = DateTime.Now.DayOfWeek;
if (dayOfWeek == DayOfWeek.Thursday)
return amount * 0.05;
else
return 0;
}
}
Nella tua applicazione comune caricherete le diverse strategie in questo modo:
public IDiscountStrategy getDiscountStrategy() {
Assembly assembly;
try {
assembly = Assembly.LoadFrom("CustomerXYZ.dll");
} catch (FileNotFoundException e) {
assembly = null;
}
IDiscountStrategy discounter;
if (assembly == null) {
discounter = new DefaultDiscountStrategy(0.10);
} else {
discounter = (IDiscountStrategy)
assembly.CreateInstance("WeirdCustomerDiscountStrategy");
}
return discounter;
}
Diventa piuttosto peloso quando l'applicazione cresce. Quindi potresti prendere in considerazione l'idea di utilizzare un framework IoC per farlo, ad esempio StructureMap o autofaq se utilizzi .NET o Spring se stanno usando Java. Ecco un esempio di "plug-in scanner" in StructureMap .