12|代码实现(下):怎样更加“面向对象”?

你好,我是钟敬。今天咱们继续研究编码。

上节课我们学习了领域服务工厂两个模式,分别用于实现领域逻辑以及创建领域对象。今天我们考虑再增加一些面向对象的元素。

面向对象的三个特征是封装、继承和多态。其中多态我们暂时还不涉及。而上节课我们完成的添加组织功能,在封装和继承方面还不到位。

今天我们会继续开发修改组织功能,结合这个功能的开发,看看怎样运用封装和继承。

为“修改组织”功能开卡

为了完成修改组织的功能,我们再一次把产品经理老王请过来进行“开卡”,看需求的细节是否理解到位。

首先要确认的一个问题是,到底要修改组织的哪些信息。目前的Org对象一共有11个属性,我们列成一个表,和老王逐一确认。

这个表的含义是,在完成某个功能时,客户端要提供哪些数据。其中“创建”一栏是创建组织时需要哪些参数。“修改”一栏中的问号,是我们觉得在修改时需要客户提供,但需要与业务确认的。

老王轻轻一笑,眼神中的意思是“你们这些搞IT的还是不懂业务呀”,然后给出了后面四条意见。

1.如果修改“上级组织”的话,实际上是在调整组织结构。一般不作为普通的修改功能,建议增加专门的“调整组织结构”功能。

2.“组织类别”不能修改,也就是说,一个开发组不能直接变成开发中心。如果真有这种需求,需要另外创建新的开发中心,把人迁移过去,然后撤销当前的开发组。

3.“状态”不能直接改,应该通过“撤销组织”功能间接修改。

4.最终只有“负责人”和“名称”可以直接修改,建议这个功能叫做“修改组织基本信息”。

根据老王的建议,我们把表格改成了下面的样子。由于调整组织结构功能比较复杂,所以以后再考虑,这里只增加了“撤销组织”功能。

接着,老王又为撤销组织增加了2个业务规则。

初步完成程序功能

采用前两节课的方法,我们初步完成了“修改组织基本信息”和“撤销组织”功能。

修改后的OrgServcie是这样的。

package chapter12.unjuanable.application.orgmng;
// imports ...

@Service
public class OrgService {
    private final OrgBuilderFactory orgBuilderFactory;
    private final OrgRepository orgRepository;
    private final OrgHandler orgHandler;  //新增了一个领域服务依赖

    @Autowired
    public OrgService(OrgBuilderFactory orgBuilderFactory
            , OrgHandler orgHandler
            , OrgRepository orgRepository) {
        // 为依赖注入赋值 ...
    }

    @Transactional
    public OrgDto addOrg(OrgDto request,Long userId) {
        // 添加组织的功能已完成,这里省略 ...
    }

    //修改组织基本信息
    @Transactional
    public OrgDto updateOrgBasic(Long id, OrgDto request, Long userId) {
        Org org = orgRepository.findById(request.getTenant(), id)
                .orElseThrow(() -> {
                    throw new BusinessException("要修改的组织(id =" 
                                + id + "  )不存在!");
                });

        orgHandler.updateBasic(org, request.getName() 
                , request.getLeader(), userId);
                
        orgRepository.update(org);

        return buildOrgDto(org);
    }

    //取消组织
    @Transactional
    public Long cancelOrg(Long id, Long tenant, Long userId) {
        Org org = orgRepository.findById(tenant, id)
                .orElseThrow(() -> {
                    throw new BusinessException("要取消的组织(id =" 
                                + id + "  )不存在!");
                });

        orgHandler.cancel(org, userId);
        orgRepository.update(org);

        return org.getId();
    }

    private static OrgDto buildOrgDto(Org org) {
        // 将领域对象转换成DTO
    }

}

我们写了一个OrgHandler来协助完成功能。和Validator一样,这也是一个领域服务,但命名为 OrgDomainService显得有点繁琐,我们这里按XxxHandler命名,也就是Xxx处理器。

OrgHandler的代码是这样的。

package chapter12.unjuanable.domain.orgmng.org;
// imports...

