您如何组织高度定制的软件?


28

我正在从事一个大型软件项目,该项目针对世界各地的各种客户进行了高度定制。这意味着我们可能有80%的代码在各个客户之间是通用的,但是还有很多代码必须从一个客户转换到另一个客户。过去,我们是在单独的存储库(SVN)中进行开发的,而当一个新项目开始时(我们的客户很少,但客户众多),我们根据过去的项目中最能满足我们需求的代码创建了另一个存储库。过去一直有效,但是我们遇到了几个问题:

  • 在一个存储库中修复的错误不会在其他存储库中修补。这可能是组织问题,但我发现很难在5个不同的存储库中修复和修补错误,请记住,维护该存储库的团队可能位于世界的另一部分,并且我们没有测试环境,既不知道他们的时间表,也不知道他们有什么要求(一个国家的“错误”可能是另一个国家的“功能”)。
  • 为一个项目进行的功能和改进可能对另一项目也可能有用,或者丢失了这些功能或进行了改进,或者如果将这些功能和改进用在另一个项目中,则经常导致将它们从一个代码库合并到另一个代码库的麻烦(因为两个分支可能已经独立开发了一年) )。
  • 如果必须在分支之间合并所有这些更改,则在一个开发分支中进行的重构和代码改进可能会丢失或造成的危害大于弊。

我们现在正在讨论如何解决这些问题,到目前为止,我们提出了以下解决方案:

  1. 将开发保持在单独的分支中,但是要通过建立一个中央存储库来更好地组织它,其中将常规错误修复程序合并到其中,并使所有项目定期(例如每天)将来自该中央存储库的更改合并到自己的更改中。这需要庞大的纪律和分支之间的合并工作。因此,我不相信这会奏效,并且我们可以保持这一纪律,尤其是在时间压力加大的情况下。

  2. 放弃单独的开发分支,并建立一个中央代码存储库,我们所有的代码都将存在于此,并通过具有可插拔模块和配置选项进行自定义。我们已经在使用Dependency Injection容器来解析代码中的依赖关系,并且我们在大多数代码中都遵循MVVM模式,以将业务逻辑与UI完全分开。

第二种方法似乎更优雅,但是这种方法有很多未解决的问题。例如:如何处理模型/数据库中的更改/添加。我们将.NET与Entity Framework结合使用来拥有强类型化的实体。我看不到如何处理一个客户所需的属性,而又另一个客户无用的属性而又不会弄乱我们的数据模型。我们正在考虑通过使用卫星表(有一个单独的表,其中特定实体的额外列与原始实体1:1映射在一起)解决数据库中的问题,但这仅是数据库。您如何在代码中处理此问题?我们的数据模型位于一个中央库中,使用该方法我们将无法为每个客户扩展。

我敢肯定,我们不是唯一一个在这个问题上苦苦挣扎的团队,我很震惊地发现关于该主题的资料很少。

所以我的问题如下:

  1. 您对高度定制的软件有什么经验,选择了哪种方法以及它如何为您工作?
  2. 您推荐哪种方法,为什么?有没有更好的方法?
  3. 您是否可以推荐有关该主题的好书或文章?
  4. 您对我们的技术环境(.NET,实体框架,WPF,DI)有具体建议吗?

编辑:

感谢所有的建议。大多数构想与我们团队中已有的构想相符,但了解您对它们的经验以及更好地实施它们的提示确实很有帮助。

我仍然不确定我们会走哪条路,也没有做出决定(单独做出),但是我会在团队中传递这一点,并且我相信这会有所帮助。

目前,男高音似乎是一个使用各种客户特定模块的单一存储库。我不确定我们的体系结构是否达到这个目标,或者我们需要投入多少资金才能使其适应要求,因此有些事情可能会在单独的存储库中保留一段时间,但是我认为这是唯一可行的长期解决方案。

因此,再次感谢您的所有回复!


考虑将数据库表视为代码。

从某种意义上说,我们已经在这样做,因为我们的Subversion存储库中有数据库脚本,但它并不能真正解决上述问题。我们不想在我们的数据库模型中有键-值样式表,因为它们有很多问题。那么,如何在允许单个客户添加模型的同时仍为所有客户维护共享代码存储库呢?
aKzenT 2012年

Answers:


10

听起来根本的问题不仅在于代码存储库维护,还在于缺乏合适的架构

  1. 系统的核心/本质是什么,将始终被所有系统共享?
  2. 每个客户需要哪些增强/偏离?

