C# Academy · บทเรียน

Records ในการออกแบบที่ขับเคลื่อนด้วยโดเมน

จำลองออบเจ็กต์ค่า DTO และเหตุการณ์โดเมนเป็น records เพื่อบังคับใช้ความไม่เปลี่ยนแปลงและทำให้โค้ดเข้าใจได้ชัดเจนขึ้น

บทเรียน 4 จาก 412 ขั้นตอน

Records ในการออกแบบที่ขับเคลื่อนด้วยโดเมน เป็นบทเรียน C# Academy ฟรีบน CoddyKit นี่คือบทเรียนที่ 4 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน C# Academy และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส C# Academy มีบทเรียนทั้งหมด 4 บทเรียน

DDD และอ็อบเจ็กต์ค่า

ในการออกแบบที่ขับเคลื่อนด้วยโดเมน (DDD) อ็อบเจ็กต์ค่าจะถูกกำหนดด้วยพร็อพเพอร์ตี ไม่ใช่ด้วยเอกลักษณ์ ค่า Money(100, "USD") สองค่าจึงสามารถใช้แทนกันได้ เรคคอร์ดเป็นตัวแทนใน C# ที่เหมาะสมอย่างยิ่งสำหรับอ็อบเจ็กต์ค่า

อ็อบเจ็กต์ค่าในรูปแบบเรคคอร์ด

จำลองอ็อบเจ็กต์ค่าในโดเมนเป็นเรคคอร์ด ความเท่ากันตามโครงสร้าง ความไม่เปลี่ยนแปลง และไวยากรณ์ที่กระชับของเรคคอร์ดสอดคล้องกับหลักการ DDD อย่างลงตัว

// Value objects — defined by their values, not identity
public record Money(decimal Amount, string Currency);
public record Email(string Value);
public record PhoneNumber(string CountryCode, string Number);
public record Address(string Street, string City, string Country, string PostalCode);

// Two Money objects with same values are equal:
var price = new Money(99.99m, "USD");
var same  = new Money(99.99m, "USD");
Console.WriteLine(price == same); // True

การตรวจสอบในตัวสร้างอ็อบเจ็กต์ค่า

เพิ่มตัวสร้างแบบกระชับเพื่อตรวจสอบเงื่อนไขคงที่ อ็อบเจ็กต์ค่าควรสร้างได้เฉพาะในสถานะที่ถูกต้องเท่านั้น — หากสถานะไม่ถูกต้อง ควรทำให้เกิดข้อผิดพลาดขณะสร้าง

public record Email(string Value)
{
    // Compact constructor for validation
    public Email : this(Value)
    {
        if (string.IsNullOrWhiteSpace(Value) || !Value.Contains('@'))
            throw new ArgumentException("Invalid email address", nameof(Value));
        Value = Value.Trim().ToLowerInvariant();
    }
}

// Usage:
var email = new Email("  Alice@Example.COM  "); // normalized to alice@example.com
new Email("not-an-email"); // throws ArgumentException

เอนทิตีเทียบกับอ็อบเจ็กต์ค่า

เอนทิตีมีเอกลักษณ์ (ติดตามด้วย ID ตลอดช่วงเวลา) ส่วนอ็อบเจ็กต์ค่าไม่มีเอกลักษณ์ และสามารถใช้แทนกันได้หากค่าตรงกัน ควรจำลองสิ่งทั้งสองแตกต่างกันใน C#

// ENTITY: has identity, mutable state
public class Customer
{
    public Guid Id { get; } = Guid.NewGuid();
    public string Name { get; set; } = "";
    public Email Email { get; set; } = default!; // value object
}

// VALUE OBJECT: no identity, immutable
public record Email(string Value);
public record Money(decimal Amount, string Currency);

// Two customers with same name are DIFFERENT entities
// Two Money(100, "USD") are the SAME value

อ็อบเจ็กต์ค่าที่มีเมธอดของโดเมน

อ็อบเจ็กต์ค่าสามารถมีตรรกะของโดเมนในรูปเมธอดได้ การดำเนินการควรส่งคืนอ็อบเจ็กต์ค่าใหม่แทนการเปลี่ยนแปลงอ็อบเจ็กต์เดิม

public record Money(decimal Amount, string Currency)
{
    public Money Add(Money other)
    {
        if (Currency != other.Currency)
            throw new DomainException("Cannot add different currencies");
        return this with { Amount = Amount + other.Amount };
    }