@Component
public class OrgHandler {
    private final CommonValidator commonValidator;
    private final OrgNameValidator nameValidator;
    private final OrgLeaderValidator leaderValidator;
    private CancelOrgValidator cancelValidator;

    public OrgHandler(CommonValidator commonValidator
            , OrgNameValidator nameValidator
            , OrgLeaderValidator leaderValidator
            , CancelOrgValidator cancelValidator) {
        // 为依赖注入赋值...
    }

    public void updateBasic(Org org, String newName
                , Long newLeader, Long userId) {
        updateName(org, newName);
        updateLeader(org, newLeader);
        updateAuditInfo(org, userId);
    }

    public void cancel(Org org, Long userId) {
        cancelValidator.cancelledOrgShouldNotHasEmp(org.getTenant()
                        , org.getId());
        cancelValidator.OnlyEffectiveOrgCanBeCancelled(org);
        org.setStatus(OrgStatus.CANCELLED);
        updateAuditInfo(org, userId);
    }

    private void updateLeader(Org org, Long newLeader) {
        if (newLeader != null && !newLeader.equals(org.getLeader())) {
            leaderValidator.leaderShouldBeEffective(org.getTenant()
                                , newLeader);
            org.setLeader(newLeader);
        }
    }

    private void updateName(Org org, String newName) {
        if (newName != null && !newName.equals(org.getName())) {
            nameValidator.orgNameShouldNotEmpty(newName);
            nameValidator.nameShouldNotDuplicatedInSameSuperior(
                  org.getTenant(), org.getSuperior(), newName);
            org.setName(newName);
        }
    }

    private void updateAuditInfo(Org org, Long userId) {
        // 设置最后修改人和时间
    }
}

与OrgBuilder类似,这个类中的方法基本上也是先校验,再赋值,只不过这里是更改而不是新建。同时OrgHandler没有可变属性,因此可以直接注入到应用服务。

提高应用API的封装性

下面我们看看怎么提高程序的封装性。

所谓“封装”,指的是将一个模块的实现细节尽量隐藏在内部,只向外界暴露最小的可访问接口,也叫信息隐藏原则,或最小接口原则,目的是减小模块间的耦合,提高程序的可维护性。这里说的模块是广义的,一个函数、一个类、一个包乃至整个应用系统,都可以看作模块,而我们之前领域建模中说的模块模式是狭义的,专门指领域模型里的领域对象所组成的包。

先看一下系统对外提供的API。我们的系统采用RestfulAPI,以JSON作为参数格式,JSON的结构则是与OrgDto一致的。目前的DTO中有领域对象中所有11个属性,因此,不论增加还是修改,入口参数的JSON格式都是下面这样。

{
  "createdAt": "2022-10-05T06:49:39.659Z",
  "createdBy": 0,
  "id": 0,
  "lastUpdatedAt": "2022-10-05T06:49:39.659Z",
  "lastUpdatedBy": 0,
  "leader": 0,
  "name": "string",
  "orgType": "string",
  "status": "string",
  "superior": 0,
  "tenant": 0
}

我们已经知道,添加和修改操作,实际上都只是用了这些属性的一个子集。现在这样简单粗暴地把所有属性都暴露给客户端,不仅违反了最小接口原则,也容易在理解上发生混淆。

为了解决这个问题,我们要为每个功能编写单独的参数DTO,只包含必要的参数。DTO本身的修改比较简单,你可以自己试一下。这里我们重点来看一下修改后应用层的包结构。


CreateOrgRequest和UpdateOrgBasicRequest 分别是添加和修改组织的参数DTO。原来的OrgDto改名为OrgResponse,作为两个功能共同的返回参数类型。另外,这里又出现了一个包结构打横分还是打竖分的问题。

一些伙伴更喜欢打横分,也就是创建一个dto包,把EmpService和OrgService的DTO都放进去。而我们这里采用的是打竖分,也就是把service和这个service用到的DTO放在同一个包内,提高了模块的内聚性。

修改之后,添加组织的JSON参数格式成了下面这样。

