贫血模型与富数据模型的比较

在软件开发中,模型的设计对于程序的可维护性、可扩展性和性能都有重要影响。本文将探讨两种常见的模型设计方法:贫血模型(Anemic Model)和富数据模型(Rich Data Model),并分析它们在面向对象编程(OOP)中的应用和争议。

什么是贫血模型(Anemic Model)?

贫血模型,顾名思义,是指那些只包含数据而不包含业务逻辑的模型。例如,下面是一个简单的“客户”模型,它只包含了数据(属性),而没有包含业务逻辑(行为)。

class Customer { private string _CustomerName; public string CustomerName { get { return _CustomerName; } set { _CustomerName = value; } } private string _CustomerCode; public string CustomerCode { get { return _CustomerCode; } set { _CustomerCode = value; } } }

在现实世界中,如果没有业务逻辑,这种简单的模型是没有用的,除非正在创建数据传输对象(DTO),它们是用于不同目的的。一些开发者更愿意将这些行为和方法放入不同的类中,这些类通常使用服务、外观、管理器、实用工具等不同的术语命名。然后,这些简单的模型被传递给这些类,由它们来执行行为。

什么是富数据模型(Rich Data Model)?

富数据模型在同一类中既包含数据也包含行为。以下是之前定义的贫血模型的富数据模型。可以看到数据(CustomerCode和CustomerName)和行为(Validate)都是同一个模型的一部分。

class Customer { private string _CustomerName; public string CustomerName { get { return _CustomerName; } set { _CustomerName = value; } } private string _CustomerCode; public string CustomerCode { get { return _CustomerCode; } set { _CustomerCode = value; } } public bool Validate() { if (CustomerCode.Length == 0) { return false; } if (CustomerName.Length == 0) { return false; } return true; } }

富数据模型将数据和行为封装在同一个类中,这符合面向对象编程的原则,即对象应该同时拥有数据和行为。

贫血模型与技术关注点

许多开发者将技术交叉关注点与贫血模型混淆。在贫血模型中,服务类中的行为是现实世界模型的行为。许多开发者创建实用程序或服务类,将技术行为与模型解耦,这不是贫血模型。例如,有人可以创建一个“Dal”类,如下所示,并将“Customer”对象传递给数据库插入。许多人将这种方法称为“仓库”模式。

class Dal { public void InsertDb(Customer obj) { // ADO.NET代码在这里 } }

这种模型不是贫血模型。这是将技术行为与业务行为分离,这是完全有意义的。在前面的例子中,有“validate”方法,它实际上属于“Customer”模型。所以贫血模型是那些将业务行为分离到不同类中的模型,而不是技术行为。所以上面的“Dal”类不是贫血模型。

对贫血模型的三大抱怨

许多开发者无意中遵循了贫血模型,因为它简单,但面向对象编程社区对贫血模型有以下担忧:

  • 贫血模型不尊重面向对象概念。面向对象编程中的对象既有数据也有行为。对象应该模拟现实世界的实体。通过分离行为,没有遵循面向对象编程。如果行为位于不同的类中,将很难继承、应用多态性、抽象等。
  • 贫血模型可能在任何时候都有不一致的状态。例如,看下面的代码,首先创建了一个有效的客户对象,验证服务逻辑运行在它上面,并将“IsValid”设置为true。所以直到步骤1,一切都很好,“IsValid”是true,与客户对象数据同步。现在在某个地方的步骤2,客户名称属性被设置为空。在这一步,客户对象在现实中是无效的,但如果在步骤3尝试显示,它显示有效。换句话说,数据和IsValid标志不同步。
  • 许多人抱怨这是过程化编程,因此是一种反模式。如果看过程化编程方法,创建方法和函数,传递结构给它并得到输出。

贫血模型在IOC和DI伪装下的存在

尽管面向对象编程开发者的观点,贫血模型在IOC和DI架构的伪装下非常美丽和谐地存在。个人发现说它是一种反模式是非常残酷的。

考虑以下场景,有一个客户类,它有不同类型的折扣计算。所以,而不是将折扣计算逻辑放在客户类内部,可以从构造函数中注入对象,如下所示。如果对依赖注入不熟悉,请阅读这个DI IOC在C#中的介绍。

class Customer { private IDiscount _Discount = null; public Customer(IDiscount _Dis) { _Discount = _Dis; } private string _CustomerName; public string CustomerName { get { return _CustomerName; } set { _CustomerName = value; } } public string Region { get; set; } public double Amount { get; set; } public double CalculateDiscount() { return _Discount.Calculate(this); } }

折扣计算逻辑在另一个类中,它接受客户对象来计算折扣。

public interface IDiscount { double Calculate(Customer Cust); }

以下是两种折扣计算,一种是周末折扣,另一种是特别折扣。周末折扣根据地区和工作日进行计算,而特别折扣是30%的固定折扣。

public class WeekEndDiscount : IDiscount { public double Calculate(Customer Cust) { if (Cust.Region == "Asia") { if (DateTime.Today.DayOfWeek == DayOfWeek.Sunday) { return (Cust.Amount * 0.20); } } return (Cust.Amount * 0.10); } } public class SpecialDiscount : IDiscount { public double Calculate(Customer Cust) { return (Cust.Amount * 0.30); } }

这些类也遵循贫血模型,因为动作在不同的类中,但它仍然完美地解耦、有效和面向对象。

贫血数据模型是一个简单的纯模型类,只有数据,行为位于不同的类中。富数据模型既包含数据也包含行为。贫血模型因不符合面向对象、可扩展性、一致性和遵循过程化编程方法而受到批评。贫血模型不应与技术类如日志记录、仓库等交叉关注点混淆。DI IOC间接地迫使贫血模型。所以,在今天的世界上,贫血模型在DI IOC的伪装下非常存在。

沪ICP备2024098111号-1
上海秋旦网络科技中心:上海市奉贤区金大公路8218号1幢 联系电话:17898875485