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

依赖第三方验证的Value Object构建难题求解

解决方案:值对象结合第三方验证库的合理实践

针对值对象(Value Object)依赖第三方验证库的问题,核心要满足两个要求:值对象保持纯不可变数据类的特性(不持有协作对象)、杜绝外部创建无效值对象,以下是几种可行方案:


方案1:静态工厂方法封装验证逻辑(推荐用于简单场景)

将第三方验证逻辑完全封装在值对象的静态工厂方法中,同时将构造函数设为私有,禁止外部直接实例化。值对象对外仅暴露接受原始类型参数的创建接口,内部自行调用验证库完成校验。

示例代码(PHP):

final class Phone {
    private string $normalizedNumber;
    private string $countryCode;

    // 私有构造函数,确保只能通过工厂方法创建
    private function __construct(string $normalizedNumber, string $countryCode) {
        $this->normalizedNumber = $normalizedNumber;
        $this->countryCode = $countryCode;
    }

    /**
     * 对外暴露的创建方法,接受原始号码和国家码
     * @throws InvalidArgumentException 号码无效时抛出
     */
    public static function create(string $rawNumber, string $countryCode): self {
        // 内部初始化第三方验证库
        $phoneUtil = \libphonenumber\PhoneNumberUtil::getInstance();
        
        try {
            $parsedNumber = $phoneUtil->parse($rawNumber, $countryCode);
            if (!$phoneUtil->isValidNumber($parsedNumber)) {
                throw new InvalidArgumentException("Invalid phone number for country code: $countryCode");
            }
            // 标准化号码格式,确保值对象内部存储统一格式
            $normalized = $phoneUtil->format($parsedNumber, \libphonenumber\PhoneNumberFormat::E164);
            return new self($normalized, $countryCode);
        } catch (\libphonenumber\NumberParseException $e) {
            throw new InvalidArgumentException("Failed to parse phone number: {$e->getMessage()}", 0, $e);
        }
    }

    // 值对象核心方法:获取值、相等性判断
    public function getNumber(): string {
        return $this->normalizedNumber;
    }

    public function equals(Phone $other): bool {
        return $this->normalizedNumber === $other->normalizedNumber 
            && $this->countryCode === $other->countryCode;
    }
}

优点:

  • 完全符合值对象定义:仅依赖原始类型,无外部协作对象依赖
  • 构造函数私有,从根源上杜绝无效实例
  • 业务代码无需关心验证细节,调用简单

缺点:

  • 验证库与值对象耦合,测试时替换验证逻辑需要修改值对象代码(可通过方案2优化)

方案2:独立工厂类+依赖注入(推荐用于需要测试或复杂场景)

将验证逻辑剥离到独立的工厂类中,通过依赖注入传入第三方验证器,同时保持值对象的构造函数私有,仅允许工厂类创建实例。

示例代码:

// 独立工厂类,负责验证和创建Phone对象
class PhoneFactory {
    private \libphonenumber\PhoneNumberUtil $phoneUtil;

    // 通过构造函数注入验证器,方便测试时替换
    public function __construct(\libphonenumber\PhoneNumberUtil $phoneUtil) {
        $this->phoneUtil = $phoneUtil;
    }

    /**
     * 创建合法的Phone对象
     * @throws InvalidArgumentException 号码无效时抛出
     */
    public function create(string $rawNumber, string $countryCode): Phone {
        try {
            $parsedNumber = $this->phoneUtil->parse($rawNumber, $countryCode);
            if (!$this->phoneUtil->isValidNumber($parsedNumber)) {
                throw new InvalidArgumentException("Invalid phone number");
            }
            $normalized = $this->phoneUtil->format($parsedNumber, \libphonenumber\PhoneNumberFormat::E164);
            // 调用Phone内部的合法创建入口
            return Phone::fromValidatedNumber($normalized, $countryCode);
        } catch (\libphonenumber\NumberParseException $e) {
            throw new InvalidArgumentException("Failed to parse phone number", 0, $e);
        }
    }
}

final class Phone {
    private string $normalizedNumber;
    private string $countryCode;

    private function __construct(string $normalizedNumber, string $countryCode) {
        $this->normalizedNumber = $normalizedNumber;
        $this->countryCode = $countryCode;
    }

    // 仅对工厂开放的创建入口(可通过命名空间或访问控制限制)
    public static function fromValidatedNumber(string $normalizedNumber, string $countryCode): self {
        // 二次校验确保传入的是合法标准化值
        if (empty($normalizedNumber) || empty($countryCode)) {
            throw new InvalidArgumentException("Normalized number and country code cannot be empty");
        }
        return new self($normalizedNumber, $countryCode);
    }

    // 其他值对象方法...
    public function getNumber(): string {
        return $this->normalizedNumber;
    }
}

优点:

  • 解耦值对象与验证库,测试时可轻松替换验证器实现
  • 依然保证值对象的纯数据特性,外部无法直接创建无效实例
  • 符合依赖注入原则,便于容器管理

缺点:

  • 多引入一个工厂类,增加了少量代码复杂度

方案3:基于验证接口的灵活创建(用于多验证策略场景)

定义统一的验证接口,值对象的静态工厂接受接口实例完成验证,但值对象本身不持有接口实例,仅在创建时临时使用。

示例代码:

// 定义验证接口
interface PhoneValidator {
    /**
     * 验证并标准化号码,失败抛出异常
     * @return string 标准化后的号码
     */
    public function validate(string $rawNumber, string $countryCode): string;
}

// 基于Google libphone的实现
class GooglePhoneValidator implements PhoneValidator {
    private \libphonenumber\PhoneNumberUtil $phoneUtil;

    public function __construct() {
        $this->phoneUtil = \libphonenumber\PhoneNumberUtil::getInstance();
    }

    public function validate(string $rawNumber, string $countryCode): string {
        try {
            $parsedNumber = $this->phoneUtil->parse($rawNumber, $countryCode);
            if (!$this->phoneUtil->isValidNumber($parsedNumber)) {
                throw new InvalidArgumentException("Invalid phone number");
            }
            return $this->phoneUtil->format($parsedNumber, \libphonenumber\PhoneNumberFormat::E164);
        } catch (\libphonenumber\NumberParseException $e) {
            throw new InvalidArgumentException("Failed to parse phone number", 0, $e);
        }
    }
}

final class Phone {
    private string $normalizedNumber;
    private string $countryCode;

    private function __construct(string $normalizedNumber, string $countryCode) {
        $this->normalizedNumber = $normalizedNumber;
        $this->countryCode = $countryCode;
    }

    /**
     * 接受验证接口,灵活切换验证策略
     */
    public static function create(string $rawNumber, string $countryCode, PhoneValidator $validator): self {
        $normalizedNumber = $validator->validate($rawNumber, $countryCode);
        return new self($normalizedNumber, $countryCode);
    }

    // 其他值对象方法...
}

优点:

  • 支持多种验证策略,可根据场景动态切换
  • 值对象依然保持纯数据特性,无长期协作对象依赖

缺点:

  • 业务代码需要传递验证器实例,增加了调用复杂度(可通过DI容器绑定默认实例优化)

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.12 02:01:31