如何设计一个通用的业务实体并且仍然是 OO?

Posted

技术标签:

【中文标题】如何设计一个通用的业务实体并且仍然是 OO?【英文标题】:How to Design a generic business entity and still be OO? 【发布时间】:2011-06-17 20:28:37 【问题描述】:

我正在开发一种打包产品,该产品应该满足具有不同要求(在一定程度上)的多个客户,因此应该以足够灵活的方式构建,以便每个特定客户都可以定制。我们在这里讨论的定制类型是不同的客户可能对某些关键业务对象具有不同的属性。此外,它们的附加属性也可能具有不同的业务逻辑

举一个非常简单的例子: 将“汽车”视为系统中的一个业务实体,因此具有 4 个关键属性,即 VehicleNumber、YearOfManufacture、Price 和 Colour。

有可能使用该系统的客户之一向汽车添加了另外 2 个属性,即 ChassisNumber 和 EngineCapacity。此客户端需要一些与这些字段相关联的业务逻辑,以验证添加新汽车时系统中不存在相同的 chassisNumber。

另一个客户只需要一个名为 SaleDate 的附加属性。 SaleDate 有自己的业务逻辑检查,在输入销售日期时验证车辆在某些警方记录中是否不存在作为被盗车辆

我的大部分经验主要是为单个客户制作企业应用程序,我真的很想知道如何处理一个业务实体,它的属性是动态的,并且还具有动态业务逻辑的能力。面向对象范式

关键问题

是否有任何通用的 OO 原则/模式可以帮助我处理这种设计?

我敢肯定,从事通用/包装产品工作的人在大多数情况下都会遇到类似的情况。任何建议/指针/一般指导也很感激。

我的技术是 .NET 3.5/C#,该项目具有分层架构,其中业务层由包含业务逻辑的业务实体组成

【问题讨论】:

您的客户是否应该能够添加属性并实现自定义业务逻辑,或者您是否需要为您添加这些客户特定的调整? @Andreas - 前者。客户应该能够自己动态完成,而无需对更改进行编程 @All - Chris 和 Ciaran 的答案各有千秋,但如果我理解正确,它们都涉及对整个应用程序架构的架构更改,因为我的应用程序是一个现有的应用程序,此要求是更多的增强,我会欣赏任何其他 OOP 模式/解决方案,它们可能更容易适应我现有的 N 层方法。因此,在这个问题上悬赏,可能会引起更多关注。 一次只有一个客户端使用这个系统还是多个客户端? 【参考方案1】:

这是我们面临的最大挑战之一,因为我们有多个客户都使用相同的代码库,但需求却大相径庭。让我与您分享我们的进化故事:

我们公司一开始只有一个客户,随着我们开始获得其他客户,您会开始在代码中看到类似这样的内容:

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 的混合。

【讨论】:

所有这些如果,你为什么不试试 OO :) @Stephan Eggermont:是的,你会注意到随着产品的发展,我们最终放弃了很多 if 语句,转而支持 XML/数据库配置或依赖注入。当然,虽然良好的 OO 实践通常会避免过度使用 if,但 OO 并不是天生的“反假设”(***.com/questions/1167589/anti-if-campaign/…)【参考方案2】:

这是您正在构建的 Dynamic Object Model 或 Adaptive Object Model。当然,当客户开始添加行为和数据时,他们正在编程,因此您需要为此进行版本控制、测试、发布、命名空间/上下文和权限管理。

【讨论】:

【参考方案3】:

解决此问题的一种方法是使用元层或反射,或两者兼而有之。此外,您需要提供一个自定义应用程序,允许用户修改您的业务逻辑层。这样的元层并不真正适合您的分层架构 - 它更像是您现有架构的正交层,尽管正在运行的应用程序可能需要引用它,至少在初始化时。这种类型的设施可能是破坏人类已知的生产应用程序的最快方法之一,因此您必须:

    确保此编辑器的访问权限仅限于系统上具有高级权限的人员(例如管理员)。 为客户修改提供一个沙盒区域,以便在他们测试的任何更改被放到生产系统之前进行测试。 一种“OOPS”工具,他们可以将其生产系统恢复为您提供的初始默认设置或更改前的最后一个版本。 您的元层必须非常严格地指定,以便严格定义活动范围 - George Orwell 的“没有明确允许的,是禁止的。”