{
  "leader": 0,
  "name": "string",
  "orgType": "string",
  "superior": 0,
  "tenant": 0
}

这就简洁多了,关键是缩小了API,系统的封装性提高了。修改组织功能的JSON的优化也是类似的。

提高领域对象的封装性

改进了系统API的封装之后,我们再来考虑领域对象层面。

现在的Org对象只是纯粹的DTO,所有属性都通过getter和setter暴露在外,没有任何封装性可言。要提高封装性,可以从两个角度考虑。第一是限制getter和setter的数量;第二是用表示业务含义的接口代替简单的setter和getter。

由于getter只是用来查询,不会破坏数据,而不恰当的setter则可能破坏数据,导致程序出错,所以相对而言,限制setter比限制getter更重要一些。为了限制setter,我们再列一个表,研究一下在创建了组织以后,哪些属性是可以修改的。

我们可以只为那些可以修改的属性保留setter,其他的只有getter,成为只读属性。再为Org类增加一个包含只读属性的构造器,以便创建对象。

修改后的Org类是这样的。

public class Org {
    private Long id;
    private Long tenantId;
    private Long superiorId;
    private String orgTypeCoDe;
    private Long leaderId;
    private String name;
    private OrgStatus status;
    private LocalDateTime createdAt;
    private Long createdBy;
    private LocalDateTime lastUpdatedAt;
    private Long lastUpdatedBy;

    public Org(Long tenantId, String orgTypeCoDe
          , LocalDateTime createdAt, Long createdBy) {
        // 为属性赋值 ...
    }

    // 所有属性的 getter ...

    // 保留了 superiorId, leaderId, name,
    // lastUpdateAt, lastUPdateBy 和 status 的 setter ... 
   
}

其中OrgId比较特殊,大部分情况下是只读的,但当Repository将新建的Org保存到数据库时,由于id是数据库自动生成的,需要回填id值。这可以通过反射等技巧来绕过去。事实上,很多数据库访问框架就是利用反射,直接为私有属性赋值,以便在不破坏封装的前提下,从数据库中取出对象。

通过减少领域对象的Setter,我们进一步提高了程序的封装性。

通过“表意接口”提高封装性

做了这些改进以后,我们再看看在OrgHandler中完成“撤销组织”功能的代码。

package chapter12.unjuanable.domain.orgmng.org;
// imports ...

@Component
public class OrgHandler {
    //...
    
    public void cancel(Org org, Long userId) {
        cancelValidator.OrgToBeCancelledShouldNotHasEmp(
                                    org.getTenantId(), org.getId());
        cancelValidator.OrgToBeCancelledShouldBeEffective(org);
        org.setStatus(OrgStatus.CANCELLED); // 直接为 Status 赋值
        updateAuditInfo(org, userId);
    }
    
    // ...
}

其中,org.setStatus(OrgStatus.CANCELLED) 直接将组织的状态设置成了“已撤销”。从面向对象的角度来看,更好的做法是Org类提供一个cancel() 方法,像下面这样:

package chapter12.unjuanable.domain.orgmng.org;
// imports ...

public class Org {
    //...
    
    //Org 自己管理自己的状态
    public void cancel() { 
        this.status = OrgStatus.CANCELLED;
    }
    
    //...
}

这样,Org类就可以自己管理自己的状态,OrgHandler就不必了解Org内部状态的转换细节,只需告诉Org需要撤销就可以了,像下面这样。

package chapter12.unjuanable.domain.orgmng.org;
// imports ...

@Component
public class OrgHandler {
    //...
    
    public void cancel(Org org, Long userId) {
        cancelValidator.OrgToBeCancelledShouldNotHasEmp(
                        org.getTenant(), org.getId());
        cancelValidator.OrgToBeCancelledShouldBeEffective(org);
        org.cancel();   // 只需告诉 Org 要进行撤销,但不必了解 Org 内部细节
        updateAuditInfo(org, userId);
    }
    
    // ...
}

类似的,我们看一下“只有有效的组织才能被撤销”这条规则的实现代码。

