实体类的理解
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了实体类的理解相关的知识,希望对你有一定的参考价值。
C#中实体类怎么理解??
详细一点,
这就给我们一个感觉, 实体类只包含属性,没有方法.尽管方法和属性其实一样,只是属性描述数据成员, 这也是C#优势所在, 它更接近人类认识世界的过程 --- 属性,比如上述的身高,体重等.但是其他语言似乎没有属性一说.
把所有动作(方法和事件)放在另外的类中, 叫做控制类. 这就是MVC的概念. 参考技术A 他们说的太抽象 太书本了
你要是刚开始学 书本还不太理解 你就把实体类当成 临时数据库就好了 只要你与服务器交互没有断(就是网页开着,或者程序开着)你就能对这个临时的数据库 存和取了 就这么回事 要说实体类的含义 就太抽象了 考试的时候才用。。可以一边学 一边理解 参考技术B 你可以把每个实体类都看成是一个对象
比如“人类”,
眼睛
鼻子
嘴巴
手
脚
这些就是他的基本特征。
如果你想让他说一句话。
public void SayHi()
MessageBox.Show("你好!");
这个就是他的方法。就像人会说话 但是不一定在什么地方都说出来,
只是在适当的时候 调用这个方法就起到效果了。
你可以把任何东西 都可以写成一个实体类 封装起来,
所以任何东西 你都可以把它看做成一个实体类。
这个属于面向对象的思想!
你刚学习C# “类”是属于引用类型 这些你肯定体会不到 或不够深刻的。
讲也讲不清楚,必须自己边学习边体会~ 参考技术C 实体类叫做数据的载体
好像拉货的汽车一样! 参考技术D 类(class)是C#类型中最基础的类型。类是一个数据结构,将状态(字段)和行为(方法和其他函数成员)组合在一个单元中。类提供了用于动态创建类实例的定义,也就是对象(object)。类支持继承(inheritance)和多态(polymorphism),即派生类能够扩展和特殊化基类的机制。使用类声明可以创建新的类。类声明以一个声明头开始,其组成方式如下:先是指定类的特性和修饰符,后跟类的名字,基类(如果有的话)的名字,以及被该类实现的接口名。声明头后面就是类体了,它由一组包含在大括号()中的成员声明组成。下面是一个名为Point的简单类的声明:
public class Point
public int x, y;
public Point(int x, int y)
this.x = x; this.y = y;
使用new运算符创建类的实例,它将为新实例分配内存,调用构造函数初始化实例,并且返回对该实例的引用。下面的语句创建两个Point对象,并且将那些对象的引用保存到两个变量中:
Point p1 = new Point(0, 0);
Point p2 = new Point(10, 20);
当不再使用对象时,该对象所占的内存将被自动回收。在C#中,没有必要也不可能显式地释放对象。
1.6.1 成员类的成员或者是静态成员(static member),或者是实例成员(instance member)。
静态成员属于类,实例成员属于对象(类的实例)。
表1.6提供了类所能包含的各种成员的描述。
表1.6 类 的 成 员成 员描 述常数与类关联的常量值字段类的变量方法能够被类执行的计算和行为属性使对象能够读取和写入类的命名属性索引器使对象能够用与数组相同的方式进行索引事件能够被类产生的通知运算符类支持的转换和表达式运算符构造函数初始化类的实例或者类本身析构函数在永久销毁类的实例之前执行的行为类型被类声明的嵌套类型1.6.2 可访问性类的每个成员都有关联的可访问性,它控制能够访问该成员的程序文本区域。有5种可能的可访问性形式。表1.7概述了类的可访问性的意义。表1.7 类的可访问性可访问性意 义public访问不受限制protected访问仅限于包含类或从包含类派生的类型internal访问仅限于当前程序集protected internal访问仅限于从包含类派生的当前程序集或类型private访问仅限于包含类1.6.3 基类类的声明可能通过在类名后加上冒号和基类的名字来指定一个基类译注4。省略基类等同于直接从object类派生。在下面的示例中,Point3D的基类是Point,而Point的基类是object:public class Point public int x, y; public Point(int x, int y) this.x = x; this.y = y; public class Point3D: Point public int z; public Point3D(int x, int y, int z): Point(x, y) this.z = z; Point3D类继承了其基类的成员。继承意味着类将隐式地包含其基类的所有成员(除了基类的构造函数)。派生类能够在继承基类的基础上增加新的成员,但是它不能移除继承成员的定义。在前面的示例中,Point3D类从Point类中继承了x字段和y字段,并且每一个Point3D实例都包含三个字段x,y和z。从类类型到它的任何基类类型都存在隐式的转换。并且,类类型的变量能够引用该类的实例,或者任何派生类的实例。例如,对于前面给定的类声明,Point类型的变量能够引用Point实例或者Point3D实例:Point a = new Point(10, 20);Point b = new Point3D(10, 20, 30);1.6.4 字段字段是与对象或类相关联的变量。当一个字段声明中含有static修饰符时,由该声明引入的字段为静态字段(static field)。它只标识了一个存储位置。不管创建了多少个类实例,静态字段都只会有一个副本。当一个字段声明中不含有static修饰符时,由该声明引入的字段为实例字段(instance field)。类的每个实例都包含了该类的所有实例字段的一个单独副本。在下面的示例中,Color类的每个实例都有r,g,b实例字段的不同副本,但是Black,White,Red,Green和Blue等静态字段只有一个副本:public class Color public static readonly Color Black = new Color(0, 0, 0); public static readonly Color White = new Color(255, 255, 255); public static readonly Color Red = new Color(255, 0, 0); public static readonly Color Green = new Color(0, 255, 0); public static readonly Color Blue = new Color(0, 0, 255); private byte r, g, b; public Color(byte r, byte g, byte b) this.r = r; this.g = g; this.b = b; 如前面的示例所示,通过readonly修饰符声明只读字段。给readonly字段的赋值只能作为声明的组成部分出现,或者在同一类中的实例构造函数或静态构造函数中出现。1.6.5 方法方法(method)是一种用于实现可以由对象或类执行的计算或操作的成员。静态方法(static method)只能通过类来访问。实例方法(instance method)则要通过类的实例访问。方法有一个参数(parameter)列表(可能为空),表示传递给方法的值或者引用;方法还有返回类型(return type),用于指定由该方法计算和返回的值的类型。如果方法不返回一个值,则它的返回类型为void。在声明方法的类中,该方法的签名必须是惟一的。方法的签名由它的名称、参数的数目、每个参数的修饰符和类型组成。返回类型不是方法签名的组成部分。1.6.5.1 参数参数用于将值或者引用变量传递给方法。当方法被调用时,方法的参数译注5从指定的自变量(argument)译注6得到它们实际的值。C#有4种参数:值参数、引用参数、输出参数和参数数组。值参数(value parameter)用于输入参数的传递。值参数相当于一个局部变量,它的初始值是从为该参数所传递的自变量获得的。对值参数的修改不会影响所传递的自变量。引用参数(reference parameter)用于输入和输出参数的传递。用于引用参数的自变量必须是一个变量,并且在方法执行期间,引用参数和作为自变量的变量所表示的是同一个存储位置。引用参数用ref修饰符声明。下面的示例展示了ref参数的使用:using System;class Test static void Swap(ref int x, ref int y) int temp = x; x = y; y = temp; static void Main() int i = 1, j = 2; Swap(ref i, ref j); Console.WriteLine("0 1", i, j); //输出 "2 1" 输出参数(output parameter)用于输出参数的传递。输出参数类似于引用参数,不同之处在于调用方提供的自变量初始值无关紧要。输出参数用out修饰符声明。下面的示例展示了out参数的使用:using System; class Test static void Divide(int x, int y, out int result, out int remainder) result = x / y; remainder = x % y; static void Main() int res, rem; Divide(10, 3, out res, out rem); Console.WriteLine("0 1", res, rem); //输出 "3 1" 参数数组(parameter array)允许将可变长度的自变量列表传递给方法。参数数组用params修饰符声明。只有方法的最后一个参数能够被声明为参数数组,而且它必须是一维数组类型。System.Console类的Write和WriteLine方法是参数数组应用的很好的例子。它们的声明形式如下: public class Console public static void Write(string fmt, params object[] args) ... public static void WriteLine(string fmt, params object[] args) ... ... 在方法中使用参数数组时,参数数组表现得就像常规的数组类型参数一样。然而,带数组参数的方法调用中,既可以传递参数数组类型的单个自变量,也可以传递参数数组的元素类型的若干自变量。对于后者的情形,数组实例将自动被创建,并且通过给定的自变量初始化。示例:Console.WriteLine("x=0 y=1 z=2", x, y, z);等价于下面的语句:object[] args = new object[3];args[0] = x;args[1] = y;args[2] = z;Console.WriteLine("x=0 y=1 z=2", args);1.6.5.2 方法体和局部变量方法体指定方法调用时所要执行的语句。方法体能够声明特定于该方法调用的变量。这样的变量被称为局部变量(local variable)。局部变量声明指定类型名、变量名,可能还有初始值。下面的示例声明了一个局部变量i,其初始值为0;另一个局部变量j没有初始值。using System; class Squares static void Main() int i = 0; int j; while(i < 10) j = i * i; Console.WriteLine("0 x 0 = 1", i, j); i = i + 1; C#要求局部变量在其值被获得之前明确赋值(definitely)。例如,假设前面的变量i的声明没有包含初始值,那么,在接下来对i的使用将导致编译器报告错误,原因就是i在程序中没有明确赋值。方法能够使用return语句将控制返回给它的调用方。如果方法是void的,则return语句不能指定表达式;如果方法是非void的,则return语句必须包含表达式,用于计算返回值。1.6.5.3 静态方法和实例方法若一个方法声明中含有static修饰符,则称该方法为静态方法(static method)。静态方法不对特定实例进行操作,只能访问静态成员。若一个方法声明中没有static修饰符,则称该方法为实例方法(instance method)。实例方法对特定实例进行操作,既能够访问静态成员,也能够访问实例成员。在调用实例方法的实例上,可以用 this来访问该实例,而在静态方法中引用this是错误的。下面的Entity类具有静态和实例两种成员: class Entity static int nextSerialNo; int serialNo; public Entity() serialNo = nextSerialNo++; public int GetSerialNo() return serialNo; public static int GetNextSerialNo() return nextSerialNo; public static void SetNextSerialNo(int value) nextSerialNo = value; 每一个Entity实例包含一个序列号(并且假定这里省略了一些其他信息)。Entity构造函数(类似于实例方法)用下一个有效的序列号初始化新的实例。因为构造函数是一个实例成员,所以,它既可以访问serialNo实例字段,也可以访问nextSerialNo静态字段。GetNextSerialNo和SetNextSerialNo静态方法能够访问nextSerialNo静态字段,但是如果访问serialNo实例字段就会产生错误。下面的示例展示了Entity类的使用:using System; class Test static void Main() Entity.SetNextSerialNo(1000); Entity e1 = new Entity(); Entity e2 = new Entity(); Console.WriteLine(e1.GetSerialNo()); //输出 "1000" Console.WriteLine(e2.GetSerialNo()); //输出 "1001" Console.WriteLine(Entity.GetNextSerialNo()); //输出 "1002" 注意,SetNextSerialNo和GetNextSerialNo静态方法通过类调用,而GetSerialNo实例成员则通过类的实例调用。1.6.5.4 虚拟方法、重写方法和抽象方法若一个实例方法的声明中含有virtual修饰符,则称该方法为虚拟方法(virtual method)。若其中没有virtual修饰符,则称该方法为非虚拟方法(nonvirtual method)。在一个虚拟方法调用中,该调用所涉及的实例的运行时类型(runtime type)确定了要被调用的究竟是该方法的哪一个实现。在非虚拟方法调用中,实例的编译时类型(compile-time type)是决定性因素。虚拟方法可以由派生类重写(override)译注7实现。当一个实例方法声明中含有override修饰符时,该方法将重写所继承的相同签名的虚拟方法。虚拟方法声明用于引入新方法,而重写方法声明则用于使现有的继承虚拟方法专用化(通过提供该方法的新实现)。抽象(abstract)方法是没有实现的虚拟方法。抽象方法的声明是通过abstract修饰符实现的,并且只允许在抽象类中使用抽象方法声明。非抽象类的派生类需要重写抽象方法。下面的示例声明了一个抽象类Expression,它表示一个表达式树的节点;它有三个派生类Constant,VariableReference,Operation,它们实现了常数、变量引用和算术运算的表达式树节点。using System;using System.Collections; public abstract class Expression public abstract double Evaluate(Hashtable vars); public class Constant: Expression double value; public Constant(double value) this.value = value; public override double Evaluate(Hashtable vars) return value; public class VariableReference: Expression string name; public VariableReference(string name) this.name = name; public override double Evaluate(Hashtable vars) object value = vars[name]; if (value == null) throw new Exception("Unknown variable: " + name); return Convert.ToDouble(value); public class Operation: Expression Expression left; char op; Expression right; public Operation(Expression left, char op, Expression right) this.left = left; this.op = op; this.right = right; public override double Evaluate(Hashtable vars) double x = left.Evaluate(vars); double y = right.Evaluate(vars); switch(op) case '+' : return x + y; case '-' : return x - y; case '*' : return x * y; case '/' : return x / y; throw new Exception("Unknown operator"); 前面的4个类用于模型化算术表达式。例如,使用这些类的实例,表达式x+3能够被表示为如下的形式:Expression e = new Operation( new VariableReference("x"), '+', new Constant(3));Expression实例的Evaluate方法将被调用,以计算表达式的值,从而产生一个double值。该方法取得一个包含变量名(输入的键)和值(输入的值)的Hashtable作为其自变量。Evaluate方法是虚拟的抽象方法,意味着派生类必须重写它并提供实际的实现。Evaluate方法的Constant的实现只是返回保存的常数。VariableReference的实现在Hashtable中查找变量名,并且返回相应的值。Operation的实现则首先计算左操作数和右操作数的值(通过递归调用Evaluate方法),然后执行给定的算术运算。下面的程序使用Expression类,对于不同的x和y的值,计算表达式x*(y+2)。using System;using System.Collections;class Test static void Main() Expression e = new Operation( new VariableReference("x"), '*', new Operation( new VariableReference("y"), '+', new Constant(2) ));Hashtable vars = new Hashtable(); Vars["x"] = 3; Vars["y"] = 5; Console.WriteLine(e.Evaluate(vars)); //输出 "21" Vars["x"] = 1.5; Vars["y"] = 9; Console.WriteLine(e.Evaluate(vars)); //输出 "16.5" 1.6.5.5 方法重载方法重载(Method overloading)允许在同一个类中采用同一个名称声明多个方法,条件是它们的签名是惟一的。当编译一个重载方法的调用时,编译器采用重载决策(overload resolution)确定应调用的方法。重载决策找到最佳匹配自变量的方法,或者在没有找到最佳匹配的方法时报告错误信息。下面的示例展示了重载决策工作机制。在Main方法中每一个调用的注释说明了实际被调用的方法。class Test static void F() Console.WriteLine("F()"); static void F(object x) Console.WriteLine("F(object)"); static void F(int x) Console.WriteLine("F(int)"); static void F(double x) Console.WriteLine("F(double)"); static void F(double x, dpuble y) Console.WriteLine("F(double, double)"); static void Main() F(); //调用F() F(1); //调用F(int) F(1.0); //调用F(double) F("abc"); //调用F(object) F((double)1); //调用F(double) F((object)1); //调用F(object) F(1, 1); //调用F(double, double) 如上例所示,总是通过自变量到参数类型的显式的类型转换,来选择特定方法。1.6.6 其他函数成员类的函数成员(function member)是包含可执行语句的成员。前面部分所描述的方法是主要的函数成员。这一节讨论其他几种C#支持的函数成员:构造函数、属性、索引器、事件、运算符、析构函数。表1.8展示一个名为List的类,它实现一个可扩展的对象列表。这个类包含了最通用的几种函数成员的例子。表1.8 类的函数成员示例public class List const int defaultCapacity = 4;常数 object[] items; int count;字段 (续表) public List(): this(defaultCapacity) public List(int capacity) items = new object[capacity]; 构造函数 public int Count get return count; public string Capacity get return items.Length; set if (value < count) value = count; if (value != items.Length) object[] newItems = new object[value]; Array.Copy(items, 0, newItems, 0, count); items = newItems; 属性 public object this[int index] get return items[index]; set items[index] = value; OnListChange(); 索引器 public void Add(object item) if (count == Capacity) Capacity = count * 2; items[count] = item; count++; OnChanged(); protected virtual void OnChanged() if (Changed != null) Changed(this, EventArgs.Empty); public override bool Equals(object other) return Equals (this,other as List ); static bool Equals ( List a,List b) if (a == null) return b == null; if (b == null || a.count != b.count) return false; for (int i = 0; i < a.count; i++) if (!object.Equals(a.item[i], b.item[i])) return false; 方法 public event EventHandler Changed;事件 public static bool operator ==(List a, List b) return Equals(a, b); public static bool operator !=(List a, List b) return !Equals(a, b); 运算符1.6.6.1 构造函数C#既支持实例构造函数,也支持静态构造函数。实例构造函数(instance constructor)是实现初始化类实例所需操作的成员。静态构造函数(static constructor)是一种在类首次加载时用于实现初始化类本身所需操作的成员。构造函数的声明如同方法一样,不过,它没有返回类型,它的名字与包含它的类名一样。若构造函数的声明中包含static修饰符,则它声明了一个静态构造函数,否则声明实例构造函数。实例构造函数能够被重载。例如,List声明了两个实例构造函数,一个不带参数,一个带有一个int参数。使用new运算符可以调用实例参数。下面的语句使用各个List类的构造函数创建了两个List实例。List list1 = new List();List list2 = new List(10);实例构造函数不同于其他方法,它是不能被继承的。并且,一个类除了自己声明的实例构造函数外,不可能有其他的实例构造函数。如果一个类没有声明任何实例构造函数,则会自动地为它提供一个默认的空的实例构造函数。1.6.6.2 属性属性(property)是字段的自然扩展,两者都是具有关联类型的命名成员,而且访问字段和属性的语法是相同的。然而,属性与字段不同,不表示存储位置。相反,属性有访问器(accessor),这些访问器指定在它们的值被读取或写入时需执行的语句。属性的声明类似于字段,不同之处在于属性的声明以定界符之间的get访问器和/或set访问器结束,而不是分号。同时包含get访问器和set访问器的属性称为读写属性(read-write property)。只具有get访问器的属性称为只读属性(read-only property)。只具有set访问器的属性称为只写属性(write-only property)。get访问器相当于一个具有属性类型返回值的无参数方法。除了作为赋值的目标外,当在表达式中引用属性时,会调用该属性的get访问器以计算该属性的值。set访问器相当于一个具有单个名为value的参数和无返回类型的方法。当一个属性作为赋值的目标,或者作为++或--运算符的操作数被引用时,就会调用set访问器,所传递的自变量将提供新值。List类声明了两个属性Count和Capacity,依次是只读和只写的。下面是使用这些属性的示例:List names = new List();names.Capacity = 100; //调用set访问器int i = names.Count; //调用get访问器int j = names.Capacity; //调用get访问器与字段和方法类似,对于实例属性和静态属性,C#两者都支持。静态属性是声明中具有static修饰符,而实例属性则没有。属性的访问器可以是虚拟的。当属性声明中包含virtual,abstract,override修饰符时,它们将运用到属性访问器。1.6.6.3 索引器索引器是这样一个成员:它使对象能够用与数组相同的方式进行索引。索引器的声明与属性很相似,不同之处在于成员的名字是this,后面的参数列表是在定界符([])之间。参数在索引器的访问器中是可用的。与属性类似,索引器可以是读写、只读、只写的,并且索引器的访问器也可以是虚拟的。List类声明了单个读写索引器,接受一个int型的参数。通过索引器就可能用int值索引List实例。例如:List names = new List();names.Add("Liz");names.Add("Martha");names.Add("Beth");for (int i = 0; i < names.Count; i++) string s = (string) names[i]; names[i] = s.ToUpper();索引器能够被重载,意味着可以声明多个索引器,只要它们的参数个数或类型不同。1.6.6.4 事件事件是使对象或类能够提供通知的成员。事件的声明与字段的类似,不同之处在于事件声明包含一个event关键字,并且事件声明的类型必须是委托类型。在包含事件声明的类中,事件可以像委托类型的字段一样使用(这样的事件不能是 abstract,而且不能声明访问器)。该字段保存了一个委托的引用,表示事件处理程序已经被添加到事件上。如果尚未添加任何事件处理程序,则该字段为null。List类声明了名为Changed的单个事件成员,Changed事件表明有一个新项添加到事件处理程序列表,它由OnChanged虚拟方法引发,它首先检查事件是否为null(意思是没有事件处理程序)。引发事件的通知正好等价于调用事件所表示的委托——因此,不需要特殊的语言构件引发事件。客户通过事件处理程序(event handler)响应事件。使用“+=”运算符添加或者使用“-=”移除事件处理程序。下面的示例添加一个事件处理程序到List类的Changed事件:using System; class Test static int changeCount; static void ListChanged(object sender, EventArgs e) changCount++; static void Main() List names = new List(); names.Changed += new EventHandler(ListChanged); names.Add("Liz"); names.Add("Martha"); names.Add("Beth"); Console.WriteLine(changeCount); //输出 "3" 对于要求控制事件的底层存储的更高级场景译注8,事件的声明可以显式地提供add和remove访问器,它们在某种程度上类似于属性的set访问器。1.6.6.5 运算符运算符(operator)是一种函数成员,用来定义可应用于类实例的特定表达式运算符的含义。有三种运算符能够被定义:一元运算符、二元运算符和转换运算符。所有的运算符必须声明为public和static。List类声明了两个运算符,运算符 “==”和运算符 “!=”,并且向表达式赋予新的含义,而这些表达式将这些运算符应用到List实例上。特别指出,这些运算符定义了两个List对象的相等比较,即使用它们的Equals方法进行比较。下面的示例使用“==”运算符比较两个List实例。using System;class Test static void Main() List a = new List(); a.Add(1); a.Add(2); List b = new List(); b.Add(1); b.Add(2); Console.WriteLine(a == b); //输出 "True" b.Add(3); Console.WriteLine(a == b); //输出 "False" 第一个Console.WriteLine输出True,原因是两个List集合对象包含个数和值都相同的对象。假如List没有定义运算符 “==”,那么第一个Console.WriteLine将输出False,因为a和b引用不同的List实例。1.6.6.6 析构函数析构函数(destructor)是用于实现析构类实例所需操作的成员。析构函数不能带参数,不能具有可访问性修饰符,也不能被显式地调用。垃圾回收期间会自动调用所涉及实例的析构函数。垃圾回收器在决定何时回收对象和运行析构函数方面采取宽松的策略。特别指出,析构函数的调用时机是不确定的,并且析构函数可能运行在任何线程上。由于这些或者其他原因,只有没有其他可行的解决方案,类才实现析构函数。
DDD领域驱动设计实战-深入理解实体
1 前言
实体是领域模型中的领域对象。
官方解释:实体是指描述了领域中唯一的且可持续变化的抽象模型。通常建模时:
- 名词用于给概念命名
- 形容词用于描述这些概念
- 而动词则表示可以完成的操作
白话解释:实体就是对象的方法和属性实现业务逻辑的类,一般由唯一标识id和值对象组成,属性发生改变,可影响类的状态和逻辑。
MVC开发人员总将关注点放在数据,而非领域。因为在软件开发中,DB占据主导地位。他们首先考虑数据的属性(即表的字段)和关联关系(外键关联),而非富有行为的领域概念。这就导致数据模型直接反映在对象模型,那些表示领域模型的实体(Entity)被包含了大量getter/setter。虽然在实体模型中加入getter/setter并非大错, 但这不是DDD的做法。
2 为什么使用实体
当我们需要考虑一个对象的个性特征或需要区分不同对象时,就引入实体。一个实体是个唯一的东西,可在一段时间内持续变化。这些对象重要的不是属性,而是其延续性和标识,会跨越甚至超出软件的生命周期。
也正是 唯一身份标识和可变性(mutability) 的特征,将实体对象区别于值对象。
实体建模没那么容易。有时一个领域概念应该建模成值对象,而非实体。这意味着DDD开发CRUD系统可能更适用。但由于只从数据出发,CRUD系统是不可能创建出好的业务模型的。使用DDD,我们会将数据模型转变为实体模型。
通过标识区分对象,而非属性
应将标识作为主要的模型定义。同时保持简单的类定义,关注对象在生命周期中的连续性和唯一标识性。不应通过对象的状态形式和历史,来区分不同实体。对于什么是相同的东西,模型应该给出定义。
那么如何正确使用和设计实体呢?
3 唯一标识
实体设计早期:
- 关注能体现实体身份唯一性的主要属性和行为及如何查询实体
- 忽略次要的属性和行为
设计实体时,首先考虑实体的本质特征,特别是实体的唯一标识和对实体的查找,而不是一开始便关注实体的属性和行为。只有在对实体的本质特征有用的情况下,才加入相应属性和行为。
找到多种能够实现唯一标识性的方式,同时考虑如何在实体生命周期内维持唯一性。
实体的唯一标识不见得一定有助对实体的查找和匹配。将唯一标识用于实体匹配通常取决于标识的可读性。
比如:
- 若系统提供根据人名查找功能,但此时一个Person实体的唯一标识可能不是人名,因为重名情况很多
- 若某系统提供根据公司税号的查找功能,税号便可作为Company实体的唯一标识
值对象可用于存放实体的唯一标识。值对象是不变(immutable)的,这就保证了实体身份的稳定性,并且与身份标识相关的行为也可得到集中处理。便可避免将身份标识相关的行为泄漏到模型的其他部分或客户端中去。
3.1 创建实体身份标识的策略
每种技术方案都存在副作用。比如将关系型DB用于对象持久化时,这样的副作用将泄漏到领域模型。创建前需考虑标识生成的时间、关系型数据的引用标识和ORM在标识创建过程中的作用等,还会考虑如何保证唯一标识的稳定性。
3.2 标识稳定性
绝大多数场景不应修改实体的唯一标识,可在实体的整个生命周期中保持标识的稳定性。
可通过一些简单措施确保实体标识不被修改。可将标识的setter方法向用户隐藏。也可在setter方法种添加逻辑以确保标识在已存在时不再更新,比如可使用一些断言:
username
属性是User
实体的领域标识,该属性只能进行一次修改,并且只能在User对象内修改。setter方法setUsername
实现了自封装性且对客户端不可见。当实体的public方法自委派给该setter方法时,该方法将检查username
属性,看是否已被赋值。若是,表明该User对象的领域标识已经存在,程序将抛异常。
这个setter方法并不会阻碍Hibernate重建对象,因对象在创建时,它的属性都是使用默认值,且采用无参构造器,因此username
属性的初始值为null。然后,Hibernate将调用setter方法,由于username属性此时为null,该 setter方法得以正确地执行,username属性也将被赋予正确的标识值。
4 实体的形态
4.1 业务形态
战略设计时,实体是领域模型的一个重要对象。领域模型中的实体是多个属性、操作或行为的载体。
事件风暴中,可根据命令、操作或者事件,找出产生这些行为的业务实体对象,进而按业务规则将依存度高和业务关联紧密的多个实体对象和值对象进行聚类,形成聚合。
实体和值对象是组成领域模型的基础单元。
4.2 代码形态
即实体类,包含实体的属性和方法,通过这些方法实现实体自身的业务逻辑。
采用充血模型:
- 该实体相关的所有业务逻辑都在实体类的方法中实现
- 跨多个实体的领域逻辑则在领域服务中实现
4.3 运行形态
实体以DO(领域对象)形式存在,每个实体对象都有唯一ID。可对实体做多次修改,所以一个实体对象可能和它之前状态存在较大差异。但它们拥有相同身份标识(identity),所以始终是同一实体。
比如商品是商品上下文的一个实体,通过唯一的商品ID标识,不管这商品的数据(比如价格)如何变,商品ID不会变,始终是同一商品。
4.4 数据库形态
DDD先构建领域模型,针对业务场景构建实体对象和行为,再将实体对象映射到数据持久化对象。
在领域模型映射到数据模型时,一个实体可能对应0、1或多个数据库持久化对象。大多数情况下实体与持久化对象是一对一。在某些场景中,有些实体只是暂驻静态内存的一个运行态实体,无需持久化。比如,基于多个价格配置数据计算后生成的折扣实体。
有些复杂场景,实体与持久化对象可能是一对多或多对一:
- 一对多
用户user与角色role两个持久化对象可生成权限实体,一个实体对应两个持久化对象 - 多对一
有时为避免DB的联表查询,会将客户信息customer和账户信息account两类数据保存至同一张数据库表,客户和账户两个实体可根据需要从一个持久化对象中生成
实体本质的探索
一开始在Java代码中建模大量实体关系。将太多关注点放在数据库、表、列和对象映射上。导致所创建的模型实际上只是含有大量getter/setter的贫血领域模型。
我们应该在DDD 上有更多的思考。
如果我们认为对象就是一组命名的类和在类上定义的操作,除此之外并不包含其他内容,那就错了。
如果一些特定的领域场景会在今后继续使用,这时可以用一个轻量的文档将它们记录下来。简单形式的通用语言可以是一组术语和一些简单的用例场景。 但是,如果我们就此认为通用语言只包含术语和用例场景,那么我们又错了。在最后,通用语言应该直接反映在代码中,而要保持设计文档的实时更新是非常困难的,甚至是不可能的。
5 创建实体
新建一个实体时,我们总期望通过构造器就能初始化足够多的实体状态,因为这样更容易通过各种条件查找到该实体。
在使用及早生成唯一标识的策略时,构造器至少需接受唯一标识参数。若还有可能通过其他方式查找实体,比如名字或描述信息,那应该将这些参数一并传给构造器。
有时一个实体维护一或多个不变条件(Invariant,在整个实体生命周期中都必须保持事务一致性的一种状态) 。
如果实体的不变条件要求该实体所包含的对象都不能为null或必须由其他状态计算所得,那么这些状态也需作为参数传递给构造器。
public class User extends Entity
...
// 每一个User对象都必须包含tenantld、username, password和person属性。
// 即在User对象得到正确实例化后,这些属性不能为null
// 由User对象的构造器和实例变量对应的setter方法保证这点
protected User (Tenantld aTenantld
String aUsername,
String aPassword,
Person aPerson) (
this();
this.setPassword(aPassword);
this.setPerson(aPerson);
this.setTenantld(aTenantld);
this.setUsername(aUsername);
this.initialize();
...
protected void setPassword(String aPassword)
if (aPassword == null)
throw new 11legalArgumentException(
"The password may not be set to null.");
)
this.password = aPassword;
)
protected void setPerson(Person aPerson) (
if (aPerson == null) (
throw new IllegalArgumentException("The person may not be set to null.");
this.person = aPerson;
protected void setTenantld(Tenantld aTenantld) (
if (aTenantld == null)
throw new IllegalArgumentException("The tenantld may not be set to null.");
this.tenantld = aTenantld;
protected void setUsername(String aUsername) (
if (this.username != null)
throw new IIlegalStateException("The username may not be changed.n);
if (aUsername == null)
throw new IllegalArgumentException("The username may not be set to null.");
this.username = aUsername;
User对象展示了一种自封装性。在构造器对实例变量赋值时,把操作委派给实例变量对应的setter方法,便保证了实例变量的自封装性。实例变量的自封装性使用setter方法来决定何时给实例变量赋值。
每个setter方法都“代表着实体”对所传进的参数做非null检查,这里的断言称为守卫(Guard)。setter方法的自封装性技术可能会变得非常复杂。所以对于复杂的创建实体场景,可使用工厂。
User对象的构造函数被声明为 protected
。 Tenant实体即为User实体的工厂也是同一个模块中唯一能够访问User 构造器的类。这样一来,只有Tenant能够创建User实例。
public class Tenant extends Entity
// 该工厂简化对User的创建,同时保证了Tenantld在User和Person对象中的正确性
// 该工厂能够反映通用语言。
public User registerUser(String aUsername,
String aPassword,
Person aPerson)
aPerson.setTenantld(this.tenantld());
User user = new User(this.tenantld(), aUsername, aPassword, aPerson);
return user;
以上是关于实体类的理解的主要内容,如果未能解决你的问题,请参考以下文章