框架或标准库包含前者,而后者将作为附件(插件,子类,DI,无论对代码结构有意义的方式)实现。

管理分支机构和分布式开发的源代码控制系统也可能会有所帮助;我是Mercurial的粉丝,其他人则喜欢Git。框架将是主要分支,例如,每个定制系统将是子分支。

用于实现系统的特定技术(.NET,WPF等)在很大程度上并不重要。

做到这一点并不容易,但是对于长期生存至关重要。当然,您等待的时间越长,您必须处理的技术债务就越大。

您可能会发现本书《软件体系结构:组织原则和模式》很有用。

祝好运!


3
对。但是,建筑通常更多是心理上的事情,而不是技术上的事情。如果您完全专注于产品,那么最终将导致工作分解,其中组件仅对该产品有用。另一方面,如果您专注于构建一组将变得更加有用的库,那么您将构建一组可以在更广泛的情况下部署的功能。当然,关键是要针对您的特定情况在这两个极端之间找到正确的平衡。
威廉·佩恩

我认为我们的许多分支机构之间都有很大的共享部分(> 90%),但是最后10%的分支总是在不同的地方,所以除了很少的实用程序库之外,我几乎无法想象到某些客户特定更改的组件。不包含任何业务逻辑。
aKzenT 2012年

@aKzenT:嗯...不要想像,要测量。检查代码,查看发生定制的所有位置,列出修改的组件,记录每个组件的修改频率,并考虑实际完成的修改的类型和模式。他们是装饰性的还是算法性的?他们在添加或更改基本功能吗?每种更改的原因是什么?同样,这是艰苦的工作,您可能不喜欢发现的内容。
史蒂文·劳

1
我接受了这个答案,因为在做出更多关于我们的体系结构的思考之前,我们无法做出此决定,确定共同的部分或应该共同的部分,然后查看我们是否可以使用单个存储库或是否有必要进行分叉(对于至少此刻)。这个答案反映了最好的IMO。不过,感谢所有其他帖子!
aKzenT

11

我工作过的一家公司遇到了同样的问题,解决该问题的方法是:为所有新项目创建一个通用框架;这包括每个项目中必须相同的所有内容。例如,表单生成工具,导出到Excel,进行日志记录。已尽力确保仅对通用框架进行改进(当新项目需要新功能时),而不要对其进行分叉。

基于该框架,特定于客户的代码在单独的存储库中维护。在有用或必要时,在项目之间复制粘贴错误修正和改进(问题中描述了所有警告)。不过,在全球范围内将有用的改进纳入通用框架。

将所有内容都存放在对所有客户来说都通用的代码库中有一些优点,但是,另一方面,当有无数个ifs使程序对于每个客户的行为不同时,读取代码将变得很困难。

编辑:一个使这更容易理解的轶事:

该公司的业务领域是仓库管理,仓库管理系统的一项任务是为进货找到免费的存储位置。听起来很简单,但是在实践中,必须遵守许多约束和策略。

在某个时间点,管理层要求程序员制作一个灵活的,可参数化的模块来查找存储位置,该模块实现了几种不同的策略,并且应该在所有后续项目中使用。崇高的努力导致了一个复杂的模块,很难理解和维护。在下一个项目中,项目负责人无法弄清楚如何使其在该仓库中工作,并且该模块的开发人员走了,所以他最终忽略了该模块,并为该任务编写了自定义算法。

几年后,最初使用此模块的仓库的布局发生了变化,并且具有所有灵活性的模块都不符合新的要求。所以我在那里也用自定义算法替换了它。

我知道LOC并不是一个很好的衡量标准,但是无论如何:“灵活”模块的大小约为3000 LOC(PL / SQL),而针对同一任务的自定义模块则需要100..250 LOC。因此,尝试灵活地极大地增加了代码库的大小,而没有获得我们所希望的可重用性。


感谢您的反馈意见。这是描述的第一个解决方案的扩展,我们也对此进行了考虑。但是,由于我们的大多数库都使用某些核心实体,并且通常将这些核心实体扩展给一个或另一个客户,所以我认为我们只能在此核心存储库中放置很少的库。例如,我们定义了一个“客户”实体并将ORM映射到一个库中,几乎所有其他库和程序都使用该库。但是每个客户都有一些额外的字段需要添加到“客户”中,因此我们需要派生该库,因此所有库都取决于它。
2012年

3
我们避免无数“ if”的想法是广泛使用依赖项注入并将完整的模块交换给不同的客户。不确定如何解决。这种方法如何为您工作?
aKzenT 2012年

