Если вы продаете свой программный продукт, то рано или поздно один из клиентов попросит вас приделать к вашему продукту какую-нибудь эдакую штучку в вашем продукте, которая лично ему до зарезу нужна. И, весьма вероятно, вы согласитесь эту штучку для него сделать. А вполне возможно, что ваш продукт изначально будет ориентирован именно на таких клиентов, которые придут к вам благодаря базовому функционалу вашего продукта, но существенную часть прибыли принесут вам за счет кастомизации, то есть доработок вашего продукта под свои нужды.

Оставим пока в стороне вопросы договоров, платежей и прочие организационные тонкости и подумаем о том, каким образом организовать работу с кастомизированным кодом конкретных клиентов. В большинстве случаев клиент ведь захочет не только получить доработанную под него версию продукта, но и поддерживать ее в актуальном состоянии, получая исправления и дополнения, сделанные в вашем исходном продукте. Мало того, большую часть разработок, сделанных для конкретных клиентов, по различным причинам нельзя будет включить в основной код проекта.

Есть несколько возможных подходов к организации кода при работе с клиентами, из которых я в своей практике использую два – разбивка функционала по модулям и клонирование кода.

Модульная структура проекта

Если у вас есть хотя бы мало-мальский опыт программирования, то вам не надо объяснять что такое разбивка проекта на модули. Часть функционала системы выносится за пределы основного проекта – вот собственно и все. В языке Ruby такими модулями могут быть gem-ы, в PHP это PEAR-пакеты, ну и так далее. Суть одна – кусок функционала системы, который опционально устанавливается и настраивается для клиента.

О процессе установки и настройки модуля стоит отдельно сказать, что вариантов может быть масса – от компиляции отдельной версии с нужным модулем, добавленным руками, до разработки API для интеграции сторонних модулей.

Первый вариант приемлем тогда, когда установка модуля для конкретного клиента случается редко, а количество доступных модулей невелико. Собственно, именно таким образом мы интегрируем модули для наших клиентов – устанавливаем модуль используя стандартные средства языка (в среде ruby это ruby gems), а затем подключаем его, вставляя соответствующие инструкции в нужных местах в коде.

Если же количество подключаемых модулей велико, модули разнообразны или даже пишутся сторонними разработчиками, то имеет смысл разработать API для подключения модулей. Эта задача специфична для каждой конкретной системы, поэтому могу дать лишь одну конкретную рекомендацию: исходите из конкретных потребностей. Не загадывайте наперед, не выдумывайте систему на пустом месте. Напишите десяток плагинов к вашей системе, используя топорный подход, описанный в предыдущем параграфе – и API вырисуется сам собой.

Клонирование кода

Несмотря на кажущуюся простоту модульного подхода, он решает далеко не все задачи. Давайте рассмотрим пример: мы разработали новый веб-браузер для корпоративных клиентов и теперь продвигаем его на рынок. К нам приходит клиент, который хочет, чтобы в нашем браузере на всех страницах всех сайтов отображалось лого компании, плюс еще 5-10 изменений по мелочи. Клиент готов за это заплатить существенные деньги.

Мы можем разработать набор модулей для решения задачи клиента, но по сути сделать поправки напрямую в коде было бы в десять раз быстрее, чем писать модули и интегрировать их. Делать поправки в основном коде, который мы поставляем всем остальным клиентам, нельзя по каким-либо причинам. В этом случае вполне логично сделать копию кода, однако исправленный код придется потом поддерживать, накатывать всевозможные обновления из последующих версий. В зависимости от того, какие инструменты мы используем в работе, сложность задачи с копированием кода и дальнейшей поддержкой может варьироваться от “это невозможно” до “это элементарно”.

На мой взгляд, наиболее подходящим инструментом для решения этой задачи является система контроля версий Git. Стандартный функционал Git позволяет создавать кастомные версии кода и обновлять их практически без усилий – за счет копирования изменений из одного репозитория в другой, используя стандартные команды. Давайте посмотрим, как можно организовать работу с кодом для кастомизации кода под задачи вышеозначенного клиента.

Допустим, наш основной код хранится в репозитории git@myserver:myrepo.git и мы хотим создать копию кода для клиента, которая будет лежать в отдельном репозитории git@myselver:client_1.git. Для начала, мы создаем локальную копию кода для клиента:

Bash Code
1
$ git clone git@myserver:myrepo client_1

Отлично, копия есть. Однако, эта копия привязана к нашему исходному репозиторию:

Bash Code
1
2
3
4
5
6
7
$ cd client_1
$ git remote show origin

  * remote origin
  Fetch URL: git@myserver:myrepo.git
  Push  URL: git@myserver:myrepo.git
  ...

Нам нужно отвязать нашу копию от исходного репозитория и привязать к новому:

Bash Code
1
2
3
$ git remote rm origin
$ git remote add origin git@myselver:client_1.git
$ git push -u origin master

Теперь в новом репозитории у нас содержится полная копия исходного репозитория. В новом репозитории мы можем делать все необходимые нам изменения для клиента. Допустим, изменения сделаны, клиент счастлив, заплатил деньги и получил свою кастомизированную копию программы. Но через неделю в вашем продукте обнаружена уязвимость, которая затрагивает все версии браузера, включая кастомную версию для клиента. Вы сделали изменения в основном репозитории и теперь их нужно каким-то образом импортировать в копию, созданную для клиента. Давайте сделаем это:

Bash Code
1
2
$ cd client_1
$ git pull git@myserver:myrepo.git master

Так как большинство коммитов в клиентском репозитории идентичны коммитам в исходном репозитории, то Git импортирует исключительно те коммиты, которых нет в клиентском репозитории. Все конфликты между коммитами (если таковые возникнут) можно будет разрешить используя стандартные механизмы Git. Останется только залить изменения в репозиторий:

Bash Code
1
$ git push

Для упрощения импорта коммитов можно прописать основной репозиторий в remote-ссылки клиентского репозитория:

Bash Code
1
$ git remote add main_repo git@myserver:myrepo.git

И использовать ссылку вместо адреса для импорта:

Bash Code
1
$ git pull main_repo master

Кроме того, импортировать можно и из различных веток или тегов:

Bash Code
1
2
$ git pull main_repo tags/stable
$ git pull main_repo experimental_feature

Вполне вероятно, что этот же подход можно использовать и с другими распределенными системами контроля версий, такими как Mercurial. Если я не прав – поправьте меня.

Итого

Вышеописанные подходы к кастомизации основного кода и поддержке клиентских копий мы используем в нашей компании уже несколько лет подряд и еще не было ни единого случая, когда они бы нас подвели. Вкратце, все вышесказанное можно резюмировать в двух советах:

  • Используйте модули тогда, когда функционал повторяется от клиента к клиенту, но его нельзя включить в основной код
  • Используйте клонирование кода тогда, когда изменения разумнее сделать напрямую в коде проекта, нежели писать для этого отдельный модуль