You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

构建器应接受原始类型还是值对象?Address构建器实现问询

Address Builder: Primitives vs Value Objects & Implementation Guide

Great question—this is a common point of tension when implementing builders, especially when balancing flexibility with domain integrity. Let’s break this down clearly.

Should the Builder Accept Primitives or Value Objects?

It depends entirely on where your builder will be used:

  • For external input layers (forms, APIs): Accept primitives. When you’re dealing with raw strings/numbers from user input or external systems, primitives are easier to work with. You can handle parsing, validation, and conversion to value objects inside the builder—keeping that complexity contained instead of leaking it to callers.
  • For internal domain code: Accept value objects. If your builder is used within your domain layer where you’re already working with validated, encapsulated value objects, this enforces that only valid data enters the Address. It aligns perfectly with your existing invariant protection.

The best approach? A hybrid one. Expose methods that accept primitives for external use, and add overloads that accept value objects for internal domain code. This gives you flexibility without sacrificing integrity.

AddressBuilder Implementation Recommendations

Here’s how to build a builder that plays nicely with your existing Address class:

1. Mirror Your Address Class’s Invariants

Since your Address already has a public constructor that enforces invariants via type hints and validation, the builder must not bypass these rules. When you call build(), delegate directly to the existing constructor—this ensures all validation logic lives in one place (the Address class) instead of being duplicated.

2. Use a Fluent Interface for Readability

Method chaining makes address creation read like natural language, which improves code maintainability. For example:

(new AddressBuilder())->firstLine("123 Main St")->postcode("SW1A 1AA")->city("London")->build();

3. Handle Optional Properties Gracefully

For optional fields (like secondLine, city, country), add setter methods that can be omitted. Initialize them to null or a sensible default (e.g., a default country) in the builder.

4. Integrate Tightly with Your Factory Method

Your existing factory method that accepts an AddressBuilder should simply delegate to the builder’s build() method. This keeps the integration clean and avoids duplicate logic.

5. Validate Early (If Using Primitives)

If you’re accepting primitives, add lightweight validation in the builder’s setter methods (or in build()) to catch obvious errors early. For example, ensure the first line isn’t empty, or the postcode matches a valid format. Throw clear exceptions so callers know exactly what went wrong.

Example Implementation (PHP)

Address Class (Existing)

class Address
{
    public function __construct(
        public readonly string $firstLine,
        public readonly string $postcode,
        public readonly ?string $secondLine = null,
        public readonly ?string $city = null,
        public readonly ?string $country = "UK"
    ) {
        // Existing invariant checks
        if (empty($firstLine)) {
            throw new InvalidArgumentException("First line cannot be empty");
        }
        if (!preg_match("/^[A-Z]{1,2}[0-9][A-Z0-9]? ?[0-9][A-Z]{2}$/i", $postcode)) {
            throw new InvalidArgumentException("Invalid postcode format");
        }
    }

    // Factory method accepting builder
    public static function fromBuilder(AddressBuilder $builder): self
    {
        return $builder->build();
    }
}

AddressBuilder

class AddressBuilder
{
    private ?string $firstLine = null;
    private ?string $postcode = null;
    private ?string $secondLine = null;
    private ?string $city = null;
    private ?string $country = "UK";

    // Primitive setter for required first line
    public function firstLine(string $firstLine): self
    {
        if (empty($firstLine)) {
            throw new InvalidArgumentException("First line cannot be empty");
        }
        $this->firstLine = $firstLine;
        return $this;
    }

    // Primitive setter for required postcode
    public function postcode(string $postcode): self
    {
        $this->postcode = $postcode;
        return $this;
    }

    // Optional property setters
    public function secondLine(?string $secondLine): self
    {
        $this->secondLine = $secondLine;
        return $this;
    }

    public function city(?string $city): self
    {
        $this->city = $city;
        return $this;
    }

    // Overload: Accept a Country value object
    public function country(Country $country): self
    {
        $this->country = $country->getCode();
        return $this;
    }

    public function build(): Address
    {
        // Ensure required fields are set before building
        if ($this->firstLine === null || $this->postcode === null) {
            throw new LogicException("First line and postcode are required to create an Address");
        }

        // Delegate to Address constructor for final validation
        return new Address(
            $this->firstLine,
            $this->postcode,
            $this->secondLine,
            $this->city,
            $this->country
        );
    }
}

Usage Examples

// Build from external input (primitives)
$residentialAddress = (new AddressBuilder())
    ->firstLine("456 Oak Ave")
    ->postcode("E1 6AN")
    ->city("London")
    ->build();

// Build via factory method
$officeAddress = Address::fromBuilder(
    (new AddressBuilder())
        ->firstLine("789 Pine Rd")
        ->postcode("W1 1AB")
);

// Build with value objects (internal domain use)
$france = new Country("FR");
$frenchAddress = (new AddressBuilder())
    ->firstLine("10 Rue des Fleurs")
    ->postcode("75001")
    ->city("Paris")
    ->country($france)
    ->build();

Key Takeaway

Your builder should adapt to its context: primitives for ease of use with external input, value objects for strict domain integrity. By keeping the builder aligned with your Address class’s existing invariants, you ensure consistency no matter how you create Address instances.

内容的提问来源于stack exchange,提问作者Peppermintology

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.20 07:24:53