An Alibaba Cloud Technical Expert’s Insight Into Domain-driven Design: Domain Primitive

Introduction

Preface

  • Best architecture practices: the core ideas and implementation solutions of the hexagon application architecture and clean architecture
  • Continuous discovery and delivery: event storming -> context map -> design heuristics -> modeling
  • Reducing the speed of architecture corruption: integrating the anti-corruption layer with the modular solution of a third-party library
  • Specifications and boundaries of standard components, such as entity, aggregate, repository, domain service, application service, event, and DTO assembler
  • Redefining application service boundaries based on use cases
  • DDD-based microservice transformation and fine-grained control
  • Transformation and challenges of Command Query Responsibility Segregation (CQRS)
  • Challenges of an event-driven architecture
  • And more

Domain Primitive

  • Not developed from anything else
  • In the first or early stage of formation or growth

1) Case Analysis

public class User {
Long userId;
String name;
String phone;
String address;
Long repId;
}
public class RegistrationServiceImpl implements RegistrationService { private SalesRepRepository salesRepRepo;
private UserRepository userRepo;
public User register(String name, String phone, String address)
throws ValidationException {
// 校验逻辑
if (name == null || name.length() == 0) {
throw new ValidationException("name");
}
if (phone == null || !isValidPhoneNumber(phone)) {
throw new ValidationException("phone");
}
// 此处省略address的校验逻辑
// 取电话号里的区号,然后通过区号找到区域内的SalesRep
String areaCode = null;
String[] areas = new String[]{"0571", "021", "010"};
for (int i = 0; i < phone.length(); i++) {
String prefix = phone.substring(0, i);
if (Arrays.asList(areas).contains(prefix)) {
areaCode = prefix;
break;
}
}
SalesRep rep = salesRepRepo.findRep(areaCode);
// 最后创建用户,落盘,然后返回
User user = new User();
user.name = name;
user.phone = phone;
user.address = address;
if (rep != null) {
user.repId = rep.repId;
}
return userRepo.save(user);
}
private boolean isValidPhoneNumber(String phone) {
String pattern = "^0[1-9]{2,3}-?\\d{8}$";
return phone.matches(pattern);
}
}

API Definition

User register(String, String, String);
service.register("殷浩", "浙江省杭州市余杭区文三西路969号", "0571-12345678");
User findByName(String name);
User findByPhone(String phone);
User findByNameAndPhone(String name, String phone);

Data Verification and Error Handling

if (phone == null || !isValidPhoneNumber(phone)) {
throw new ValidationException("phone");
}
if (phone == null || !isValidPhoneNumber(phone) || !isValidCellNumber(phone)) {
throw new ValidationException("phone");
}
if (phone == null) {
throw new ValidationException("phone不能为空");
} else if (!isValidPhoneNumber(phone)) {
throw new ValidationException("phone格式错误");
}
// Use Bean Validation
User registerWithBeanValidation(
@NotNull @NotBlank String name,
@NotNull @Pattern(regexp = "^0?[1-9]{2,3}-?\\d{8}$") String phone,
@NotNull String address
);
// Use ValidationUtils:
public User registerWithUtils(String name, String phone, String address) {
ValidationUtils.validateName(name); // throws ValidationException
ValidationUtils.validatePhone(phone);
ValidationUtils.validateAddress(address);
...
}
  • Bean Validation is only applicable to simple verification logic. Implementing complex verification logic requires writing code to implement a custom verifier.
  • Add the Bean Validation annotation in every required place in the new verification logic. If it is missing from any required place, the DRY principle is violated.
  • The Single Responsibility principle is violated when a large amount of verification logic is concentrated in one class. As a result, the code looks confusing and is unmaintainable.
  • Business exceptions and verification exceptions are still mixed.

Business Code Definition