package chapter12.unjuanable.domain.orgmng.org.validator;
// imports...

@Component
public class CancelOrgValidator {
    //...

    // 只有有效的组织才能被撤销
    public void OnlyEffectiveOrgCanBeCancelled(Org org) {
        //直接访问了状态属性
        if (!org.getStatus().equals(OrgStatus.EFFECTIVE)) { 
            throw new BusinessException("该组织不是有效状态,不能撤销!");
        }
    }
    
    //...
}

我们看到CancelOrgValidator直接访问了Org的状态,判断是否为有效组织。这实际上又是重构中的一种坏味道,叫做特性依恋(Feature Envy)。Status这个特性是属于Org类的,而 CancelOrgValidator要通过访问这个特性来实现自己的逻辑,所以 CancelOrgValidator “依恋”了Org的特性。这是对象封装性被破坏的征兆。

解决方法是将这段判断逻辑移动到Org类内部,重构后的Org类是这样的。

package chapter12.unjuanable.domain.orgmng.org;
// imports ...

public class Org {
    //...
    
    public boolean isEffective() {
        return status.equals(OrgStatus.EFFECTIVE);
    }
      
    //...
}

这样,CancelOrgValidator就可以不依赖Org的内部状态了。

package chapter12.unjuanable.domain.orgmng.org.validator;
// imports...

@Component
public class CancelOrgValidator {
    //...

    // 只有有效的组织才能被撤销
    public void OnlyEffectiveOrgCanBeCancelled(Org org) {
        //不再依赖 Org 的内部状态
        if (!org.isEffective()) { 
            throw new BusinessException("该组织不是有效状态,不能撤销!");
        }
    }
    
    //...
}

Org.cancel() 和 Org.isEffective() 既提高了对象的封装性,也表达了这个功能的业务含义,因此也是表意接口模式的一种应用。

上一节的表意接口体现在“领域服务”,这一节体现在“领域对象”。对“表意接口”的运用,往往可以避免“特性依恋”的坏味道,反之,发现和消除“特性依恋”,也会重构出“表意接口”。

用继承消除重复

下面再来看看Org类还有什么可以优化的地方。

我们发现,其实每个实体都包含4个审计字段createdAt、createdBy、lastUpdatedAt和 lastUpdatedBy。那么我们可以考虑抽出一个包含这四个属性的父类,减少编写各个类的重复工作。抽出的父类是下面的样子。

package chapter12.unjuanable.common.framework.domain;

import java.time.LocalDateTime;

public abstract class AuditableEntity {
    protected LocalDateTime createdAt;
    protected Long createdBy;
    protected LocalDateTime lastUpdatedAt;
    protected Long lastUpdatedBy;

    public AuditableEntity(LocalDateTime createdAt, Long createdBy) {
        this.createdAt = createdAt;
        this.createdBy = createdBy;
    }

    // lastUPdatedAt 和 lastUpdatedBy 的 setter 以及所有属性的 getter ...
  
}

这个类放在common.framework包里面,因为这种基类属于框架层面。

在面向对象设计中一个常见的陷阱就是滥用继承。要防止这一倾向,要记住一个原则,不要仅仅为了复用而使用继承。你还要问自己一个问题,父类和子类的关系,在语义上,是否有分类关系,或者概念的普遍和特殊关系。只有符合这种关系的,才能采用继承,否则应该用“组合”来实现复用。

在我们的例子中,我们发现审计字段可以复用,只是可能可以采用继承的一个征兆。然后我们再从语义上进行分析。可以说,每个实体都是可审计的,或者说,每类实体都是一类特殊的可审计实体。我们发现了这种普遍和特殊的分类关系,所以在这里运用继承是合理的

编程风格回顾

第10节课中我们说过,具体的编程风格是多种多样的,每种都各有利弊。因此,不可能也没有必要强求一致。关键是理解背后的原理,作出取舍,然后在自己的开发团队中形成比较统一的风格。

下面总结一下我们这几节课采用的风格的要点,说明背后的原因,以及其他可选的方式。

第一,领域对象不访问数据库。

