Giter VIP home page Giter VIP logo

eas-ddd's Introduction

Build Status codecov

项目介绍

EAS-DDD是我为GitChat课程《领域驱动设计实战(战略篇)》与《领域驱动设计实战(战术篇)》提供的实战项目案例。该案例取材自我参与过的真实项目,整个案例的代码完全按照领域驱动设计的过程进行建模、设计与编码。

通过访问本Repository对应的Wiki,可以了解EAS项目的需求、建模过程与建模产出物;更为详细的分析设计内容,请订阅我在GitChat上发布的课程。每个领域场景对应的用户故事、拆分的任务,请访问本Repository的Issue

环境

本项目的开发基于Java语言进行开发,具体环境包括:

Java: Java 8+
Maven: 3
Spring: 5.1.10+
Spring Boot:2.1.9
MyBatis:3.5.3
Druid:1.1.20
MySQL: 8.0 Community

我个人认为JPA ORM更加符合DDD的设计,在另外一个采用DDD开发的项目Payroll-DDD,我选择的持久化框架就是Spring Data JPA。本项目之所以采用MyBatis,是考虑到MyBatis在国内企业软件开发领域中更为常见。同时,我也希望通过本项目说明使用MyBatis作为持久化框架,同样可以做DDD。

数据库环境准备

项目默认的数据库用户名为sa,密码为123456,数据库主机为localhost,数据库为eas-db。在安装了MySQL 8.0后,若数据库服务器信息与默认信息不同,请修改如下文件。在使用flywaydb执行数据库脚本时,需要确保数据库配置正确,并已经创建了eas-db数据库。

flywaydb的数据库配置

pom.xml文件的<plugins>中配置了如下内容:

<plugin>
   <groupId>org.flywaydb</groupId>
   <artifactId>flyway-maven-plugin</artifactId>
   <version>${flyway.version}</version>
   <configuration>
        <driver>${db.driver}</driver>
        <url>${db.url}</url>
        <user>${db.username}</user>
        <password>${db.password}</password>
   </configuration>
</plugin>

在同一个pom文件的属性配置部分,配置了数据库的相关属性:

<properties>
    <db.driver>com.mysql.jdbc.Driver</db.driver>
    <db.url>jdbc:mysql://localhost:3306/eas-db?serverTimezone=UTC</db.url>
    <db.username>sa</db.username>
    <db.password>123456</db.password>
</properties>

一旦准备好flywaydb的环境,就可以运行命令执行DB的清理:

mvn package flyway:clean

或执行命令执行DB的迁移:

mvn package flyway:migrate

若要忽略运行单元测试,可以在Maven命令后面加上参数:

-DskipTests

测试环境准备

本项目采用测试驱动开发对领域层的类与方法进行了编码实现,因而领域层的相关方法基本皆为单元测试所覆盖。

应用服务则通过集成测试保障其正确性。若要运行集成测试,需通过flywaydb执行SQL脚本,以准备集成测试所必须的数据库环境。如果数据库配置与我的配置不同,除了要修改pom.xml文件中的flywaydb的配置之外,还需要各个限界上下文模块test/resources/spring-mybatis.xml文件的相关配置,如:

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:context="http://www.springframework.org/schema/context" xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd
		http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
		http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
    <context:component-scan base-package="xyz.zhangyi.ddd.eas.trainingcontext" />

    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"
          init-method="init" destroy-method="close">
        <property name="url" value="jdbc:mysql://localhost:3306/eas-db?serverTimezone=UTC"/>
        <property name="username" value="sa"/>
        <property name="password" value="123456"/>
        <property name="connectionProperties" value="com.mysql.jdbc.Driver"/>
    </bean>
</beans>    

运行测试

默认情况下,如果运行mvn test则只会运行单元测试。如果确保数据库已经准备好,且通过flywaydb确保了数据库的表结构与测试数据已经准备好,可以运行mvn integration-test。该命令会运行所有测试,包括单元测试和集成测试。

**注意:**项目中所有的单元测试以Test为测试类后缀,所有集成测试以IT为测试类后缀。

运行Spring Boot服务

整个项目采用了单体架构,故而所有限界上下文的远程服务都通过一个统一的主程序入口,即eas-entry模块下的EasApplication。Spring Boot的应用配置在该模块的resources文件夹下,文件名为application.yml,内容为:

