Это одна из наших самых больших проблем, поскольку у нас есть несколько клиентов, которые используют одну и ту же кодовую базу, но имеют различные потребности. Позвольте мне поделиться с вами нашей историей эволюции:
Наша компания начинала с одного клиента, и, как только мы начали привлекать других клиентов, в коде вы увидите такие вещи:
if(clientName == "ABC") {
// do it the way ABC client likes
} else {
// do it the way most clients like.
}
В конце концов мы поняли, что это делает действительно уродливый и неуправляемый код. Если другой клиент хотел, чтобы их клиенты вели себя как ABC в одном месте, а CBA - в другом, мы застряли. Вместо этого мы обратились к файлу .properties с кучей точек конфигурации.
if((bool)configProps.get("LastNameFirst")) {
// output the last name first
} else {
// output the first name first
}
Это было улучшение, но все еще очень неуклюже. «Волшебные струны» изобиловали. Не было никакой реальной организации или документации вокруг различных свойств. Многие свойства зависят от других свойств и ничего не сделают (или даже что-то сломают!), Если не будут использованы в правильных комбинациях. Большая часть (возможно, даже большая часть) нашего времени в некоторых итерациях была потрачена на исправление ошибок, возникших из-за того, что мы «исправили» что-то для одного клиента, что нарушило конфигурацию другого клиента. Когда у нас появился новый клиент, мы просто начинали с файла свойств другого клиента, который имел конфигурацию, «наиболее похожую» на ту, которую хотел этот клиент, и затем пытались настраивать объекты до тех пор, пока они не будут выглядеть правильно.
Мы пытались использовать различные методы, чтобы сделать эти точки конфигурации менее громоздкими, но добились лишь умеренного прогресса:
if(userDisplayConfigBean.showLastNameFirst())) {
// output the last name first
} else {
// output the first name first
}
Было несколько проектов, чтобы получить контроль над этими конфигурациями. Один из них заключался в написании механизма представления на основе XML, чтобы мы могли лучше настроить отображение для каждого клиента.
<client name="ABC">
<field name="last_name" />
<field name="first_name" />
</client>
Другой проект включал в себя написание системы управления конфигурацией для консолидации нашего кода конфигурации, обеспечения того, чтобы каждая точка конфигурации была хорошо документирована, позволить суперпользователям изменять значения конфигурации во время выполнения и позволить коду проверять каждое изменение, чтобы избежать получения недопустимая комбинация значений конфигурации.
Эти различные изменения определенно облегчили жизнь каждому новому клиенту, но большинство из них не смогли устранить корень наших проблем. Больше всего нас порадовало изменение, когда мы перестали рассматривать наш продукт как серию исправлений, заставляющих что-то работать для еще одного клиента, и мы начали рассматривать наш продукт как «продукт». Когда клиент запросил новую функцию, мы начали тщательно рассматривать вопросы, такие как:
- Сколько других клиентов смогут использовать эту функцию, сейчас или в будущем?
- Может ли он быть реализован таким образом, чтобы не сделать наш код менее управляемым?
- Можем ли мы реализовать другую функцию, отличную от той, о которой они просят, которая по-прежнему отвечала бы их потребностям и в то же время подходила бы для повторного использования другими клиентами?
При реализации функции мы бы взяли длинную перспективу. Вместо создания нового поля базы данных, которое будет использоваться только одним клиентом, мы можем создать совершенно новую таблицу, которая позволит любому клиенту определять любое количество пользовательских полей. Это потребовало бы дополнительной работы заранее, но мы могли бы позволить каждому клиенту настраивать свой собственный продукт с большой степенью гибкости, не требуя от программиста изменения какого-либо кода.
Тем не менее, иногда есть определенные настройки, которые вы действительно не сможете выполнить, не вкладывая огромных усилий в сложные механизмы правил и так далее. Когда вам просто нужно заставить его работать в одну сторону для одного клиента, а в другом - для другого клиента, я обнаружил, что вам лучше всего программировать для интерфейсов и использовать внедрение зависимостей . Если вы следуете принципам «SOLID», чтобы убедиться, что ваш код написан модульно с хорошим «разделением интересов» и т. Д., Изменить реализацию конкретной части кода для конкретного клиента не так уж и болезненно:
public FirstLastNameGenerator : INameDisplayGenerator
{
IPersonRepository _personRepository;
public FirstLastNameGenerator(IPersonRepository personRepository)
{
_personRepository = personRepository;
}
public string GenerateDisplayNameForPerson(int personId)
{
Person person = _personRepository.GetById(personId);
return person.FirstName + " " + person.LastName;
}
}
public AbcModule : NinjectModule
{
public override void Load()
{
Rebind<INameDisplayGenerator>().To<FirstLastNameGenerator>();
}
}
Этот подход усиливается другими техниками, которые я упоминал ранее.Например, я не написал AbcNameGenerator
, потому что, возможно, другие клиенты захотят подобное поведение в своих программах.Но используя этот подход, вы можете довольно легко определить модули, которые переопределяют настройки по умолчанию для конкретных клиентов, очень гибким и расширяемым способом.
Поскольку подобные системы изначально хрупки, важно также сосредоточиться на нихАвтоматическое тестирование: модульные тесты для отдельных классов, интеграционные тесты, чтобы убедиться (например), что все ваши инъекционные привязки работают правильно, и системные тесты, чтобы убедиться, что все работает вместе без регресса.
PS:Я использую «мы» на протяжении всей этой истории, хотя я фактически не работал в компании большую часть ее истории.
PPS: извините за смесь C # и Java.