String areaCode = null;
String[] areas = new String[]{"0571", "021", "010"};
for (int i = 0; i < phone.length(); i++) {
String prefix = phone.substring(0, i);
if (Arrays.asList(areas).contains(prefix)) {
areaCode = prefix;
break;
}
}
SalesRep rep = salesRepRepo.findRep(areaCode);
private static String findAreaCode(String phone) {
for (int i = 0; i < phone.length(); i++) {
String prefix = phone.substring(0, i);
if (isAreaCode(prefix)) {
return prefix;
}
}
return null;
}
private static boolean isAreaCode(String prefix) {
String[] areas = new String[]{"0571", "021"};
return Arrays.asList(areas).contains(prefix);
}
String areaCode = findAreaCode(phone);
SalesRep rep = salesRepRepo.findRep(areaCode);

Testability

2) Solution

public class PhoneNumber {

private final String number;
public String getNumber() {
return number;
}
public PhoneNumber(String number) {
if (number == null) {
throw new ValidationException("number不能为空");
} else if (isValid(number)) {
throw new ValidationException("number格式错误");
}
this.number = number;
}
public String getAreaCode() {
for (int i = 0; i < number.length(); i++) {
String prefix = number.substring(0, i);
if (isAreaCode(prefix)) {
return prefix;
}
}
return null;
}
private static boolean isAreaCode(String prefix) {
String[] areas = new String[]{"0571", "021", "010"};
return Arrays.asList(areas).contains(prefix);
}
public static boolean isValid(String number) {
String pattern = "^0?[1-9]{2,3}-?\\d{8}$";
return number.matches(pattern);
}
}
  • “private final String number” is used to ensure that PhoneNumber is an immutable value object. Value objects are typically immutable.
  • The verification logic is included in the constructor. Verification is successful as long as the PhoneNumber class is created.
  • The findAreaCode method is changed to getAreaCode in the PhoneNumber class, indicating that areaCode is a computing attribute of PhoneNumber.
  • The data type is used to explicitly identify PhoneNumber in subsequent code.
  • The class is used to include all logic related to PhoneNumber in a file.
public class User {
UserId userId;
Name name;
PhoneNumber phone;
Address address;
RepId repId;
}
public User register(
@NotNull Name name,
@NotNull PhoneNumber phone,
@NotNull Address address
) {
// 找到区域内的SalesRep
SalesRep rep = salesRepRepo.findRep(phone.getAreaCode());
// 最后创建用户,落盘,然后返回,这部分代码实际上也能用Builder解决
User user = new User();
user.name = name;
user.phone = phone;
user.address = address;
if (rep != null) {
user.repId = rep.repId;
}
return userRepo.saveUser(user);
}

API Definition

public User register(Name, PhoneNumber, Address)
service.register(new Name("殷浩"), new Address("浙江省杭州市余杭区文三西路969号"), new PhoneNumber("0571-12345678"));

Data Verification and Error Handling

public User register(
@NotNull Name name,
@NotNull PhoneNumber phone,
@NotNull Address address
) // no throws

Business Code Definition

SalesRep rep = salesRepRepo.findRep(phone.getAreaCode());
User user = xxx;
return userRepo.save(user);

Testability

  • M test cases are still required by PhoneNumber, but the code in each test case is greatly reduced because only one object is tested. This lowers the maintenance cost.
  • For every parameter in every method, the code only needs to check whether the parameter value is null. Other conditions are impossible because all non-null parameters are valid.

3) Advanced Usage

Case 1) Account Transfer

public void pay(BigDecimal money, Long recipientId) {
BankService.transfer(money, "CNY", recipientId);
}
@Value
public class Money {
private BigDecimal amount;
private Currency currency;
public Money(BigDecimal amount, Currency currency) {
this.amount = amount;
this.currency = currency;
}
}
public void pay(Money money, Long recipientId) {
BankService.transfer(money, recipientId);
}

Case 2) Cross-border Transfer