我们在领域对象中既不会显式,也不会隐式访问数据库,目的是使领域对象和数据库解耦,便于维护和测试。

如果使用JPA并结合延迟加载,领域对象就会隐式地访问数据库;如果在领域对象的方法中写SQL,则会显式地访问数据库。领域对象访问数据库,才能实现更加纯粹的面向对象风格,代价是一般需要对ORM框架有深入的理解,或者增加了领域对象和数据库访问机制的耦合,还可能无意间导致性能等问题。你可以根据这两者的利弊进行取舍。

第二,领域服务只能读数据库。

在实现一些业务规则时,需要访问数据库中的数据,因此领域服务需要读数据库。而写库的功能通常可以由应用服务来做,从而减轻领域层的负担。有些伙伴喜欢把写功能也放在领域服务,从而使应用服务非常薄,这样也可以。

第三,应用服务可以读写数据库。

纯粹的(通过调用仓库)读写数据库并不包含领域知识,因此放在应用服务似乎更合适一些。比如更新一个对象前,要把这个对象先从数据库中读出来;创建或修改对象后,要存回数据库,这些本身都没有领域逻辑。

第四,用 ID 表示对象之间的关联。

如果按偏面向对象的风格,对象之间的关联应该用对象导航的方式来实现。比如说,Org对象的leader属性的类型,应该是Emp(员工),这样就可以在内存中直接从组织对象导航到充当负责人的员工对象了。不过我们的程序中,leader属性仅保存了员工的ID,而不是员工对象。这是考虑到在企业应用中节省带宽和内存。关于对象导航风格,我们会在下个迭代中继续探讨。

第五,领域对象有自己的领域服务。

如果是偏面向对象风格,很多领域逻辑可以在领域对象内部完成,因此不一定需要领域服务。但对于偏过程式的风格,由于领域对象不能访问数据库,很多领域逻辑就要外化到领域服务中,因此多数领域对象都会有相应的领域服务。

第六,在以上前提下利用封装和继承。

即使是我们这种偏过程式的风格,如果善于利用封装、合理利用继承,也能有效提高程序质量。一个技巧是识别特性依恋的坏味道,并重构到表意接口

最后要说的是,我们这个例子其实比较简单,但体现出的原理和复杂功能是一样的,咱们要以小见大,举一反三。

总结

好,今天的内容先讲到这里,现在来总结一下。

这节课我们主要是利用封装和继承,进一步提高了代码质量。我们可以通过两个层面来提高封装性。

第一个层面是API的封装。添加和修改组织两种API的参数是不同的,我们只对外暴露出每个API 必须的参数,从而缩小了接口,提高了封装性。

第二个层面是领域对象的封装。我们分析了哪些属性是不需要修改的,把这些属性变成只读的,从而缩小了对象的接口。另一方面,我们用表意接口封装了状态属性,使外界不需要直接关心状态转换的细节,同时消除了特性依恋的坏味道。

另外,我们还可以利用继承减少代码的重复。在我们的例子里,是通过抽象出AuditableEntity 作为所有实体的父类来实现的。

最后,我们对这三节课所使用的编程风格进行了回顾,说明了这么做的理由以及一些其他做法。这里没有唯一正确的方式,重要的是根据实际情况进行权衡。

讲完今天的课,第一个迭代就结束了。通过实现组织创建修改功能,我已经把这个迭代需要掌握的知识点都介绍清楚了(为了让你关注重点,我没有把所有的代码实现都写进课程里)。你可以尝试运用这些知识,自己实现其他的功能。

学完这个迭代,应该可以利用DDD开发一些不太复杂的需求了,你不妨在自己的项目中试一试。

另外,为了帮助你检验迭代一的学习效果,我策划了加餐1的内容(包括十道选择题和一道建模题目),你可以通过这个链接去练习。建模题目的解析和参考答案,等你自己亲手练习以后,可以对照加餐4 的分析点评加深理解。

思考题

1.对于领域对象,可否利用Java的包级私有权限,进一步增强封装?

