zoukankan      html  css  js  c++  java
  • Spring Boot+JPA实现DDD(四)

    优化Entity,类型改为值对象

    前面我们已经定义了2个聚合根,定义了2个聚合根之间的关系,并且自动生成了表结构。
    在实现具体的业务前,优化一下我们的Entity。

    @Column(name = "product_no", length = 32, nullable = false, unique = true)
    private String productNo;
    @Column(name = "name", length = 64, nullable = false)
    private String name;
    @Column(name = "price", precision = 10, scale = 2)
    private BigDecimal price;
    @Column(name = "category_id", nullable = false)
    private Integer categoryId;
    @Column(name = "product_status", nullable = false)
    private Integer productStatus;
    

    咦?是不是有点眼熟?跟之前三层架构写的entity类有啥区别?没有区别,因为都是一些简单的字段跟DB对应一下就完事了。
    这正是我们需要优化的地方,在实现DDD的时候我们应该尽量多使用值对象

    • 比如productNo这个字段,生成商品码这个方法放在哪里比较合适?放在Product里?
    • 比如price这个字段,假如我们希望加一个币种字段怎么办? 直接再加一个@Column
    • 比如productStatus这个字段,它应该是一个枚举对不对?定义成Integer类型我们看代码根本就不知道这个数字代表什么对不对?

    把它们定义成值对象问题就迎刃而解了。解决问题的同时还收获了额外的好处:
    我们的代码更加OO(面向对象)了。Entity类不再是一个简单的ORM类了,它是一个真正的模型对象了。

    生成商品编码的方法放在ProductNumber里再适合不过了。
    ①新建domain.model.product.ProductNumber

    @Getter
    @EqualsAndHashCode
    @NoArgsConstructor(access = AccessLevel.PROTECTED)
    @AllArgsConstructor(access = AccessLevel.PRIVATE)
    public class ProductNumber implements Serializable {
        private String value;
    
        public static ProductNumber of(Integer categoryId) {
            checkArgument(categoryId != null, "商品类目不能为空");
            checkArgument(categoryId > 0, "商品类目id不能小于0");
            return new ProductNumber(generateProductNo(categoryId));
        }
    
        public static ProductNumber of(String value) {
            checkArgument(!StringUtils.isEmpty(value), "商品编码不能为空");
            return new ProductNumber(value);
        }
    
        private static String generateProductNo(Integer categoryId) {
            String prefix = "PRODUCT";
            String typeStr = String.format("%04d", categoryId);
            SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmssSSS");
            String currentTime = sdf.format(new Date());
            int randomNum = (int) (Math.random() * 9999 + 1);
            String randomNumStr = String.format("%04d", randomNum);
            return prefix + typeStr + currentTime + randomNumStr;
        }
    }
    

    四个注意点(非常重要):

    • 商品编码是业务主键,它应该是用户可读的,并且本身包含了一些有用信息。
      我们定义商品码的生成规则为:PRODUCT + 4位类目 + 当前时间 + 4位随机数 共32位。

    • 检查参数的时候,我们全部使用guava包的checkArgument方法,而不是checkNotNull方法。因为我们这是业务代码,不能把空指针异常返回给客户端。
      我们要提供用户可读的错误信息。

    • 值对象是不可修改的,是不可修改的,是不可修改的。只提供getter就行了

    • 值对象的equalshashCode方法,与实体有唯一标识不同,值对象没有唯一标识,两个值对象所有的属性值相等才能判定相等。

    然后将private String productNo; 替换成 private ProductNumber productNo;

    ②新建domain.model.product.ProductStatusEnum:

    @AllArgsConstructor
    public enum ProductStatusEnum {
        // 新建
        DRAFTED(1000111, "草稿"),
        // 待审核
        AUDIT_PENDING(1000112, "待审核"),
        // 已上架
        LISTED(1000113, "已上架"),
        // 已下架
        UNLISTED(1000114, "已下架"),
        // 已失效
        EXPIRED(1000115, "已失效");
    
        @Getter
        // @JsonValue
        private Integer code;
    
        @Getter
        private String remark;
    
        public static ProductStatusEnum of(Integer code) {
            ProductStatusEnum[] values = ProductStatusEnum.values();
            for (ProductStatusEnum val : values) {
                if (val.getCode().equals(code)) {
                    return val;
                }
            }
            // throw new InvalidParameterException(String.format("【%s】无效的产品状态", code));
            return null;
        }
    }
    

    为什么是枚举而不是字典?
    个人觉得符合以下特征才应该使用字典,否则就应该用枚举:

    • 子项可动态修改,而且修改比较频繁
    • 修改子项不影响现有业务逻辑,也就是说代码不用动

    像商品状态这种字段,每个状态都很业务密切相关。如果你把它放在字典里,只在字典里新加了一个状态没有用,因为代码里还得修改相关业务逻辑。

    private Integer productStatus;替换成private ProductStatusEnum productStatus;

    调整一下of工厂方法:

    public static Product of(String productNo, String name, BigDecimal price, Integer categoryId, Integer productStatus, String remark, 
                                               Boolean allowAcrossCategory, Set<ProductCourseItem> productCourseItems) {
        ProductNumber newProductNo = ProductNumber.of(categoryId);
        ProductStatusEnum defaultProductStatus = ProductStatusEnum.DRAFTED;
        return new Product(null, newProductNo, name, price, categoryId, defaultProductStatus, remark, allowAcrossCategory, 
                                                                                              productCourseItems);
    }
    

    ③新建Price值对象
    商品和课程明细都有价格,我们可以把Price放在一个公共的地方。
    在domain下新建common.model.Price, 内容如下:

    @Embeddable
    @Getter
    @EqualsAndHashCode
    @NoArgsConstructor(access = AccessLevel.PROTECTED)
    @AllArgsConstructor(access = AccessLevel.PRIVATE)
    public class Price implements Serializable {
    
        //@Convert(converter = CurrencyConverter.class)
        @Column(name = "currency_code", length = 3)
        private Currency currency;
        @Column(name = "price", nullable = false, precision = 10, scale = 2)
        private BigDecimal value;
    
        public static Price of(String currencyCode, BigDecimal value) {
            checkArgument(!StringUtils.isEmpty(currencyCode), "币种不能为空");
            Currency currency;
            try {
                currency = Currency.getInstance(currencyCode);
            } catch (IllegalArgumentException e) {
                throw new InvalidParameterException(String.format("【%s】不是有效的币种", currencyCode));
            }
            checkArgument(value != null, "价格不能为空");
            checkArgument(value.compareTo(BigDecimal.ZERO) > 0, "价格必须大于0");
            return new Price(currency, value);
        }
    }
    

    在值对象里验证币种的有效性很合理对不对?否则每次用到币种的时候都得判断一下是否有效。一个处理业务逻辑的方法里到处都是if判断,不雅观不说,
    还影响看代码的思路。

    注意这里我故意加了一行代码:
    checkArgument(value.compareTo(BigDecimal.ZERO) > 0, "价格必须大于0");
    大家想想加在这里是否合理? 我的理解,如果你的系统所有用到价格的地方都必须是正价格,可以加这句代码。虽然大多数场景价格都是正的,
    哪儿有倒赔钱的道理? 但是保不准有些系统就是有“负价格”这个概念,那样的话就不能加这个判断了。

    Product

    @Column(name = "price", precision = 10, scale = 2)
    private BigDecimal price;
    

    替换成

    @Embedded
    private Price price;
    

    ④自定义异常
    定义一个通用的运行时异常:

    @NoArgsConstructor
    @AllArgsConstructor
    @Setter
    @Getter
    public class BusinessException extends RuntimeException {
        private String code;
        private String message;
    }
    

    具体的业务异常:

    public class InvalidParameterException extends BusinessException {
        private static final String CODE = "invalid-parameter";
    
        public InvalidParameterException(String message) {
            super(CODE, message);
        }
    
    }
    

    异常code定义成String类型,这样看到异常编码就能知道是哪种异常,如果定义成int类型,还得查表之后才能知道是哪种异常。

    CourseItem类同理,这里就不再重复了。

    demo地址: productcenter4.zip

  • 相关阅读:
    部署phpmyadmin登录不进去
    无法获取快照信息:锁定文件失败
    nginx: [emerg] BIO_new_file("/etc/nginx/ssl_key/server.crt") failed (SSL: error:02001002:syste
    nginx重启失败
    An error occurred (500 Error)
    Failed to set session cookie. Maybe you are using HTTP instead of HTTPS to access phpMyAdmin.
    clnt_create: RPC: Program not registered
    [error] 2230#2230: *84 client intended to send too large body: 1711341 bytes
    lnmp部署知乎出现403
    easyui下拉框过滤优化
  • 原文地址:https://www.cnblogs.com/ahau10/p/13518953.html
Copyright © 2011-2022 走看看