您的元层中将包含对象,例如业务对象、方法、属性和事件,例如添加业务对象、调用方法等。

网络上有大量关于元编程的信息,但我将从程序设计的模式语言第 2 卷或任何与 Kent 或 Coplien 相关或源自 Kent 或 Coplien 的 WWW 资源开始。

【讨论】:

感谢元编程的参考。这绝对是我不太了解的事情,需要更详细地研究。【参考方案4】:

我们开发了一个可以执行此类操作的 SDK。我们选择 COM 作为我们的核心,因为我们使用它比使用低级 .NET 更舒服,但毫无疑问,您可以在 .NET 中本地完成所有操作。

基本架构是这样的:类型在 COM 类型库中描述。所有类型都派生自一个称为 Object 的根类型。 COM DLL 实现此根 Object 类型并通过 IDispatch 提供对派生类型属性的通用访问。此 DLL 包装在 .NET PIA 程序集中,因为我们预计大多数开发人员会更喜欢在 .NET 中工作。 Object 类型有一个工厂方法可以在模型中创建任何类型的对象。

我们的产品是第 1 版,我们还没有实现方法 - 在这个版本中,业务逻辑必须编码到客户端应用程序中。但我们的总体设想是,方法将由开发人员以他选择的语言编写,编译为 .NET 程序集或 COM DLL(也可能是 Java)并通过 IDispatch 公开。然后我们根 Object 类型中的相同 IDispatch 实现可以调用它们。

如果您预计大多数自定义业务逻辑将是验证(例如检查重复的底盘号),那么您可以在您的根对象类型上实现一些常规事件(假设您按照我们的方式进行了操作。)每当更新属性时,我们的 Object 类型都会触发一个事件,我想这可以通过一个验证方法来增强,如果定义了一个验证方法,该方法会自动调用。

创建这样的通用系统需要大量工作,但回报是基于 SDK 的应用程序开发非常快。

您说您的客户应该能够添加自定义属性并自己“无需编程”实现业务逻辑。如果您的系统还实现基于类型的数据存储(我们的),那么客户可以通过编辑模型(我们提供 GUI 模型编辑器)添加属性而无需编程。您甚至可以提供一个通用的用户应用程序,动态呈现适当的数据输入控件取决于类型,因此您的客户无需额外编程即可捕获自定义数据。 (我们提供了一个通用的客户端应用程序,但它更像是一个开发人员工具,而不是一个可行的最终用户应用程序。)我不明白你如何允许你的客户在没有编程的情况下实现自定义逻辑......除非你想提供某种拖放式 GUI 工作流构建器...肯定是一项艰巨的任务。

我们不打算让商业用户做任何这些事情。在我们的开发模型中,所有定制都由开发人员完成,但不一定是昂贵的定制 - 我们的部分愿景是让经验不足的开发人员开发出强大的业务应用程序。

【讨论】:

感谢您的详细解答。它绝对帮助了我,并给了我一些可能的想法。【参考方案5】:

设计一个核心模型作为自己的独立项目

这里列出了一些可能的基本要求...

核心设计将包含:

在所有子项目中都有效(并且可能被扩展)的类。 更复杂的工具,例如数据库交互(除非是特定于项目的) 一种通用配置结构,应视为所有项目的标准配置

然后,为每个客户定制的所有后续项目都被视为此核心项目的扩展。

您所描述的是任何框架的基本目的。也就是说,创建一组可以与整体分开的核心功能,这样您就不必在您创建的每个项目中重复该开发工作。即,加入一个框架,你的工作已经完成了一半。


您可能会说,“SCM(软件配置管理)呢?”

如何在不将核心包含到子项目存储库中的情况下跟踪所有子项目的修订历史?

幸运的是,这是一个老问题。许多软件项目,尤其是 linux/开源世界中的软件项目,都大量使用外部库和插件。

事实上 git 有一个命令专门用于将一个项目存储库作为子存储库导入另一个项目存储库(保留所有子存储库的修订历史等)。事实上,您无法修改子存储库的内容,因为项目根本不会跟踪它的历史记录。