mybatis:
  mapperLocations: classpath:mapper/*.xml
  type-aliases-package: xyz.zhangyi.ddd.eas.trainingcontext.domain
  type-handlers-package: xyz.zhangyi.ddd.eas.trainingcontext.gateway.acl.impl.persistence.typehandlers

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/eas-db?serverTimezone=UTC
    username: sa
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver
    # 使用druid数据源
    type: com.alibaba.druid.pool.DruidDataSource

若要启动Spring Boot服务,需确保数据库的配置正确。在没有配置端口的情况下,默认端口号为8080。你也可以在application.yml中指定端口。

可以直接在IDE下运行EasApplication启动Spring Boot服务。

如你所见,当前项目采用了单体架构,但是可以非常容易迁移到微服务架构。若采用Spring Boot公开服务,需要在每个模块的pom.xml文件中,加入spring-boot-starter的依赖。可以参考eas-entry模块的依赖配置。

eas-ddd's People

Contributors

agiledon avatar iamzhangyi avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

eas-ddd's Issues

AttendanceContext - 确定出勤状态

根据员工考勤记录、请假情况、假日设置确定员工的出勤状况。测试用例包括:

  • 日期为假期,员工无考勤记录或为无效考勤记录,出勤状态为Holiday
  • 日期为假期,员工有有效考勤记录,出勤状态为Overtime
  • 日期为工作日,员工有考勤记录,上下班打卡时间满足工作时间规则定义,出勤状态为Normal
  • 日期为工作日,员工有考勤记录,上班打卡事件晚于上班时间定义范围,出勤状态为Late
  • 日期为工作日,员工有考勤记录,下班打卡事件早于上班时间定义范围,出勤状态为LeaveEarly
  • 日期为工作日,员工有考勤记录,上班打卡事件晚于上班时间定义范围,下班打卡事件早于上班时间定义范围,出勤状态为LateAndLeaveEarly
  • 日期为工作日,员工无考勤记录,无请假记录,出勤状态为Absence
  • 日期为工作日,员工无考勤记录,有请假记录,出勤状态为请假类型

EmployeeContext - 生成员工号

生成员工号由领域服务EmployeeIdGenerator承担,它的子任务为:

  • 获取最近入职员工的顺序号
  • 按照规则生成员工号(#3 )

ProjectContext - 分配问题给项目成员

作为一名项目管理成员
我希望将问题分配给指定项目成员
以便于确定问题的负责人进行进度跟踪

分解的任务为:

  • 分配问题给项目成员 (#12 )
    • 获得问题
    • 分配问题给经办人
    • 更新问题
    • 创建问题的变更记录
    • 通知报告人 (#13 )
      • 生成报告人通知
      • 发送通知
    • 通知经办人 (#13 )
      • 获取经办人信息
      • 生成经办人通知
      • 发送通知

领域场景的时序图伪代码为:

IssueAppService.assign(issueId, owner) {
    IssueService.assign(issueId, owner) {
        Issue issue = IssueRepository.issueOf(issueId);
        issue.assignTo(ownerId);
        IssueRepository.update(issue);
        ChangeHistoryRepository.add(changeHistory);
        NotificationService.notifyOwner(issue, owner) {
            AssignmentNotification notification = AssignmentNotification.forOwner(issue, owner);
            NotificationClient.notify(NotificationRequest.from(notification));
        }
        NotificationService.notifyReporter(issue) {
            EmployeeResponse empResponse = EmployeeClient.employeeOf(reporterId);
            AssignmentNotification notification = AssignmentNotification.forReporter(issue, empResponse.toReporter());
            NotificationClient.notify(NotificationRequest.from(notification));
        }
    }
}

EmployeeContext - 办理员工入职

办理员工入职由EmployeeService领域服务承担,子任务为:

  • 根据身份证号或手机号确认该员工是否已经存在
  • 添加员工

伪代码:

    EmployeeService.onboarding(employee) {
        EmployeeRepository.isExist(idNumber, mobilePhone);
        EmployeeRepository.save(employee);
    }

AttendanceContext - 生成员工出勤记录

由领域服务AttendanceGenerator承担,其子任务包括:

  • 根据日期获取员工的打卡记录
  • 获取工作时间
  • 确定是否节假日
  • 获取员工请假信息
  • 确定出勤状态 (#8 )

伪代码为:

            AttendanceGenerator.generate(empId, day) {
                TimeCard timeCard = TimeCardRepository.timeCardOf(empId, day);
                Worktime worktime = Worktime.worktime();
                boolean isHoliday = HolidayRepository.isHoliday(day);
                Leave leave = LeaveRepository.leaveOf(empId, day);
                Attendance.assureStatus(timeCard, worktime, leave, isHoliday);
            }

TrainingContext - 培训签到

As 一名培训学员
I want to 签到
So that 记录我已正常出勤

场景1:学员签到
Given:拥有Confirmed状态的培训票的学员
And: 培训已经开始
When: 签到
Then: 记录学员的出勤信息
And: 培训票被设置为Closed状态
And: 记录学员的学习信息

时序图脚本:

TrainingAppService.checkIn(checkInRequest) {
    CheckInService.checkIn(traineeId, trainingId) {
        TicketRepository.ticketOf(traineeId, trainingId, ticketStatus);
        Ticket.checkIn();
        TicketRepository.update(ticket);
        AttendanceRepository.add(attendance);
        LearningService.append(trainee, trainingId) {
            CourseRepository.courseOf(trainingId);
            LearningRepository.add(learning);
        }
    }
}

EmployeeContext - 按照规则生成员工ID

为新员工生成一个唯一的员工号。员工号的生成规则为:
{入职日期}-{4位顺序号}

说明:

  • 入职日期为员工入职当天的日期,格式为{yyyyMMdd}

例如入职日期为2019年12月24日,最近入职员工的顺序号为0101,则新生成的员工号为:201912240102

ProjectContext - 分配问题

IssueService领域服务承担职责,其子任务包括:

  • 获得问题
  • 分配问题给经办人
  • 更新问题
  • 创建问题的变更记录

伪代码:

    IssueService.assign(issueId, owner) {
        Issue issue = IssueRepository.issueOf(issueId);
        issue.assignTo(ownerId);
        IssueRepository.update(issue);
        ChangeHistoryRepository.add(changeHistory);
    }

对于IssueassignTo方法,需要编写测试用例覆盖:

  • 状态为ResolvedClosed的Issue不能再分配
  • 如果Issue已有Owner,不能将该Issue分配给同一个Owner

TrainingContext - 确定候选人是否已经参加过该课程的培训

组合任务,由LearningService承担,分解的任务为:

  • 确定候选人是否已经参加过该课程
    • 获取该培训对应的课程
    • 确定课程学习记录是否有该候选人

该组合任务的两个原子任务都会访问数据库,由Repository完成,故而无需针对这两个原子任务进行测试驱动开发。

时序图脚本如下:

LearningService.beLearned(candidateId, trainingId) {
    TrainingRepository.trainingOf(trainingId);
    LearningRepository.isExist(candidateId, courseId);
}

AttendanceContext - 生成员工工作时间

根据员工的打卡记录生成员工的工作时间(TimeCard)。需要考虑多种情况(这里规则做了一定程度简化):

  • 正常打卡,则员工每天会有两条打卡记录,以PunchedTime早的为上班时间,晚的为下班时间
  • 只有一次打卡,若PunchedTime为上午12点之前,记录为上班时间,下班时间缺失,PunchedTime为上午12点之后,记录为下班时间,上班时间缺失
  • 无打卡,则返回null

EmployeeContext - 员工入职

作为一名HR,
我希望办理员工入职,
以便于提供更好的员工管理与服务。

任务分解为:

  • 入职 (#1 )
    • 验证员工信息 (#2 )
    • 生成员工号 (#5 )
      • 获取最后入职员工的顺序号
      • 按照规则生成员工号
    • 根据身份证号或手机号确认该员工是否已经存在 (#6 )
    • 添加员工 (#6 )

时序图伪代码为:

EmployeeAppService.onboarding(OnboardingRequest) {
    OnboardingRequestAssembler.composeEmployee() {
        OnboardingRequest.toEmployee();
        EmployeeIdGenerator.generate() {
            EmployeeRepository.latestEmployee();
            employee.idFrom(sequenceCode);
        }
    }
    EmployeeService.onboarding(employee) {
        EmployeeRepository.isExist(idNumber, mobilePhone);
        EmployeeRepository.save(employee);
    }
}

**说明:**需求分析与领域模型参见本项目的Wiki

AttendanceContext - 生成出勤记录

作为一名人事管理专员,
我希望系统能够生成出勤记录,
以便于及时对员工进行考勤。

任务分解为:

  • 生成出勤记录
    • 获取员工信息
    • 生成员工出勤记录 (#9 )
      • 获取员工工作时间 (#10 )
        • 根据日期获取员工的打卡记录
        • 生成员工工作时间
      • 获取工作时间规则
      • 确定是否节假日
      • 获取员工请假信息
      • 确定出勤状态 (#8 )
    • 保存出勤记录

时序图脚本为:

AttendanceAppService.generate(day) {
    AttendancesGenerator.generate(day) {
        List<String> eployeeIds = EmployeeClient.allEmployeeIds();
        for (String empId : employeeIds) {
            AttendanceGenerator.generate(empId, day) {
                TimeCardGenerator.generate(empId, day) {
                    PunchedCardRepository.punchedCardsOf(empId, day);
                    TimeCard.createFrom(List<PunchedCard>);
                }    
                WorktimeRule worktimeRule = WorktimeRuleRepository.worktimeRule();
                boolean isHoliday = HolidayRepository.isHoliday(day);
                Leave leave = LeaveRepository.leaveOf(empId, day);
                Attendance.assureStatus(timeCard, worktimeRule, leave, isHoliday);
            }
        }
        AttendanceRepository.saveAll(attendances);
    }
}

**说明:**该Story的需求说明与领域模型参见本项目的Wiki页面

EmployeeContext - 转换OnBoardingRequest为Employee领域对象

将应用服务接收的消息OnBoardingRequest转换为Employee领域对象,由OnboardingRequestAssembler装配器承担。子任务包括:

  • 创建Employee对象
  • 生成员工ID(#5 )
  • 装配Employee

伪代码为:

    OnboardingRequestAssembler.composeEmployee() {
        OnboardingRequest.toEmployee(employeeId);
        EmployeeIdGenerator.generate() {
            EmployeeRepository.latestEmployee();
            employee.idFrom(sequenceCode);
        }
    }

ProjectContext - 发送问题分配通知

当Issue被分配给Owner后,需要发送通知给该Issue的Owner和Reporter。任务为:

  • 通知报告人
    • 生成报告人通知
    • 发送通知
  • 通知经办人
    • 获取经办人信息
    • 生成经办人通知
    • 发送通知

伪代码:

        NotificationService.notifyOwner(issue, owner) {
            AssignmentNotification notification = AssignmentNotification.forOwner(issue, owner);
            NotificationClient.notify(NotificationRequest.from(notification));
        }
        NotificationService.notifyReporter(issue) {
            EmployeeResponse empResponse = EmployeeClient.employeeOf(reporterId);
            AssignmentNotification notification = AssignmentNotification.forReporter(issue, empResponse.toReporter());
            NotificationClient.notify(NotificationRequest.from(notification));
        }
``

TrainingContext - 发送提名通知

组合任务,由NotificationService领域服务履行职责。分解的任务为:

  • 发送提名通知
    • 获取通知邮件模板
    • 组装提名通知内容
    • 发送通知

“组装提名通知内容”原子任务由MailTemplate聚合履行,时序图脚本如下:

NotificationService.notifyNominee(ticket, nominee) {
    MailTemplateRepository.templateOf(templateType);
    MailTemplate.compose(ticket, nominee);
    NotificationClient.notify(notificationRequest);
}

提名邮件的类型为MeetingRequest,邮件模板内容为:

Hi $nomineeName$:
We are glad to notify that you are nominated by $nominatorName$ to attend the training. Please click the link as below to confirm this nomination before the deadline $deadline$:
$url$

Here is the basic information of training:
Title: $title$
Description: $description$
Begin time: $dbeginTime$
End time: $dendTime$
Place: $dplace$

Thanks! We're excited to have you in the training.
EAS Team

TrainingContext - 提名候选人

As 一名协调者
I want to 提名候选人参加培训
So that 部门的员工得到技能培训的机会

场景1:候选人获得提名
Given:从候选人名单中选择要提名的候选人
And: 选择要提名的培训票
When: 提名候选人
Then: 培训票被设置为WaitForConfirm状态
And: 该培训票不可再被提名
And: 候选人将收到培训提名的邮件通知

场景2:候选人参加过该课程
Given:从候选人名单中选择要提名的候选人
And: 该候选人已经参加过该培训要学习的课程
And: 选择要提名的培训票
When: 提名候选人
Then: 提示该候选人已经参加过该课程
And: 提名失败
And: 培训票仍然处于Available状态

时序图脚本为:

NominationAppService.nominate(nominationRequest) {
    NominationService.nominate(ticketId, candidate) {
        LearningService.beLearned(candidateId, trainingId) {
            TrainingRepository.trainingOf(trainingId);
            LearningRepository.isExist(candidateId, courseId);
        }
        TicketService.nominate(ticketId, candidate) {
            TicketRepository.ticketOf(ticketId);
            Ticket.nominate(candidate);
            TicketRepository.update(ticket);
        }
        NotificationService.notifyNominee(ticket, nominee) {
            MailTemplateRepository.templateOf(templateType);
            MailTemplate.compose(ticket, nominee);
            NotificationClient.notify(notificationRequest);
        }
    }
}

TrainingContext - 将票提名给候选人

组合任务,由TicketService领域服务承担,任务分解为:

  • 提名候选人
    • 获得培训票
    • 提名
    • 保存票的状态
    • 添加票的历史记录
    • 将被提名人移除候选人名单

“提名”原子任务,由Ticket聚合根实体承担。该任务的测试用例包括:

  • 验证票的状态必须为“Available”
  • 提名给候选人后,票的状态更改为“WatiForConfirm”
  • 为票生成提名历史记录

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.