C# - 值对象模式,数据传输对象模式
VO 和 DTO 模式的入门教程,附带示例
1. 值对象模式 - 定义
- 通常,在 C# 中谈论“值对象”(VO) 时,我们想到的是一个小型对象,其主要目的是保存数据并具有“值语义”。这意味着相等性和赋值是基于值(与基于标识/引用相反)。
- 值对象背后的主要思想是使对象基于值而不是标识(引用)进行比较。
- 值对象通常没有任何行为,除了存储、检索、相等性比较和赋值。
- 值对象通常存在于应用程序的核心中,并在业务逻辑中发挥重要作用。
- 值对象经常被设置为不可变的。
- 相关模式:不可变对象模式
2. VO 模式中的 C# 结构体
C# struct
已经具有**值语义**,但需要重写运算符 ==
和 !=
以及 Hash
函数。 这是 VO 模式中 C# struct
的一个示例。
public struct CarStruct
{
public CarStruct(Char? brand, Char? model, int? year)
{
Brand = brand;
Model = model;
Year = year;
}
public Char? Brand { get; set; }
public Char? Model { get; set; }
public int? Year { get; set; }
public override string ToString()
{
return $"Brand:{Brand}, Model:{Model}, Year:{Year}";
}
public static bool operator ==(CarStruct? b1, CarStruct? b2)
{
if (b1 is null)
return b2 is null;
return b1.Equals(b2);
}
public static bool operator !=(CarStruct? b1, CarStruct? b2)
{
return !(b1 == b2);
}
public override int GetHashCode()
{
return (Brand, Model, Year).GetHashCode();
}
}
//=============================================
//===Sample code===============================
//--assigning struct based objects
Console.WriteLine("-----");
Console.WriteLine("Assignment of Value Object - Struct");
CarStruct car7 = new CarStruct('T', 'C', 1991);
CarStruct car8 = car7;
Console.WriteLine($"Value object, car7={car7}");
Console.WriteLine($"Value object, car8={car8}");
string? address7 = Util.GetMemoryAddressOfStruct(ref car7);
string? address8 = Util.GetMemoryAddressOfStruct(ref car8);
Console.WriteLine($"Address car7={address7}, Address car8={address8}");
Console.WriteLine();
Console.WriteLine("Equality of Value Object - Struct");
CarStruct car5 = new CarStruct('T', 'C', 1991);
CarStruct car6 = new CarStruct('T', 'C', 1991);
Console.WriteLine($"Value object, car5={car5}");
Console.WriteLine($"Value object, car6={car6}");
bool equal56 = car5 == car6;
Console.WriteLine($"Value of car5==car6:{equal56}");
Console.WriteLine();
Console.ReadLine();
//=============================================
//===Result of execution=======================
/*
-----
Assignment of Value Object - Struct
Value object, car7=Brand:T, Model:C, Year:1991
Value object, car8=Brand:T, Model:C, Year:1991
Address car7=0x5AB657E4D0, Address car8=0x5AB657E4C0
Equality of Value Object - Struct
Value object, car5=Brand:T, Model:C, Year:1991
Value object, car6=Brand:T, Model:C, Year:1991
Value of car5==car6:True
*/
3. VO 模式中的 C# 类
C# 类具有引用语义,因此需要重写 Equality 运算符以及 ==
和 !=
运算符以及 Hash
函数。 问题是“赋值运算符 =”无法在 C# 中重载,因此我们将创建一个复制构造函数来代替。 这是 VO 模式中 C# 类的一个示例
public class CarClass
{
public CarClass(Char? brand, Char? model, int? year)
{
Brand = brand;
Model = model;
Year = year;
}
public CarClass(CarClass original)
{
Brand = original.Brand;
Model = original.Model;
Year = original.Year;
}
public Char? Brand { get; set; }
public Char? Model { get; set; }
public int? Year { get; set; }
public override string ToString()
{
return $"Brand:{Brand}, Model:{Model}, Year:{Year}";
}
public static bool operator ==(CarClass? b1, CarClass? b2)
{
if (b1 is null)
return b2 is null;
return b1.Equals(b2);
}
public static bool operator !=(CarClass? b1, CarClass? b2)
{
return !(b1 == b2);
}
public override bool Equals(object? obj)
{
if (obj == null)
return false;
return obj is CarClass b2 ? (Brand == b2.Brand &&
Model == b2.Model &&
Year == b2.Year) : false;
}
public override int GetHashCode()
{
return (Brand, Model, Year).GetHashCode();
}
}
//=============================================
//===Sample code===============================
//--assigning class based objects
Console.WriteLine("-----");
Console.WriteLine("Assignment of Value Object - Class");
CarClass car7 = new CarClass('T', 'C', 1991);
CarClass car8 = new CarClass(car7);
Console.WriteLine($"Value object, car7={car7}");
Console.WriteLine($"Value object, car8={car8}");
Tuple<string?, string?> addresses1 =Util.GetMemoryAddressOfClass(car7, car8);
Console.WriteLine($"Address car7={addresses1.Item1}, Address car8={addresses1.Item2}");
Console.WriteLine();
Console.WriteLine("Equality of Value Object - Class");
CarClass car5 = new CarClass('T', 'C', 1991);
CarClass car6 = new CarClass('T', 'C', 1991);
Console.WriteLine($"Value object, car5={car5}");
Console.WriteLine($"Value object, car6={car6}");
bool equal56 = car5 == car6;
Console.WriteLine($"Value of car5==car6:{equal56}");
Console.WriteLine();
Console.ReadLine();
//=============================================
//===Result of execution=======================
/*
-----
Assignment of Value Object - Class
Value object, car7=Brand:T, Model:C, Year:1991
Value object, car8=Brand:T, Model:C, Year:1991
Address car7=0x1ED375614E0, Address car8=0x1ED37561500
Equality of Value Object - Class
Value object, car5=Brand:T, Model:C, Year:1991
Value object, car6=Brand:T, Model:C, Year:1991
Value of car5==car6:True
*/
4. 数据传输对象模式 - 定义
- 通常,在 C# 中谈论“数据传输对象”(DTO) 时,我们想到的是一个对象,其主要目的是充当要传输的数据的容器。
- 数据传输对象背后的主要思想是促进/简化系统各层/边界之间的数据传输。 通常,它们通过聚合原本需要在多次调用中传输的数据来实现这一点。
- 数据传输对象通常没有任何行为,除了存储、检索、序列化和反序列化。
- 数据传输对象通常存在于层/系统的边界上。
- 数据传输对象通常不是不可变的,因为它们不会从不变性中受益。
- 相关模式:外观模式。 这是因为 DTO 经常聚合需要传输的多个对象的部分。
5. DTO 模式中的 C# 结构体/类
通常,层/系统边界之间的通信是一个耗时的操作。 因此,减少边界或层之间的调用次数非常重要。 这就是使用 DTO 对象的原因,该对象聚合属于多个对象的数据,否则需要在多次调用中传输。 这是一个 DTO 对象的示例。
public class Person
{
public Person(int id, String name, String nationality, int age)
{
Id =id;
Name =name;
Nationality =nationality;
Age =age;
}
public int Id { get; set; }
public String Name { get; set; }
public String Nationality { get; set;}
public int Age { get; set;}
}
public class Address
{
public Address(int id, String street, String city)
{
Id=id;
Street=street;
City=city;
}
public int Id { get; set; }
public string Street { get; set; }
public string City { get; set; }
}
public class Position
{
public Position(int id, String title, int salary)
{
Id=id; Title=title;Salary=salary;
}
public int Id { get; set; }
public String Title { get; set; }
public int Salary { get; set;}
}
public class EmployeeDto
{
public EmployeeDto()
{}
public EmployeeDto(Person person, Address address, Position position)
{
Name = person.Name;
City = address.City;
Title = position.Title;
}
public string? Name { get; set; }
public string? City { get; set; }
public string? Title { get; set; }
public override string ToString()
{
return $"Name:{Name}, City:{City}, Title:{Title}";
}
public byte[]? Serialize()
{
byte[]? result = null;
string json = JsonSerializer.Serialize(this);
result=System.Text.Encoding.Unicode.GetBytes(json);
return result;
}
public static EmployeeDto? Deserialize(byte[]? data)
{
EmployeeDto? result = null;
if (data != null)
{
string original2 = System.Text.Encoding.Unicode.GetString(data);
result = JsonSerializer.Deserialize<EmployeeDto>(original2);
}
return result;
}
}
//=============================================
//===Sample code===============================
Console.WriteLine("-----");
Person p = new Person(111, "Rafael", "Spanish", 36);
Address a = new Address(222, "Rafa's Way", "Majorca");
Position po = new Position(333, "Senior Programmer", 50_000);
EmployeeDto e1 = new EmployeeDto(p, a, po);
byte[]? emplyeeData1 = e1.Serialize();
//-------transfer over wire-------------
EmployeeDto? e2 = EmployeeDto.Deserialize(emplyeeData1);
Console.WriteLine($"Original DTO, e1={e1}");
Console.WriteLine($"Received DTO, e2={e2?.ToString() ?? String.Empty}");
Console.WriteLine();
Console.ReadLine();
//=============================================
//===Result of execution=======================
/*
-----
Original DTO, e1=Name:Rafael, City:Majorca, Title:Senior Programmer
Received DTO, e2=Name:Rafael, City:Majorca, Title:Senior Programmer
*/
6. 用于查找对象地址的实用程序
我们开发了一个小型实用程序,可以为我们提供相关对象的地址,因此通过比较地址,可以很容易地看出我们讨论的是相同还是不同的对象。 唯一的问题是我们的地址查找实用程序有一个限制,也就是说,它仅适用于堆上不包含堆上其他对象(引用)的对象。 因此,我们被迫仅在对象中使用原始值,这就是我需要避免使用 C# string
并仅使用 char
类型的原因。
public class Util
{
public static Tuple<string?, string?> GetMemoryAddressOfClass<T1, T2>(T1 o1, T2 o2)
where T1 : class
where T2 : class
{
//using generics to block structs, that would be boxed
//so we would get address of a boxed object, not struct
//works only for objects that do not contain references
// to other objects
string? address1 = null;
string? address2 = null;
GCHandle? handleO1 = null;
GCHandle? handleO2 = null;
if (o1 != null)
{
handleO1 = GCHandle.Alloc(o1, GCHandleType.Pinned);
}
if (o2 != null)
{
handleO2 = GCHandle.Alloc(o2, GCHandleType.Pinned);
}
if (handleO1 != null)
{
IntPtr pointer1 = handleO1.Value.AddrOfPinnedObject();
address1 = "0x" + pointer1.ToString("X");
}
if (handleO2 != null)
{
IntPtr pointer2 = handleO2.Value.AddrOfPinnedObject();
address2 = "0x" + pointer2.ToString("X");
}
if (handleO1 != null)
{
handleO1.Value.Free();
}
if (handleO2 != null)
{
handleO2.Value.Free();
}
Tuple<string?, string?> result =
new Tuple<string?, string?>(address1, address2);
return result;
}
public static unsafe string? GetMemoryAddressOfStruct<T1>(ref T1 o1)
where T1 : unmanaged
{
//In order to satisfy this constraint "unmanaged" a type must be a struct
//and all the fields of the type must be unmanaged
//using ref, so I would not get a value copy
string? result = null;
fixed (void* pointer1 = (&o1))
{
result = $"0x{(long)pointer1:X}";
}
return result;
}
}
7. 结论
值对象 (VO) 是一种相等性基于值而不是标识的对象。 数据传输对象 (DTO) 是一个用于移动数据的数据容器,其目的是简化层之间的数据传输。 这两个模式名称有时可以互换使用。
值对象 (VO) 模式和数据传输对象 (DTO) 模式,虽然不是很复杂,但经常使用,并且需要成为任何严肃的 C# 程序员的知识储备。
相关主题是“不可变对象模式”和“C# 中的记录”。
8. 参考文献
- [1] https://en.wikipedia.org/wiki/Value_object
- [2] https://en.wikipedia.org/wiki/Data_transfer_object
- [3] https://martinfowler.com.cn/eaaCatalog/dataTransferObject.html
- [4] https://martinfowler.com.cn/eaaCatalog/valueObject.html
- [5] https://medium.com/@hermesmonteiro1981/valueobject-pattern-when-to-use-identify-pattern-situation-e753292113c7
- [6] https://deviq.com/domain-driven-design/value-object
- [7] https://enterprisecraftsmanship.com/posts/csharp-records-value-objects/
- [8] https://enterprisecraftsmanship.com/posts/dto-vs-value-object-vs-poco/
- [9] https://en.wikipedia.org/wiki/Immutable_object
- [10] https://stackoverflow.com/questions/6986032/difference-between-value-object-pattern-and-data-transfer-pattern
- [11] https://blog.devgenius.io/3-different-ways-to-implement-value-object-in-csharp-10-d8f43e1fa4dc
- [12] https://en.wikipedia.org/wiki/Value_semantics
- [13] https://en.wikipedia.org/wiki/Facade_pattern
历史
- 2023年2月8日:初始版本