如何为多个客户端维护同一软件的不同定制版本


46

我们有多个具有不同需求的客户。尽管我们的软件在某种程度上实现了模块化,但是几乎可以肯定,我们需要在这里和那里为每个客户调整一些模块的业务逻辑。所做的更改可能太微小了,不足以证明为每个客户端将模块拆分为一个单独的(物理)模块是合理的,我担心构建会出现问题,从而造成链接混乱。但是,这些更改太大且太多,无法通过某些配置文件中的开关进行配置,因为这会导致部署期间出现问题,并且可能会带来很多支持问题,尤其是对于修补程序类型的管理员而言。

我想让构建系统创建多个构建,每个客户端一个,其中更改包含在单个物理模块的专用版本中。所以我有一些问题:

您是否建议让构建系统创建多个构建?如何在源代码管理(尤其是svn)中存储不同的自定义项?


4
#ifdef对你有用吗?
哦,

6
预处理程序指令可能很快变得非常复杂,并使代码更难阅读和调试。
猎鹰

1
您应该包括有关实际平台和应用程序类型(台式机/网络)的详细信息,以获得更好的答案。在桌面C ++应用程序中进行自定义与PHP Web应用程序完全不同。
GrandmasterB

2
@Falcon:自从您在2011年选择答案以来,我对此有很多疑问,您能否告诉我们您是否有以建议的方式使用SVN的经验?我的异议是否有根据?
布朗

2
@Doc Brown:分支和合并既繁琐又复杂。那时,我们使用了具有客户端专用插件或“补丁”的插件系统,这些插件或“补丁”改变了行为或配置。您会有一些开销,但可以通过依赖注入进行管理。
猎鹰2015年

Answers:


7

您需要的是功能分支之类的代码组织。在您的特定情况下,这应该称为特定于客户端的中继分支,因为在开发新内容(或解决错误)时,您可能还会使用功能分支。

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

想法是合并具有新功能分支的代码主干。我的意思是所有非特定于客户的功能。

然后,您还拥有特定于客户的分支,您还可以在其中根据需要合并相同功能的分支(如果愿意,可以选择樱桃)。

这些客户端分支的确与功能分支相似,尽管它们不是临时的或短暂的。它们会保留较长的时间,并且主要只合并到其中。应尽可能少地开发特定于客户的功能分支。

特定于客户的分支是中继到中继的并行分支,只要中继本身本身就处于活动状态,甚至在任何地方都不会被合并。

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

7
+1用于功能分支。您也可以为每个客户端使用一个分支。我只想建议一件事:使用分布式VCS(hg,git,bzr)而不是SVN / CVS来完成此操作;)
Herberth Amaral 2011年

43
-1。那不是“功能分支”的目的。它与“功能开发结束时合并的临时分支”的定义相矛盾。
P Shved

14
您仍然必须在源代码管理中维护所有这些增量,并使它们始终保持对齐。短期内这可能有效。从长远来看,这可能会很糟糕。
quick_now

4
-1,这不是命名问题-为不同的客户端使用不同的分支只是代码复制的另一种形式。这是恕我直言,一种反模式,如何不这样做的一个例子。
布朗

7
罗伯特,我想我什至在编辑之前就很理解您的建议,但是我认为这是一种可怕的方法。假设您有N个客户端,每当您向中继添加新的核心功能时,SCM似乎都可以轻松地将新功能传播到N个分支。但是以这种方式使用分支使得避免明确区分客户端特定的修改变得太容易了。结果,您现在有N次机会为中继中的每次更改获得合并冲突。此外,您现在必须运行N个集成测试,而不是一个。
布朗

38

不要对SCM分支执行此操作。使通用代码成为一个产生库或项目框架工件的单独项目。每个客户项目都是一个单独的项目,然后依赖于公共项目作为依赖项。

如果您的通用基础应用程序使用像Spring这样的依赖项注入框架,这是最容易的,那么当您需要自定义功能时,您可以轻松地在每个客户项目中注入对象的不同替换变体。即使您还没有DI框架,添加一个框架并以这种方式进行可能是您最不痛苦的选择。


使事情保持简单的另一种方法是使用继承(和单个项目),而不是使用依赖项注入,方法是将公共代码和自定义项都保留在同一项目中(使用继承,部分类或事件)。通过这种设计,客户端分支将可以正常工作(如所选答案中所述),并且始终可以通过更新公共代码来更新客户端分支。
drizin

如果我不遗漏任何东西,那么您的建议是,如果您有100个客户,您将创建(100 x NumberOfChangedProjects)并使用DI来管理它们吗?如果真是那样,我将绝对避免这种解决方案,因为可维护性将很糟糕
。.–

13

将所有客户端的软件存储在一个分支中。无需区分为不同客户进行的更改。最随意的是,您希望您的软件对于所有客户端来说都是最好的质量,并且核心基础结构的错误修正应该影响每个人而没有不必要的合并开销,这也可能会引入更多的错误。

模块化通用代码,并实现不同文件中不同的代码或使用不同的定义对其进行保护。使您的构建系统具有针对每个客户端的特定目标,每个目标仅使用与一个客户端相关的代码来编译一个版本。例如,如果您的代码是C,则您可能想使用“ #ifdef”或您的语言用于构建配置管理的任何机制来保护不同客户端的功能,并准备一组与客户端已付费功能相对应的定义。

如果您不喜欢ifdefs,请使用“接口和实现”,“ functors”,“目标文件”或您的语言提供的用于在一个地方存储不同内容的任何工具。