+1,这基本上与我的经验非常吻合。如果要使用“针对不同客户的if”方法,请注意2点:1.不要执行if(customer1)...,而要执行if(configurationOption1)...,并具有针对每个客户的配置选项。2.尽量不要这样做!也许有1%的时间比拥有特定于配置的模块更好(更易于理解/易于维护)。
vaughandroid 2012年

@Baqueta:只是要澄清一下:您建议使用按客户模块而不是配置选项(ifs),对吗?我喜欢您的想法来区分功能而不是客户。因此,两者的结合将是具有由配置选项控制的各种“功能模块”。然后,仅将客户表示为一组独立的功能模块。我非常喜欢这种方法,但是我不确定如何设计这种方法。DI解决了模块加载和交换的问题,但是您如何管理客户之间的不同数据模型?
aKzenT 2012年

是的,按功能模块,然后按客户配置/功能选择。DI将是理想的。不幸的是,尽管我从未将您对每个客户的大量自定义要求与单个数据库相结合,所以我不确定在那里我能提供的帮助很大……
vaughandroid 2012年

5

我从事的项目之一支持大量产品发布中的多个平台(超过5个)。您描述的许多挑战都是我们面临的事情,尽管方式略有不同。我们有一个专有的数据库,所以在那个领域我们没有相同类型的问题。

我们的结构与您的结构相似,但是我们的代码只有一个存储库。特定于平台的代码进入代码树中自己的项目文件夹。普通代码根据其所属的层位于树中。

我们基于要构建的平台进行了条件编译。维护起来很麻烦,但是只有在平台特定层添加了新模块时才需要这样做。

将所有代码都放在一个存储库中,使我们可以轻松地同时在多个平台和版本之间进行错误修复。我们为所有平台提供了一个自动构建环境,以作为新代码破坏假定无关平台的支持。

我们试图阻止它,但在某些情况下,平台需要基于平台特定的错误进行修复,而该错误在其他通用代码中已经存在。如果我们可以有条件地覆盖编译而又不会使模块看起来很丑,那么我们将首先这样做。如果没有,我们将模块移出公共区域,并将其推入特定平台。

对于数据库,我们有一些表具有特定于平台的列/修改。我们将确保该表的每个平台版本都满足基本功能级别,以便通用代码可以引用该表而不必担心平台依赖性。平台特定的查询/操作已推入平台项目层。

因此,回答您的问题:

  1. 很多,那是我合作过的最好的团队之一。当时的代码库约为1M loc。我没有选择方法,但是效果很好。即使事后看来,我也没有找到更好的处理方式。
  2. 我建议您在答案中提到的细微差别所建议的第二种方法。
  3. 我没有想到的书,但是我将首先研究多平台开发。
  4. 学会一些强有力的治理。这是确保遵循您的编码标准的关键。遵循那些标准是使事物可管理和可维护的唯一方法。我们有慷慨激昂的恳求来打破我们遵循的模式,但是这些呼吁都没有动摇整个高级开发团队。

感谢您分享您的体验。只是为了更好地理解:您的案例中如何定义平台。Windows,Linux,X86 / x64?还是与不同的客户/环境更相关的东西?您在4)中提出了一个好观点,我认为这是我们遇到的问题之一。我们的团队由许多非常聪明和合格的人组成,但是每个人对做事的想法略有不同。如果没有明确负责架构的人员,很难就一个共同的策略达成共识,并且您可能会在讨论中失去自己,而无需进行任何更改。
aKzenT 2012年

@aKzenT-是的,我的意思是物理硬件和操作系统作为平台。我们拥有一个大型功能集,其中一些功能可以通过许可模块进行选择。并且我们支持各种硬件。与此相关的是,我们有一个公共层目录,该目录中有一堆设备,这些设备在该层中都有自己的代码树目录。因此,我们的情况确实与您所处的位置并没有那么遥远。我们的高级开发人员之间可能会进行激烈的辩论,但是一旦做出集体决定,每个人都同意排队。

4

我在养老金管理应用程序上工作了很多年,该应用程序也有类似的问题。养老金计划在公司之间有很大的不同,并且需要高度专业的知识来实施计算逻辑和报告以及非常不同的数据设计。我只能简要介绍一下架构的一部分,但也许可以提供足够的想法。

我们有2个独立的团队:一个核心开发团队,负责核心系统代码(这将是您上面的80%的共享代码);以及一个实施团队,其在养老金系统方面具有领域专业知识,并负责学习客户客户的需求,编码脚本和报告。