public void pay(Money money, Currency targetCurrency, Long recipientId) {
if (money.getCurrency().equals(targetCurrency)) {
BankService.transfer(money, recipientId);
} else {
BigDecimal rate = ExchangeService.getRate(money.getCurrency(), targetCurrency);
BigDecimal targetAmount = money.getAmount().multiply(new BigDecimal(rate));
Money targetMoney = new Money(targetAmount, targetCurrency);
BankService.transfer(targetMoney, recipientId);
}
}
@Value
public class ExchangeRate {
private BigDecimal rate;
private Currency from;
private Currency to;
public ExchangeRate(BigDecimal rate, Currency from, Currency to) {
this.rate = rate;
this.from = from;
this.to = to;
}
public Money exchange(Money fromMoney) {
notNull(fromMoney);
isTrue(this.from.equals(fromMoney.getCurrency()));
BigDecimal targetAmount = fromMoney.getAmount().multiply(rate);
return new Money(targetAmount, to);
}
}
public void pay(Money money, Currency targetCurrency, Long recipientId) {
ExchangeRate rate = ExchangeService.getRate(money.getCurrency(), targetCurrency);
Money targetMoney = rate.exchange(money);
BankService.transfer(targetMoney, recipientId);
}

4) Discussion and Summary

Definition of Domain Primitive

  • Domain primitives are immutable.
  • A domain primitive is a complete concept with a precise definition.
  • Domain primitives use the native language of the business domain.
  • A domain primitive is the smallest component of the business domain and can be used to build complex combinations.

Three Principles for Using Domain Primitives

  • Make Implicit Concepts Explicit
  • Make Implicit Context Explicit
  • Encapsulate Multi-object Behavior

Differences Between Domain Primitives and Value Objects in DDD

  • In the book Domain Driven Design by Eric Evans, value objects are mostly of the non-entity type.
  • In the book Implementing DDD by Vaughn Vernon, the author focuses on the immutability attribute, Equals method, and Factory method of value objects.

Differences Between Domain Primitives and Data Transfer Objects

Scenarios Suitable for Domain Primitives

  • Strings with limited formats, such as Name, PhoneNumber, OrderNumber, ZipCode, and Address
  • Limited integers, such as OrderId (>0), Percentage (0–100%), and Quantity (>=0)
  • Enumerated int such as Status (generally, Enum is not used due to deserialization)
  • Double or BigDecimal domain primitives with business meanings, such as Temperature, Money, Amount, ExchangeRate, and Rating
  • Complex data structure such as Map. Encapsulate all operations of Map and expose only necessary behaviors.

5) Practice — Refactoring Old Applications

Step 1) Create Domain Primitives and Collect all Domain Primitive Behaviors

Step 2) Replace the Data Verification Logic and Stateless Logic

public User register(String name, String phone, String address)
throws ValidationException {
if (name == null || name.length() == 0) {
throw new ValidationException("name");
}
if (phone == null || !isValidPhoneNumber(phone)) {
throw new ValidationException("phone");
}

String areaCode = null;
String[] areas = new String[]{"0571", "021", "010"};
for (int i = 0; i < phone.length(); i++) {
String prefix = phone.substring(0, i);
if (Arrays.asList(areas).contains(prefix)) {
areaCode = prefix;
break;
}
}
SalesRep rep = salesRepRepo.findRep(areaCode);
// 其他代码...
}
public User register(String name, String phone, String address)
throws ValidationException {

Name _name = new Name(name);
PhoneNumber _phone = new PhoneNumber(phone);
Address _address = new Address(address);

SalesRep rep = salesRepRepo.findRep(_phone.getAreaCode());
// 其他代码...
}

Step 3) Create an API

public User register(Name name, PhoneNumber phone, Address address) {
SalesRep rep = salesRepRepo.findRep(phone.getAreaCode());
}

Step 4) Modify External Calls

service.register("殷浩", "0571-12345678", "浙江省杭州市余杭区文三西路969号");
service.register(new Name("殷浩"), new PhoneNumber("0571-12345678"), new Address("浙江省杭州市余杭区文三西路969号"));

Original Source:

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Alibaba Cloud

Alibaba Cloud

Follow me to keep abreast with the latest technology news, industry insights, and developer trends. Alibaba Cloud website:https://www.alibabacloud.com