    public Money Subtract(Money other) =>
        Amount >= other.Amount
            ? this with { Amount = Amount - other.Amount }
            : throw new DomainException("Insufficient funds");

    public Money ApplyDiscount(decimal percent) =>
        this with { Amount = Amount * (1 - percent / 100) };

    public static Money Zero(string currency) => new(0, currency);
}

เหตุการณ์ของโดเมนในรูปแบบเรคคอร์ด

เหตุการณ์ของโดเมนอธิบายสิ่งที่เกิดขึ้นในโดเมน เรคคอร์ดจึงเหมาะอย่างยิ่ง เพราะไม่เปลี่ยนแปลง อธิบายความหมายได้ชัดเจน และใช้ความเท่ากันตามโครงสร้าง ซึ่งทำให้การทดสอบง่ายขึ้น

// Domain events as records
public abstract record DomainEvent(DateTime OccurredAt);

public record OrderPlaced(
    Guid OrderId,
    Guid CustomerId,
    Money Total,
    DateTime OccurredAt) : DomainEvent(OccurredAt);

public record OrderShipped(
    Guid OrderId,
    string TrackingNumber,
    DateTime OccurredAt) : DomainEvent(OccurredAt);

public record PaymentReceived(
    Guid OrderId,
    Money Amount,
    DateTime OccurredAt) : DomainEvent(OccurredAt);

DTO และสัญญา API ในรูปแบบเรคคอร์ด

DTO สำหรับคำขอและการตอบกลับมีลักษณะเป็นอ็อบเจ็กต์ค่าโดยธรรมชาติ เพราะใช้ส่งข้อมูลโดยไม่มีเอกลักษณ์ เรคคอร์ดช่วยให้เขียนได้กระชับและไม่เปลี่ยนแปลง

// Request DTOs
public record CreateOrderCommand(
    Guid CustomerId,
    IReadOnlyList<OrderLineDto> Lines);

public record OrderLineDto(Guid ProductId, int Quantity);

// Response DTOs
public record OrderCreatedResponse(
    Guid OrderId,
    string Status,
    decimal Total,
    DateTime CreatedAt);

// These can be compared in tests by value:
var expected = new OrderCreatedResponse(id, "Pending", 99m, now);
Assert.Equal(expected, actual);

การจัดเก็บอ็อบเจ็กต์ค่าใน EF Core

ใช้เอนทิตีที่เป็นเจ้าของเพื่อจัดเก็บอ็อบเจ็กต์ค่าใน EF Core พร็อพเพอร์ตีของอ็อบเจ็กต์ค่าแต่ละรายการจะถูกจัดเก็บในตารางของเจ้าของ (หรือตารางแยกต่างหาก) โดยไม่มี PK แยกต่างหาก

public class Order
{
    public int Id { get; set; }
    public Money Total { get; set; } = new(0, "USD");
    public Address ShipTo { get; set; } = default!;
}

// EF Core config:
modelBuilder.Entity<Order>(e =>
{
    e.OwnsOne(o => o.Total, money =>
    {
        money.Property(m => m.Amount).HasColumnType("decimal(18,2)");
        money.Property(m => m.Currency).HasMaxLength(3);
    });
    e.OwnsOne(o => o.ShipTo);
});

รากของ Aggregate ที่ใช้อ็อบเจ็กต์ค่า

นำทุกอย่างมารวมกัน: รากของ Aggregate ของ Order ที่ใช้เรคคอร์ดสำหรับอ็อบเจ็กต์ค่าและเหตุการณ์ของโดเมน พร้อมบังคับใช้กฎทางธุรกิจทั้งหมดในเมธอด

public class Order
{
    private readonly List<DomainEvent> _events = new();
    public IReadOnlyList<DomainEvent> DomainEvents => _events;

    public Guid Id { get; } = Guid.NewGuid();
    public Address ShipTo { get; private set; } = default!; // value obj
    public Money Total { get; private set; } = Money.Zero("USD");

    public void PlaceOrder(Address shipTo, IEnumerable<OrderLine> lines)
    {
        ShipTo = shipTo;
        Total  = lines.Aggregate(Money.Zero("USD"),
            (acc, l) => acc.Add(l.Price));
        _events.Add(new OrderPlaced(Id, CustomerId, Total, DateTime.UtcNow));
    }
}

การใช้งานจริง: ID ที่ปลอดภัยตามชนิดข้อมูล