我们所有的表都由Xml定义(在实体框架经过时间测试和通用之前)。实施团队将在Xml中设计所有表,并且可以提示核心应用程序在Xml中生成所有表。每个客户端还具有关联的VB脚本文件,Crystal Reports,Word文档等。(Xml中还内置了一个继承模型,以允许重用其他实现)。

核心应用程序(一个用于所有客户端的应用程序)将在请求该客户端时缓存所有特定于客户端的内容,并生成一个公共数据对象(类似于远程ADO记录集),该对象可以序列化并传递周围。

这种数据模型不如实体/域对象那么光滑,但是它具有高度的灵活性,通用性,并且可以由一组核心代码进行处理。也许在您的情况下,您可以仅使用公共字段定义基本实体对象,并为自定义字段添加一个附加的Dictionary(向您的实体对象添加某种类型的数据描述符集,以便它具有自定义字段的元数据。 )

对于核心系统代码和实现代码,我们有单独的源存储库。

除了一些非常标准的通用计算模块之外,我们的核心系统实际上几乎没有业务逻辑。核心系统的功能是:屏幕生成器,脚本运行器,报告生成器,数据访问和传输层。

分割核心逻辑和定制逻辑是一个艰巨的挑战。但是,我们总是觉得最好让一个核心系统运行多个客户端,而不是为每个客户端运行该系统的多个副本。


感谢您的反馈。我喜欢在词典中添加其他字段的想法。这将使我们能够对实体进行单一定义,并将所有特定于客户的内容放入字典中。我不确定是否有一种好的方法可以使其与我们的ORM包装器(实体框架)一起使用。而且我也不确定拥有一个全局共享的数据模型而不是为每个功能/模块都有一个模型是否真的是一个好主意。
aKzenT 2012年

2

我在一个较小的系统(20 kloc)上工作,发现DI和配置都是管理客户端之间差异的好方法,但不足以避免分叉该系统。数据库分为具有固定模式的应用程序特定部分和通过自定义XML配置文档定义的客户端相关部分。

我们保留了一个分支机构,该分支机构被配置为可以交付,但为虚构的客户进行了品牌和配置。错误修复是该项目的主线,并且仅在此处进行核心功能的新开发。实际客户的发行版是分支的分支,存储在其自己的存储库中。我们通过手动编写的版本号跟踪代码的重大更改,并使用提交号跟踪错误修复。


您是区分客户之间相同的核心库还是分叉完整的树?您是否定期从主线合并到各个分支?如果是的话,每日合并例行程序要花费多少时间,以及如何避免在时间压力开始时此程序崩溃(就像它总是在项目中的某个时刻一样)?抱歉,这么多问题:p
aKzenT 2012年

我们分叉整棵树,主要是因为客户端请求先于构建系统完整性。使用Mercurial和其他工具手动进行从主系统的合并,并且通常仅限于严重的错误或大型功能更新。由于合并的成本,而且由于许多客户端托管自己的系统,因此我们试图将更新保持在很少的大块中,并且我们不希望在不提供价值的情况下增加安装成本。
Dan Monego'8

我很好奇:IIRC mercurial是类似于git的DVCS。与Subversion或其他传统VCS相比,您是否注意到分支之间进行这些合并的任何优势?我刚刚使用subversion完成了2个完全独立的已开发分支之间的非常痛苦的合并过程,并在考虑是否使用git会更容易。
aKzenT 2012年

与Mercural进行合并要比与我们先前的工具Vault进行合并要容易得多。主要优点之一是,Mercurial确实擅长将更改历史放在应用程序的前面和中心,这使得在哪里进行跟踪变得容易。对于我们来说,最困难的部分是转移现有分支机构-如果要合并两个分支机构,mercurial要求它们都具有相同的根,因此进行设置需要最后一次完全手动合并。
Dan Monego'8

2

恐怕我对您描述的问题没有直接的经验,但是我有一些评论。

第二种选择是将代码整合到一个中央存储库中(在可行的情况下),并为定制进行架构设计(再次在可行的情况下),这从长远来看几乎是肯定的方法。

问题是您打算如何到达那里,需要多长时间。

在这种情况下,一次(在存储库中)一次拥有一个以上的应用程序副本可能是可以的。

这将使您逐渐过渡到直接支持自定义的体系结构,而无需一fell而就。


2

第二种方法似乎更优雅,但是这种方法有很多未解决的问题。

我确信所有这些问题都可以解决。如果您遇到困难,请在此处或其他位置询问具体问题。