如果您将源分发给客户,则最好使您的构建脚本具有特殊的“源分发目标”。调用此类目标后,它将创建软件的特殊版本,将它们复制到单独的文件夹中,以便您可以运输它们,而不编译它们。


8

正如许多人所说的那样:适当地分解代码并根据通用代码进行自定义 -这将大大提高可维护性。无论您是否使用面向对象的语言/系统,这都是可能的(尽管在C语言中比真正面向对象的东西要困难得多)。这正是继承和封装有助于解决的问题类型!


5

非常小心

功能分支是一个选项,但我发现它有点繁重。而且,如果不加以控制,则很容易进行深层修改,从而可能导致应用程序完全瘫痪。理想情况下,您希望尽可能增加自定义项,以使核心代码库尽可能通用和通用。

尽管我不知道它是否适用于您的代码库,但无需进行大量修改和重构,这就是我将如何做。我有一个类似的项目,其基本功能是相同的,但每个客户都需要一组非常特定的功能。我创建了一组模块和容器,然后通过配置(IoC)进行组装。

然后,我为每个客户创建了一个项目,该项目主要包含配置和构建脚本,以为其站点创建完全配置的安装。有时我还会放置一些为此客户定制的组件。但这很少见,只要有可能,我都会尝试将其制作为更通用的形式并将其下推,以便其他项目可以使用它们。

最终结果是我获得了所需的定制级别,获得了定制的安装脚本,因此,当我到达客户站点时,我似乎不会一直在对系统进行周游,并且获得了非常可观的奖励以便能够创建直接挂在构建上的回归测试。这样,无论何时我遇到特定于客户的错误,我都可以编写一个测试,该测试将在部署系统时断言系统,因此即使在该级别也可以进行TDD。

简而言之:

  1. 具有平坦项目结构的高度模块化的系统。
  2. 为每个配置概要文件创建一个项目(客户,尽管可以共享一个概要文件,但可以有多个)
  3. 将所需的功能集组装为其他产品,并按原样对待。

如果做得正确,您的产品程序集应包含除少数几个配置文件之外的所有配置文件。

在使用了一段时间后,我最终创建了将最常用或必不可少的系统组装为核心单元的元软件包,并将此元软件包用于客户装配。几年后,我最终拥有了一个大型工具箱,可以快速组装以创建客户解决方案。我目前正在研究Spring Roo,看看是否能进一步推广这个想法,希望有一天我可以在我们的第一次采访中与客户一起创建系统的初稿...我想您可以将其称为“用户驱动”发展;-)。

希望这对您有所帮助


3

所做的更改可能太微小了,不足以证明为每个客户端将模块拆分为一个单独的(物理)模块是合理的,我担心构建会出现问题,从而造成链接混乱。

海事组织,他们不能太小。如果可能的话,我会在几乎所有地方使用策略模式来分解出特定于客户的代码。这将减少必须分支的代码量,并减少使所有客户端的通用代码保持同步所需的合并。它还将简化测试...您可以使用默认策略来测试常规代码,并分别测试特定于客户端的类。

如果您对模块进行了编码,使得在模块A中使用策略X1要求在模块B中使用策略X2,请考虑进行重构,以便可以将X1和X2合并为一个策略类。


1

您可以使用SCM维护分支。使主分支保持原始/干净,不受客户端自定义代码的影响。在此分支上进行主要开发。对于应用程序的每个自定义版本,维护单独的分支。任何好的SCM工具都非常适合合并分支(想到Git)。master分支中的所有更新都应合并到自定义分支中,但是客户端特定的代码可以保留在其自己的分支中。


但是,如果有可能,请尝试以模块化和可配置的方式设计系统。这些自定义分支的不利之处可能在于,它与核心/主服务器之间的距离太远。


1

如果使用纯C语言编写,这是一种相当丑陋的方法。

  • 通用代码(例如单位“ frangulator.c”)

  • 客户特定的代码,这些代码很小,仅用于每个客户。

  • 在主机代码中,使用#ifdef和#include做类似

#ifdef CLIENT = CLIENTA
#include“ frangulator_client_a.c”
#万一

在需要特定于客户的自定义的所有代码单元中反复使用此模式。

这非常丑陋,并导致其他麻烦,但也很简单,您可以很容易地将客户端专用文件与另一个文件进行交叉比较。

这也意味着所有特定于客户端的片段始终清晰可见(每个片段都在自己的文件中),并且主代码文件与文件中特定于客户端的部分之间存在清晰的关系。

如果您真的很聪明,可以设置makefile来创建正确的客户端定义,例如:

使客户

将为client_a构建,“ make clientb”将为client_b构建,依此类推。

(并且“ make”(没有提供目标)可以发出警告或使用说明。)

我以前也曾使用过类似的想法,虽然设置起来需要一段时间,但它可能非常有效。以我为例,一棵源树构建了大约120种不同的产品。


0

在git中,我要做的方法是拥有一个包含所有通用代码的master分支,并为每个客户端分支。每当对核心代码进行更改时,只需将所有特定于客户的分支重新置于master之上即可,这样您便拥有了一组适用于当前基准之上的针对客户的移动补丁。

每当您为客户端进行更改时,如果发现应该包含在其他分支中的错误,则可以将其樱桃拣选到master或需要修复的其他分支中(尽管不同的client分支正在共享代码) ,您可能应该让它们都从master分支到一个公共分支)。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.