DDD多聚合根共享值对象的可行性与结构设计问询
Absolutely—sharing value objects across aggregate roots is not just allowed, it’s one of their core design strengths. Value objects are defined by their attribute values (not a unique identity), so they’re inherently reusable across your domain model. Let’s break down why you might have seen per-aggregate Address examples, and how to structure your code to avoid duplicate validation logic.
First: Why Do Some Examples Create Per-Aggregate Value Objects?
You might have seen cases where teams create separate CustomerAddress, DoctorAddress, etc., but that’s usually only necessary if the address has domain-specific rules that differ between aggregates. For example:
- A
Doctor’s clinic address might require a valid medical license number associated with it - A
Customer’s home address might need to validate against residential-only zip codes
If your Address value object has universal validation logic (e.g., valid zip code format, non-empty street/city) that applies to all three aggregates, there’s zero reason to duplicate that code.
How to Structure Shared Value Objects
Here’s a practical approach to reuse your Email, Phone, and Address value objects across Customer, Doctor, and Receptionist:
1. Place Shared Value Objects in a Core Domain Layer
Create a dedicated module/package (e.g., Domain.Core.ValueObjects or Shared.Domain.ValueObjects) to house your general-purpose value objects. All aggregate root modules will depend on this core layer.
Example Code (C#):
// Core layer: Shared value object with validation namespace Domain.Core.ValueObjects; public record Address(string Street, string City, string ZipCode) { // Constructor validation runs once, reused everywhere public Address { if (string.IsNullOrWhiteSpace(Street)) throw new ArgumentException("Street cannot be empty or whitespace.", nameof(Street)); if (string.IsNullOrWhiteSpace(City)) throw new ArgumentException("City cannot be empty or whitespace.", nameof(City)); if (!IsValidZipCode(ZipCode)) throw new ArgumentException("Invalid zip code format.", nameof(ZipCode)); } private bool IsValidZipCode(string zipCode) { // Your zip code validation logic here return !string.IsNullOrWhiteSpace(zipCode) && zipCode.Length == 5; } }
Now reference this in your aggregate roots:
// Customer aggregate root namespace Domain.Customers; public class Customer { public Guid Id { get; private set; } public Address HomeAddress { get; private set; } public Email ContactEmail { get; private set; } public Customer(Guid id, Address homeAddress, Email contactEmail) { Id = id; HomeAddress = homeAddress ?? throw new ArgumentNullException(nameof(homeAddress)); ContactEmail = contactEmail ?? throw new ArgumentNullException(nameof(contactEmail)); } } // Doctor aggregate root namespace Domain.Doctors; public class Doctor { public Guid Id { get; private set; } public Address ClinicAddress { get; private set; } public Phone OfficePhone { get; private set; } public Doctor(Guid id, Address clinicAddress, Phone officePhone) { Id = id; ClinicAddress = clinicAddress ?? throw new ArgumentNullException(nameof(clinicAddress)); OfficePhone = officePhone ?? throw new ArgumentNullException(nameof(officePhone)); } }
2. Handle Aggregate-Specific Variations (If Needed)
If one aggregate needs a modified version of a value object (e.g., a doctor’s address needs a clinic license number), don’t modify the shared value object. Instead:
- Use composition: Wrap the shared
Addressin a new aggregate-specific value object - Or use inheritance (if your language supports it, though composition is often preferred for flexibility)
Composition Example:
namespace Domain.Doctors.ValueObjects; public record ClinicAddress { public Address BaseAddress { get; } public string ClinicLicenseNumber { get; } public ClinicAddress(Address baseAddress, string clinicLicenseNumber) { BaseAddress = baseAddress ?? throw new ArgumentNullException(nameof(baseAddress)); if (string.IsNullOrWhiteSpace(clinicLicenseNumber)) throw new ArgumentException("Clinic license number is required.", nameof(clinicLicenseNumber)); } }
This way, you still reuse the validation logic in the core Address while adding aggregate-specific rules.
3. Enforce Immutability
Make sure your value objects are immutable (e.g., using C# records, or private setters in classes). This ensures that when you share them across aggregates, changes to one won’t affect another—critical for maintaining domain consistency.
Key Takeaway
Reusing value objects across aggregates is a best practice in DDD. It eliminates duplicate code, keeps validation logic centralized, and aligns with the value object’s purpose as a reusable, value-focused domain construct. Only create aggregate-specific value objects when the domain rules truly differ between contexts.
内容的提问来源于stack exchange,提问作者manuc