正如其他人指出的那样,您最好选择拥有一个中央代码库/一个存储库。我尝试回答您的示例问题。

例如:如何处理模型/数据库中的更改/添加。我们将.NET与Entity Framework结合使用来拥有强类型化的实体。我看不到如何处理一个客户所需的属性,而又另一个客户无用的属性而又不会弄乱我们的数据模型。

有一些可能性,我在现实系统中已经看到过。选择哪种取决于您的情况:

  • 在一定程度上忍受混乱
  • 引入一个表“ CustomAttributes”(描述名称和类型)和“ CustomAttributeValues”(用于值,例如,以字符串表示形式存储,即使它们是数字)。这将允许在安装时或运行时添加此类属性,并为每个客户提供单独的值。不要坚持在数据模型中“可视化地”对每个自定义属性建模。

  • 现在应该清楚如何在代码中使用它:只有用于访问这些表的通用代码,以及只有代码(可能在您自己的单独的插件DLL中,由您自己决定)才能正确地解释该属性

  • 另一种选择是给每个实体表一个大字符串字段,您可以在其中添加单个XML字符串。
  • 尝试归纳一些概念,以便可以更轻松地在不同客户之间重用它们。我推荐马丁·福勒(Martin Fowler)的书“ 分析模式 ”。尽管这本书不是关于自定义软件本身的,但对您也可能会有帮助。

对于特定的代码:您还可以尝试将脚本语言引入您的产品中,尤其是在添加客户特定的脚本时。这样,您不仅可以在代码和特定于客户的代码之间建立清晰的界限,还可以使您的客户在一定程度上自行定制系统。


我在添加CustomAttributes或XML列以存储自定义属性时看到的问题是它们的功能非常有限。例如,基于这些属性进行排序,分组或过滤非常具有挑战性。我曾经在一个使用额外属性表的系统上工作,维护和处理这些自定义属性变得越来越复杂。由于这个原因,我在考虑将这些属性作为列放置在一个额外的表中,该表将1:1映射到原始表。但是,如何定义,查询和管理这些问题仍然存在。
aKzenT 2012年

@aKzenT:可定制性不是免费提供的,您将不得不权衡易用性和可定制性。我的一般建议是,不要在系统的核心部分以任何方式依赖于任何自定义部件的情况下引入依赖关系,而只能采用相反的方式。例如,在为客户1引入这些“额外表”时,您是否可以避免为客户2部署该表和相关代码?如果答案是肯定的,那么解决方案就可以了。
布朗

0

我只构建了一个这样的应用程序。我想说90%的售出产品是按原样出售的,没有任何修改。每个客户都有自己的自定义外观,我们在该外观中提供了系统服务。当mod出现并影响了核心部分时,我们尝试使用IF分支。当Mod#2进入同一部分时,我们切换到CASE逻辑,以便将来进行扩展。这似乎可以处理大多数次要请求。

任何其他较小的自定义请求都通过实现Case逻辑来处理。

如果mod是两个基本模块,我们将构建一个克隆(单独的include),并在其周围包裹一个CASE以包含其他模块。

对核心的错误修复和修改影响了所有用户。在投入生产之前,我们在开发中进行了全面的测试。我们始终会发送伴随任何更改的电子邮件通知,并且从不,从不,从不在星期五发布生产更改...从不。

我们的环境是Classic ASP和SQL Server。我们不是一家意大利面条式的代码商店...一切都是使用包括,子例程和函数进行模块化的。


-1

当要求我开始开发与A共享80%功能的B时,我将:

  1. 克隆并修改它。
  2. 将A和B共同使用的功能提取到C中。
  3. 使A具有足够的可配置性,以满足B及其自身的需求(因此B嵌入在A中)。

您选择1,这似乎不太适合您的情况。您的任务是预测2和3中的哪个更合适。


1
听起来很简单,但实际上如何做到这一点?如何使您的软件具有可配置性,而又不会因“ if(customer1)”而混乱,该软件在一段时间后变得难以维护。
aKzenT 2012年

@aKzenT这就是为什么我留下2和3供您选择的原因。如果那种需要通过配置进行customer1表的项目支持的customer2的需求的改变将会使代码难以维护,那么请不要做3
摩西Revah

我更喜欢用“ if(option1)”代替“ if(customer1)”。这样,使用N个选项,我可以管理很多可能的客户。例如,使用N个布尔选项,您可以管理2 ^ N个客户,仅拥有N个'if'...不可能通过需要2 ^ N个'if'的“ if(customer1)”策略进行管理。
2014年
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.