ใช้เรคคอร์ดห่อหุ้ม ID แบบพื้นฐาน เพื่อป้องกันการนำ ID ของเอนทิตีต่างชนิดมาใช้ปะปนกันโดยไม่ตั้งใจ ซึ่งเป็นเทคนิค DDD ทั่วไปที่เรียกว่า "ID แบบระบุชนิดอย่างเข้มงวด"

// Strongly typed IDs — can't confuse CustomerId with OrderId
public record CustomerId(Guid Value)
{
    public static CustomerId New() => new(Guid.NewGuid());
    public static implicit operator Guid(CustomerId id) => id.Value;
}

public record OrderId(Guid Value)
{
    public static OrderId New() => new(Guid.NewGuid());
}

// Type safety:
void ShipOrder(OrderId orderId, CustomerId customerId) { }

ShipOrder(OrderId.New(), CustomerId.New()); // OK
ShipOrder(CustomerId.New(), OrderId.New()); // COMPILE ERROR!

ตรวจสอบความเข้าใจ

อะไรเป็นตัวกำหนดอ็อบเจ็กต์ค่าใน DDD และเหตุใดเรคคอร์ดจึงเหมาะกับสิ่งนี้โดยธรรมชาติ

สรุป: เรคคอร์ดในการออกแบบที่ขับเคลื่อนด้วยโดเมน

ประเด็นสำคัญ:

  • อ็อบเจ็กต์ค่า: กำหนดด้วยค่า ไม่มีเอกลักษณ์ — ใช้เรคคอร์ด
  • เอนทิตี: มีเอกลักษณ์ (ID) และสถานะที่เปลี่ยนแปลงได้ — ใช้คลาส
  • ตรวจสอบเงื่อนไขคงที่ในตัวสร้างแบบกระชับ — สถานะที่ไม่ถูกต้องไม่สามารถแทนค่าได้
  • เหตุการณ์ของโดเมนในรูปแบบเรคคอร์ด: ไม่เปลี่ยนแปลง เปรียบเทียบได้ และอธิบายตัวเอง
  • ใช้เอนทิตีที่เป็นเจ้าของของ EF Core เพื่อจัดเก็บอ็อบเจ็กต์ค่า
  • ID แบบระบุชนิดอย่างเข้มงวดด้วยเรคคอร์ดช่วยป้องกันการนำ ID ของเอนทิตีมาใช้ปะปนกัน
เริ่มต้นได้ฟรี

เรียนรู้ C# ด้วย AI tutor — ฟรี

เขียนและเรียกใช้โค้ดจริงในเบราว์เซอร์ของคุณ รับความช่วยเหลือทันทีจาก AI tutor 24/7 และเรียนรู้ต่อจากที่คุณหยุดบนเว็บหรือในแอป

คอร์ส
93
บทเรียน
346

คำถามที่พบบ่อย

บทเรียน “Records ในการออกแบบที่ขับเคลื่อนด้วยโดเมน” ฟรีหรือไม่

ใช่ — ข้อความเต็มของ “Records ในการออกแบบที่ขับเคลื่อนด้วยโดเมน” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส C# Academy ให้อัปเกรดเป็น CoddyKit PRO คอร์ส C# Academy มีบทเรียนทั้งหมด 4 บทเรียน

คุณจะเรียนรู้อะไรในบทเรียน “Records ในการออกแบบที่ขับเคลื่อนด้วยโดเมน”

จำลองออบเจ็กต์ค่า DTO และเหตุการณ์โดเมนเป็น records เพื่อบังคับใช้ความไม่เปลี่ยนแปลงและทำให้โค้ดเข้าใจได้ชัดเจนขึ้น คุณปฏิบัติ C# Academy ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน

คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน C# Academy หรือไม่

ไม่จำเป็นต้องมีประสบการณ์มาก่อน C# Academy บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 4 จากทั้งหมด 4 บทเรียน

บทเรียน “Records ในการออกแบบที่ขับเคลื่อนด้วยโดเมน” ใช้เวลานานแค่ไหน

บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย

ฉันเขียนและรันโค้ดในบทเรียน C# Academy นี้ได้ไหม

ได้ บทเรียน C# Academy ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ

บทเรียนทั้งหมดในหลักสูตรนี้

  1. ชนิด Record: พื้นฐานและไวยากรณ์
  2. ความไม่เปลี่ยนแปลงด้วย init และ with
  3. ความเท่ากันของค่าและการแยกโครงสร้าง
  4. Records ในการออกแบบที่ขับเคลื่อนด้วยโดเมน
← กลับไปที่ C# Academy