2.在现在的Org类里,只对setter进行了限制,而所有属性都有getter,能否举例说明什么样的属性不应该有getter呢?

好,今天的课程结束了,有什么问题欢迎在评论区留言。从下节课开始,我们将进入第二个迭代,学习DDD中几个有些难度的内容,包括聚合值对象模式以及泛化等技能。相信在咱们的共同努力下,你一定能顺利掌握。

精选留言

  • 别天神

    2023-02-04 15:40:33

    1、对于包级私有权限来增强封装我觉得是可行的,领域对象的更改我认为只能通过领域方法来做,一方面保护了领域状态的一致性,另一方面也能最大程度确保开发人员随意更改状态,所以通常我觉得 领域对象内部不应该存在set方法,取而代之的是更加“表意”的领域方法。

    2、但是从存储介质进行领域对象恢复的时候,有时候又需要set方法,恢复领域对象应该无需关心业务,所以存在一些矛盾点:
    - 为了保护业务一致性和封装,不暴露set
    - 为了恢复领域对象,我们需要set,更进一步,为了使用工具类进行属性的copy还要求set必须是public的

    在实际实现上,我采用了一些折中的方案,
    - 领域对象自身不具备set方法,有一个包级别的领域对象Builder,Builder中承载set,如@Builder(setterPrefix = "set", toBuilder = true, access = AccessLevel.PROTECTED)
    - 这样可以在同一个包内访问builder, 构建builder实例,同时因为setXxx是public的,还能解决属性copy问题
    - builder.build()可以转换成领域对象
    - 可以建一个FQN一样的包(和仓储实现同模块),里面仅存放着 持久化对象到领域对象的转换,因为包名一样,可以访问到set
    作者回复

    这个思路不错👍🏻

    2023-02-06 08:05:50

  • Jaising

    2023-01-20 23:48:09

    还在犹豫是否要翻过墙的时候,就先把帽子丢到墙的另一边,剩下的自然会有办法。

    以我切身体会仅仅迭代一学完就治好了不少DDD恐惧症,真的感谢钟老师和编辑小新的最小闭环设计,对零基础入门DDD不要太好,平时不愿记笔记的我都能跟着老钟医通过重新演绎更新了五篇——事件风暴、领域建模、数据库设计、分层架构和代码实现,小结与导航在这里:https://juejin.cn/post/7190270005072625723。

    辞旧迎新,给自己的最好礼物就是持续学习,学以致用,换取职业生涯不断上台阶的可能,终身成长就有了立身之本,用来抵御各类危机。新的一年从入门DDD开始,会发现软件开发的世界照进了新的光,让我们一起加油!

    祝各位兔年快乐!
    作者回复

    很佩服你的学习精神,加油!

    2023-03-04 22:08:57

  • bin

    2022-12-31 11:03:53

    handler如果不依赖数据库,这部分业务逻辑好像也可以放到领域对象里面。有没有一个好的边界区分?
    作者回复

    目前的风格就是以是否依赖数据库来区分,参考课程中对代码风格的总结。当然,也可以制定你自己的代码规则。

    2023-01-07 19:18:03

  • aoe

    2023-01-03 23:34:43

    原来我现在的编程风格是偏过程式的

    思考题
    1. 可以。这样确实可以增强封装。再疯狂一点,可以使用 Java 模块化技术,让反射都无法打破封装。
    2. 没发现不应该有 getter 的属性。
    作者回复

    关于第2问,可以扩展一下。比如对于状态,如果用 isEffective(), isTerminated()之类的方法封装了,那么未必要用 getStatus()了。

    2023-01-07 10:28:40

  • H·H

    2023-01-30 19:41:04

    领域对象是否可以通过依赖注入的方式注入 仓储Repository?
    作者回复

    你是指把repository注入领域对象,从而使领域对象可以访问数据库吗?理论上可以,就看你要不要选这种编程风格。

    2023-02-01 13:00:09

  • 6点无痛早起学习的和尚

    2023-01-17 09:27:31

    一些问题:
    5. 为什么在 addOrg 加事务?里面就一个 save 操作,是否没必要加@Transactional
    6. 很多用@Autowired 注入的属性,有些是 final,有些不是 final,这个有什么讲究吗?
    作者回复

    加了@Transactional不会有什么坏处,如果以后逻辑更复杂了,可以避免忘掉。
    @Autowired注入的属性一般都用final,我有些地方没注意,等我稍后统一改一下

    2023-03-04 19:49:22

  • Breakthrough

    2023-07-17 11:00:04

    钟老师,DDD设计完成后的交付文档一般长啥样,是不是也要做类似设计需求说明书的WORD档。如果是,是否有相关的模板参考下?
    作者回复

    关键是有领域模型、词汇表、业务规则表,可以把这几个制品融入到目前使用的文档中。

    2023-08-03 21:50:01

  • 龙腾

    2023-01-02 14:35:57

    老师请教一下,“第三,应用服务可以读写数据库。”这块没太明白,什么情况下读写数据库不包含领域知识呢?咱们的数据库设计不就是按照领域来划分设计的吗,只要读写都是在领域内进行的吧?
    作者回复

    “调用Repository,从数据库里取出一个领域对象”这句话是不需要和业务人员谈的,因此这句话反应的逻辑不包含领域知识,

    2023-01-07 19:25:56

  • 开心

    2024-04-25 12:58:51

    看到这里,没有感受到太多面向对象,更多是面向过程的实现。领域业务大都是放在了没有属性只有方法的领域服务,更多的是如何定义领域服务模块。有点不解。
    作者回复

    正如第10课说的,我们这门课采用的是“偏过程”的风格,“在领域对象不直接或间接访问数据库的前提下,尽量面向对象”。所以你的感觉是正确的。当然,也可以采用更加面向对象的编程方式,这时通常要用JPA了。

    2024-11-14 09:52:20

  • 路上

    2023-12-15 13:28:05

    类似撤销这部分校验逻辑,目前看领域对象自己就可以校验,目前是在外层进行的校验之后调用领域方法进行状态变更
    是否可以在领域对象变更状态的方法内部自己进行校验呢
    作者回复

    没错,后面就会讲到这个思路。

    2023-12-17 17:24:38

  • 末日,成欢

    2023-06-13 20:34:02

    老师,有一些疑问。
    第一,领域对象不访问数据库。
    这里的不访问数据库,是指可以在领域服务中访问,而不要在领域对象中里访问数据是吧。
    第二个疑问, 修改组织信息和撤销组织, 第一步骤是查询组织是否为空, 这应该也是领域服务中的逻辑判断,应该也放在领域服务中把。这里直接放在应用服务的考虑是什么?
    还有为何save方法没有放到领域服务中?这里的应该不是应用逻辑把?
    第三个疑问, 事件风暴法中的第一个识别领域事件,这个领域事件维度多大呢,比如我想进行任务分解, 应该在哪个流程进行分解才合适呢?
    目前代码看到这里,还是偏向过程化, 只是利用了一些软件的设计原则(比如solid),代码更业务化, 还没理解有啥好处
    作者回复

    第一:是的
    第二:为了避免累赘,或者你看能否有更好的写法贡献出的?
    对save的调用不算领域逻辑
    第三:粒度大体相当于一个事务
    本课程的的代码是偏过程式的。或许以后有时间给大家讲讲真正的面向对象编程是什么样。

    2023-06-15 22:39:59

  • 🏄🏻米兰的大铁匠🎤🎈

    2023-05-28 16:25:00

    请问老师,"只有有效的组织才能被撤销"这段校验逻辑是否可以更进一步的内聚到org对象本身的cancel()方法中,这样外部触发"撤销"动作时,只需调用一个方法就行了
    作者回复

    可以的

    2023-06-05 20:39:44

  • marker

    2023-05-20 22:41:50

    老师,有一个问题就是领域服务,比如下单需要验证商品一系列的验证、但是商品和订单在两个bc下、会去做rpc操作、这个应该放在哪里合适
    作者回复

    不同规则情况不同,要举出具体验证逻辑才能说明白。一般来说,数据在哪里,逻辑就放在哪里。

    2023-06-05 20:43:06

  • Hesher

    2023-04-07 06:44:48

    回答一下课后问题:
    1. 一般直接用私有,避免被外部访问,即使是包内,封装性一步到位;
    2. 审计字段一般不会被业务使用,可以没有getter,其他字段看情况。
    作者回复

    1、Java中区分了包级私有(package private)和私有(private)两种权限,和C++有所不同。

    2023-04-16 16:33:51

  • Geek_e10c1b

    2023-03-28 10:32:26

    老师,领域对象中不访问数据库,那如何对某些属性进行懒加载呢
    作者回复

    这是个好问题。

    首先,是否要懒加载取决于我们选择的编程范式。
    如果选择偏过程式的编程,那么是否加载某个对象,在应用服务或领域服务中已经确定了,不存在懒加载;如果选择偏对象式的编程,才存在懒加载的需求。
    这两种方式没有绝对的对错,只是取决于团队的选择。在课程里,为了照顾多数国内同学的编程习惯,选择了偏过程的范式,在这种方式中,领域对象不访问数据库,也不存在懒加载。
    但这并不意味着偏过程式的编程是唯一正确的方式。如果你的团队经过权衡,选择了偏对象式的范式,那么领域对象就可以访问数据库(直接或间接),可以使用懒加载。

    2023-03-30 15:38:38

  • Fredo

    2023-03-04 20:52:55

    "简单粗暴地把所有属性都暴露给客户端" 这里实际前后端开发过程中,前端是否会觉得你直接给我提供一个共用的结构,对我前端来说更方便呢?
    作者回复

    “最小接口原则”建议只暴露最少的字段,这是通用的最佳实践。只有在有充分理由的情况下才应该打破。是个权衡问题。

    2023-03-15 09:17:58

  • py

    2023-02-09 11:49:57

    1. 可以
    2. 不需要被外部知道的信息不许要getter,比如一般情况下创建时间等
    作者回复

    不错

    2023-03-16 22:19:31

  • escray

    2023-01-29 15:51:42

    从 org.setStatus(OrgStatus.CANCELLED) 到 org.cancel() 的重构,这个是不是应该在面向对象分析的时候就能够做到?org.isEffective() 也有类似的问题。

    1. 领域对象不访问数据库
    2. 领域服务只读数据库
    3. 应用服务可以写数据库
    4. 组合优于继承

    这三条编程风格,其实从开始设计的时候就采纳。

    对于思考题,

    1. 对于 Java 的包级私有权限,我的理解是可以默认为私有,只有在需要的时候才改为 protect 或者是 public,也就是说默认采用相对严格的封装。
    2. 对于不适合 getter 的属性,我的理解是所有没有明显业务含义的,都不需要 getter,比如 ID 字段,以及一些采用 ID 来表示对象之间关系的字段,例如 leaderID 就不需要 getter,可以补充一个 getLeader 的公开方法。

    课程看到这里,感觉在讲领域驱动的同时,也在说明面向对象的分析和设计,甚至包括代码实现和重构。
    作者回复

    第1题说的包级私有权限,指的是方法名前面什么都不加(不加public、private、protected)的权限,这种方法只有自己包里的其他对象才能访问。这是Java特有的一种权限修饰符,C++里是没有的。

    2023-03-17 00:12:24

  • Johar

    2023-01-25 16:56:01

    1. 对于领域对象,可否利用 Java 的包级私有权限,进一步增强封装?
    可以
    2. 在现在的 Org 类里,只对 setter 进行了限制,而所有属性都有 getter,能否举例说明什么样的属性不应该有 getter 呢?
    应用层不需要的字段,或者是需要进行装换字段
    作者回复

    不错

    2023-03-17 00:19:02

  • Sam Jiang

    2023-01-23 19:55:18

    问题一:个人觉得可以。
    问题二:如果getter返回的是容器类对象,需要防止客户程序通过引用修改容器。一种解决办法是返回不可变的容器,就无法增删元素了,但还是可以修改其中某个元素的属性。