Domain Primitive (DP) of DDD

Posted by Mgccl on Sun, 16 Jan 2022 08:06:41 +0100

preface:

DDD is an architectural idea, not a framework.

Domain Primitive:

What is DP? It is the "basic data structure" in DDD. Like int and string in Java, it is the only way for us to learn. It's a little abstract. Let's illustrate it through a case.

For the user registration function, you need to enter the user's name, telephone (Landline with area code) and address. And the background needs to count which area has the most registered users according to the telephone information. Then you can see that the phone needs its own verification logic to judge whether the number is legal and split the area code.

Traditional way:

public class User {
    String name;
    String phone;
    String address;
}

public class RegistrationServiceImpl implements RegistrationService {

    public User register(String name, String phone, String address) 
      throws ValidationException {
        // Check logic
        if (name == null || name.length() == 0) {
            throw new ValidationException("name");
        }
        if (phone == null || !isValidPhoneNumber(phone)) {
            throw new ValidationException("phone");
        }
        // The verification logic of address is omitted here

        // Take the area code in the phone number, and then find the SalesRep in the area through the area code
        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);

        // Finally, create a user, drop the disk, and then return
        User user = new User();
        user.name = name;
        user.phone = phone;
        user.address = address;
        return userRepo.save(user);
    }

    private boolean isValidPhoneNumber(String phone) {
        String pattern = "^0[1-9]{2,3}-?\\d{8}$";
        return phone.matches(pattern);
    }
}

After reading the above code, some people may ask that some judgments can be made by using @ validation annotation in DTO. This is really no problem, but validation annotation is not omnipotent. In case of complex situations, you still need to write code in the business layer to judge.

From the above code, we can see the following problems:

▍ question 1 - clarity of interface:
From the perspective of compilation, the method does not have parameter names when compiling, but only parameter types, so it is as follows

User register(String, String, String); 

Therefore, if the parameter is illegal, the problem cannot be found during compilation. The problem will be exposed only when it reaches the business layer during execution.

A more obvious example is what I have seen most in the project so far

User findByName(String name);
User findByPhone(String phone);
User findByNameAndPhone(String name, String phone);

I believe everyone is familiar with the above code. It is very common in the query function. In this scenario, because the input parameters are all String types, ByXXX has to be added to the method name to distinguish them, and findByNameAndPhone will also fall into the problem of wrong input parameter order, which is different from the previous input parameters. If the parameter order is entered incorrectly, The method will not report an error, but will return null, and this kind of bug is more difficult to find. The thinking here is, is there any way to make the method reference clear at a glance and avoid the bug caused by the parameter entry error?

▍ problem 2 - data validation and error handling

if (phone == null || !isValidPhoneNumber(phone)) {
            throw new ValidationException("phone");
        }

This code can be seen to judge whether the phone number is legal. Now it is a phone number with area code. If other types of numbers need to be added in the future, don't you need to modify them here? And if there are judgments in other places, don't you need to change many places.
At this time, someone will definitely propose to use ValidationUtils for judgment, but when a large number of judgment logic is filled in a class, the Single Responsibility singleness principle is broken.

▍ question 3 - clarity of business code

// Take the area code in the phone number, and then find the SalesRep in the area through the area code
        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;
            }
        }

Obviously, the above paragraph is the legendary glue code. The so-called glue code is to extract part of the data from some input, then call an external dependency to get more data, and then extract some data from the new data as other functions. The best way to solve the glue code is to pull it out and make a method:

//Get the area code from the number
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;
}
//Judge whether the area code exists
private static boolean isAreaCode(String prefix) {
    String[] areas = new String[]{"0571", "021"};
    return Arrays.asList(areas).contains(prefix);
}

The original code becomes:

//Get area code
String areaCode = findAreaCode(phone);
//Find the person in charge of the area code
SalesRep rep = salesRepRepo.findRep(areaCode);

In order to reuse the above methods, a static tool class PhoneUtils may be extracted. But what we need to think about here is whether static tool classes are the best implementation? When your project is full of a large number of static tool classes and the business code is scattered in multiple files, can you still find the core business logic?

Problem solving:

First, let's analyze the problem we just encountered. First, we found that verifying the phone number makes the code very messy in the business layer. Then, we found that the area code of the separated phone number is also very messy in the code. Judging whether the area code exists is even more chaotic in the code. It's clearly just a registered business. I did write a lot of things related to the phone number. Is it sick? So when you suspect that you are ill, you can consider that this phone number is not the hidden concept of tm.

Right and right, the first principle of DP is to find the hidden concept and show it explicitly. Without saying a word, draw out the number directly into a DP

public class PhoneNumber {
  
    private final String number;
    public String getNumber() {
        return number;
    }

    public PhoneNumber(String number) {
        if (number == null) {
            throw new ValidationException("number Cannot be empty");
        } else if (isValid(number)) {
            throw new ValidationException("number Format error");
        }
        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);
    }

}

It can be seen that we added judgment to the construction method, and took the method just as an attribute.
Let's see what the original code looks like after implementing DP:

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
) {
    // Find SalesRep in area
    SalesRep rep = salesRepRepo.findRep(phone.getAreaCode());

    // Finally, create a user, drop the disk, and then return. In fact, this part of the code can also be solved by Builder
    User user = new User();
    user.name = name;
    user.phone = phone;
    user.address = address;
    return userRepo.saveUser(user);
}

Change the interface call to:

service.register(new Name("xx"), new Address("xxxxx"), new PhoneNumber("0571-12345678"));

It can be seen that as long as it is an incoming parameter, it must meet the conditions, and the code is much clearer than before. For number related operations, only the DP of the operation number can be used.

Summary:

▍ definition of Domain Primitive

Let's redefine Domain Primitive: Domain Primitive is a Value Object with precisely defined, self verifiable and behavior in a specific field.

DP is a Value Object in the traditional sense, with Immutable features
DP is a complete concept with precise definition
DP uses the native language in the business domain
DP can be the smallest part of a business domain, or it can build complex compositions

(Reference: Alibaba technical experts explain DDD series - Domain Primitive)

Topics: Java Programming Spring