65.9K
CodeProject 正在变化。 阅读更多。
Home

C# - 值对象模式,数据传输对象模式

starIconstarIconstarIconstarIconstarIcon

5.00/5 (4投票s)

2023年2月8日

CPOL

3分钟阅读

viewsIcon

13414

downloadIcon

106

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. 参考文献

历史

  • 2023年2月8日:初始版本
© . All rights reserved.