我所说的命令叫做'git submodule'。

您可能会问,“如果我在一个客户的项目中开发了一个非常酷的功能,我想在我的所有客户的项目中使用它怎么办?”。

只需将该功能添加到核心并在所有其他项目上运行“git submodule sync”。 git submodule 的工作方式是,它指向子存储库历史树中的特定提交。因此,当该树在上游发生更改时,您需要将这些更改拉回下游到使用它们的项目中。

实现这种东西的结构将像这样工作。假设您的软件是专门为管理汽车经销商(库存、销售、员工、客户、订单等)而编写的。您创建了一个涵盖所有这些功能的核心模块,因为它们预计将在您的所有客户的软件中使用。

但是,您最近获得了一位新客户,该客户希望通过在其经销商中增加在线销售来更加精通技术。当然,他们的网站是由一个单独的 Web 开发人员/设计师和网站管理员团队设计的,但他们想要一个 Web API(即服务层)来利用他们网站的当前基础架构。

您要做的是为客户端创建一个项目,我们将其命名为 WebDealersRUs 并将核心子模块链接到存储库。


这样做的隐藏好处是,一旦您开始将代码库视为可插拔部件,您就可以从一开始就将它们设计为模块化部件,能够轻松地放入项目中。

考虑上面的例子。假设您的客户群开始看到添加网络前端以增加销售额的优点。只需将 Web API 从 WebDealersRUs 中拉出到它自己的存储库中,然后将其作为子模块链接回来。然后传播给所有需要它的客户。

您得到的是以最少的努力获得重大回报。


当然,每个项目中总会有一些特定于客户的部分(品牌等)。这就是为什么每个客户应该都有一个单独的存储库,其中包含他们独特的软件版本。但这并不意味着您不能将部分提取出来并概括它们以在后续项目中重复使用。

虽然我从宏观层面处理这个问题,但它可以应用于代码库的更小/更具体的部分。这里的关键是您希望重复使用的代码需要被泛化

OOP 在这里发挥作用是因为:在核心实现功能但在客户端代码中扩展的地方,您将使用基类并从它继承;如果该功能预计会返回类似类型的结果,但该功能的实现可能在不同类之间存在很大差异(即,没有直接继承层次结构),最好使用接口来强制实现这种关系。

【讨论】:

【参考方案6】:

我知道您的问题很笼统,与技术无关,但由于您提到您实际上使用 .NET,我建议您查看 .NET 4 中的一项新的非常重要的技术:the 'dynamic' type.

这里还有一篇关于 CodeProject 的好文章:DynamicObjects – Duck-Typing in .NET。

可能值得一看,因为如果我必须实现您描述的动态系统,我肯定会尝试基于 DynamicObject 类实现我的实体,并使用 TryGetxxx 方法添加自定义属性和方法。这还取决于您关注的是编译时间还是运行时。这里有一个关于 SO 的有趣链接:Dynamically adding members to a dynamic object 关于这个主题。

【讨论】:

【参考方案7】:

我觉得有两种方法:

1) 如果不同的客户属于同一个领域(如制造/财务),那么最好以这样的方式设计对象,即 BaseObject 应该具有非常常见的属性,而其他属性可能在客户之间作为键值而有所不同对。在此之上,尝试实现类似 IBM ILog 的规则引擎(http://www-01.ibm.com/software/integration/business-rule-management/rulesnet-family/about/)。

2) 预测模型标记语言(http://en.wikipedia.org/wiki/PMML)

【讨论】:

对于一个键值对数据库,将值持久地存储在磁盘检查 memcachedb.org 上。其他此类数据库实现(以及基于内存而非磁盘的键值)可以在本文底部找到 - en.wikipedia.org/wiki/NoSQL。

以上是关于如何设计一个通用的业务实体并且仍然是 OO?的主要内容,如果未能解决你的问题,请参考以下文章

谈谈通用对账平台的业务架构设计

OO的五大原则是指SRPOCPLSPDIPISP。

WCF 作为业务逻辑

模块和类的通用转换规则,如何oo?

通用持久层如何设计

在重构业务系统时,应用领域驱动设计