概述
如前所述,领域模型(Domain Model)只是涉及程序业务逻辑的 POCO 类。所以不管是开发哪类程序,领域模型设计的原则和技巧是相通的。
基本设计原则——SOLID 原则
C# 是一种面向对象的编程语言。作为一名经验丰富的 C# 开发人员,你可能听说过面向对象设计的一大常识—— SOLID 原则。要设计一个好的领域模型,也应该遵循 SOLID 原则,当然在必要时可以视业务逻辑做适当调整。
下表是 SOLID 原则的摘要。你可以在 Wikipedia 上找到更多详细信息。另外,如果你想进一步了设计原则,我强烈推荐《Agile Principles, Patterns, and Practices in C#》。
首字母 | 原则 | 概念 |
---|---|---|
S | Single responsibility principle (SRP) 单一职责原则 |
一个类应该只有一个职责。 换句话说,类在设计好后再修改它的潜在原因只能有一个。 |
O | Open/closed principle (OCP) 开闭原则 |
软件实体应该是可扩展,而不可修改的。 换句话说,类对扩展是开放的,而对修改是封闭的。 |
L | Liskov substitution principle (LSP) 里氏替换原则 |
程序中的对象应可替换为其子类实例,且不会改变程序的正确性。 换句话说,当一个子类的实例能够替换任何其父类的实例时,它们间才具有 is-A 关系 。 |
I | Interface segregation principle (ISP) 借口隔离原则 |
使用多个专用的接口比使用单一的通用接口好。 换句话说,不能强迫消费类去依赖那些他们不使用的接口。 |
D | Dependency inversion principle (DIP) 依赖反转原则 |
应该依赖于抽象而不是实体。 |
高级设计原则和设计模式
当将现实世界对象抽象并封装成领域模型时,领域模型之间的关系映射了现实世界对象之间的关系。这其中有些关系是清晰、固定、通用的。例如,经理的下属也可以是经理,文件夹中的子项也可以是文件夹。对于它们,我们可以设计一个接口:
public interface IComponent
{
int ID { get; set; }
string Name { get; set; }
IComponent Parent { get; set; }
ICollection<IComponent> Children { get; set; }
}
这种将一组对象组合在一起并生成数据结构的关系,我们称之为设计模式。设计模式的教学超出了本课程的范畴,如果你想了解更多有关设计模式的信息,推荐你看看 DoFactory。DoFactory 有所有 23 种经典设计模式的定义及其 C# 实现。
PS:设计模式是基于 SOLID 原则的。只要你遵循 SOLID 原则来抽象业务逻辑和创建领域模型,那么设计模式将不请自来。
将领域模型映射到数据库表
领域模型的另一个来源是数据库。既然我们安装了 MySQL 及其示例数据库,那我们就使用 sakila 数据库作为示例。
当我们将表结构转换为模型类时,有两组关键信息:
Columns tab 的列定义详细信息
Information 面板的简要信息
从这两个面板我们可以得到如下信息:
actor 表有四列,分别是 actor_id、first_name、last_name 和 last_update
这四列的 MySQL 数据类型分别是 smallint(5) unsigned、varchar(45)、varchar(45) 和 timestamp
表的主键是 actor_id,last_update 列的值是由触发器自动生成的
基于以上信息,我们设计的模型类如下:
public partial class Actor
{
public int Actor_ID { get; set; }
public string First_Name { get; set; }
public string Last_Name { get; set; }
public DateTime Last_Update { get; set; }
}
领域模型作为实体类
大多数 Web 开发框架都使用 ORM 框架将领域模型类映射到数据库表。使用 ORM 框架的好处是“业务逻辑层开发人员”可以直接操作数据库,而无需在 C# 中混合使用 SQL 语句。ORM 框架可以将数据的 CRUD 都转换为 SQL 语句,也可以将存储过程包装在类方法中。
EF Core 就是适用于 ASP.NET Core 的 ORM 框架。为了在项目中使用 EF Core,我们需要添加其 NuGet 包。将 EF Core 引用添加到项目后,我们可以使用 EF Core 的特性标注领域模型类,使其成为实体类。
修改后的模型/实体类:
public partial class Actor
{
[Key]
public int Actor_ID { get; set; }
public string First_Name { get; set; }
public string Last_Name { get; set; }
public DateTime Last_Update { get; set; }
}
更近一步,我们可以把属性名里面的下划线都去掉:
[Table("actor")]
public partial class Actor
{
[Key, Column("actor_id")]
public int ActorID { get; set; }
[Column("first_name")]
public string FirstName { get; set; }
[Column("last_name")]
public string LastName { get; set; }
[Column("last_update")]
public DateTime LastUpdate { get; set; }
}
使用部分类来分离数据与操作
你可能已经注意到了我们的模型类是部分类(partial)。那是因为我们希望将模型的操作/行为部分与数据/实体部分分开。这样做的一大好处是,你可以使用自动化工具生成模型类的数据/实体部分,而无需覆盖或删除操作/行为代码。
例如,当我们用工具重新生成 Actor 的实体类部分时,不会影响下面的代码:
public partial class Actor
{
public IList<Film> GetFilmsInStock() {
// call stored procedures
}
public IList<Film> GetFilmsNotInStock() {
// call stored procedures
}
}
将数据库数据类型映射为 C# 数据类型
DBMS 通常拥有比程序框架更丰富的数据类型集。这主要是因为 DBMS 需要处理极具挑战性的数据存储和数据操作方案。若要正确的将表列映射到模型类属性,可以参考下表:
MySQL
MySQL 类型名 | GetColumnClassName 的返回值 | .NET Framework 类型 |
---|---|---|
BIT(1) | BIT | bool/System.Boolean |
BIT(>1) | BIT | byte[] |
TINYINT | TINYINT | bool/System.Boolean if the configuration property tinyInt1isBit is set to true (the default) and the storage size is 1, or int/System.Int32 if not. |
BOOL, BOOLEAN | TINYINT | See TINYINT, above as these are aliases for TINYINT(1), currently. |
SMALLINT[(M)] [UNSIGNED] | SMALLINT [UNSIGNED] | int/System.Int32 (uint/System.UInt32 if it is UNSIGNED) |
MEDIUMINT[(M)] [UNSIGNED] | MEDIUMINT [UNSIGNED] | int/System.Int32 (uint/System.UInt32 if it is UNSIGNED) |
INT,INTEGER[(M)] [UNSIGNED] | INTEGER [UNSIGNED] | int/System.Int32, if UNSIGNED System.UInt32 |
BIGINT[(M)] [UNSIGNED] | BIGINT [UNSIGNED] | long/System.Int64, if UNSIGNED ulong/System.UInt64 |
FLOAT[(M,D)] | FLOAT | float/System.Single |
DOUBLE[(M,B)] | DOUBLE | double/System.Double |
DECIMAL[(M[,D])] | DECIMAL | decimal |
DATE | DATE | System.DateTime |
DATETIME | DATETIME | System.DateTime |
TIMESTAMP[(M)] | TIMESTAMP | System.DateTime |
TIME | TIME | System.DateTime |
YEAR[(2 | 4)] | YEAR | int/System.Int32 |
CHAR(M) | CHAR | string/System.String (unless the character set for the column is BINARY, then byte[] is returned. |
VARCHAR(M) [BINARY] | VARCHAR | string/System.String (unless the character set for the column is BINARY, then byte[] is returned. |
BINARY(M) | BINARY | byte[] |
VARBINARY(M) | VARBINARY | byte[] |
TINYBLOB | TINYBLOB | byte[] |
TINYTEXT | VARCHAR | string/System.String |
BLOB | BLOB | byte[] |
TEXT | VARCHAR | string/System.String |
MEDIUMBLOB | MEDIUMBLOB | byte[] |
MEDIUMTEXT | VARCHAR | string/System.String |
LONGBLOB | LONGBLOB | byte[] |
LONGTEXT | VARCHAR | string/System.String |
ENUM(‘value1’,’value2’,…) | CHAR | string/System.String |
SET(‘value1’,’value2’,…) | CHAR | string/System.String |
SQLServer
SQL Server 数据库引擎类型 | .NET Framework 类型 |
---|---|
bigint | long/System.Int64 |
binary | byte[]/System.Byte[] |
bit | bool/System.Boolean |
char | string/System.String/char[]/System.Char[] |
date/(SQL Server 2008 and later) | System.DateTime |
System.DateTime | System.DateTime |
System.DateTime2/(SQL Server 2008 and later) | System.DateTime |
System.DateTimeoffset/(SQL Server 2008 and later) | System.DateTimeOffset |
decimal/System.Decimal | decimal/System.Decimal |
FILESTREAM attribute (varbinary(max)) | byte[]/System.Byte[] |
float | double/System.Double |
image | byte[]/System.Byte[] |
int | int/System.Int32 |
money | decimal/System.Decimal |
nchar | string/System.String/char[]/System.Char[] |
ntext | string/System.String/char[]/System.Char[] |
numeric | decimal/System.Decimal |
nvarchar | string/System.String/char[]/System.Char[] |
real | float/System.Single |
rowversion | byte[]/System.Byte[] |
smallSystem.DateTime | System.DateTime |
smallint | short/System.Int16 |
smallmoney | decimal/System.Decimal |
sql_variant | object/System.Object |
text | string/System.String/char[]/System.Char[] |
time/(SQL Server 2008 and later) | System.TimeSpan |
timestamp | byte[]/System.Byte[] |
tinyint | byte/System.Byte |
uniqueidentifier | System.Guid |
varbinary | byte[]/System.Byte[] |
varchar | string/System.String/char[]/System.Char[] |
xml | System.Xml |