• DDD Series 1: What is an Object-relational mapping ORM? Just plain not writing SQL?
  • DDD series ii: Re-understand object-oriented development, subvert traditional cognition, and break CURD dilemma
  • DDD Series 3: Transforming Procedural Code, Step by step (Step 1)
  • DDD series 3: Domain objects and value objects are confused
  • DDD Repository: DDD Repository and traditional Repository
  • DDD series 5: Polymerization root magic and egg pain
  • DDD series six: no matter how good the design can not afford to toss, software corruption step by step abyss
  • DDD series 7: Reconstructing the 16-character mind method
  • DDD Series 8: CodeReview for errands

Learning from bottom to bottom: ask questions -> analyze problems -> solve problems -> summarize

Demand scenarios

The business requirements

As shown in the figure, return the check-in record of the current month. For the convenience of explanation, start from simple and return the check-in record of the current week: 2021-05-23 0 2021-05-24 0 2021-05-25 0 2021-05-26 1 2021-05-27 1 2021-05-28 0 2021-05-29 0 1 Sign-in 0 Sign-in failedCopy the code

Procedural code implementation

type UserSign struct {
    Id int
    UserId int
    Date time.Time
}

type Calendar struct {}

func (cal *Calendar) SignLogs(userId int) {

    now := time.Now()
    index := int(now.Weekday())
    var dateArray [7]string 
    for i := 0; i < 7; i++ {
        day := now.AddDate(0.0, i - index).Format("2006-01-02")
        dateArray[i] = day
    }
    // dateArray [2021-05-30 2021-05-31 2021-06-01 2021-06-02 2021-06-03 2021-06-04 2021-06-05]

    var userSignLogs []*UserSign
    db.Where(&UserSign{UserId: userId}).Where("`date` IN ?", dateArray).Find(&userSignLogs)

    userSignLogsMap := make(map[string]int)
    for _, log := range userSignLogs {
        k := log.Date.Format("2006-01-02")
        userSignLogsMap[k] = 1
    }

    for i := 0; i < 7; i++ {
        if _, ok := userSignLogsMap[dateArray[i]]; ok {
                continue
        }
        userSignLogsMap[dateArray[i]] = 0
    }

    jsonString, _ := json.Marshal(userSignLogsMap)
    fmt.Println(string(jsonString))
    / / {" 2021-05-30 ": 0," 2021-05-31 ": 0," 2021-06-01 ": 1," 2021-06-02 ": 1," 2021-06-03 ": 0," 2021-06-04 ": 0," 2021-06-05 ": 0}
}
Copy the code

Consider: if you are in the codeReview phase and need a codeReview, what’s wrong with the code?

Examine the code

Before review code and tell you a story, recently a trip the power went out in the home, home appliances, is unlikely to try one by one, fortunately now always beside the gate and a series of “switch” function, I turn off the switch all first, then up the hall, found no problem, and then push all advocate lie, one side, is no problem, When pushed to the kitchen, and tripping, and finally found that the electric kettle in the kitchen leakage, the whole process only takes a few minutes, and if there is only one main gate in the home, can only pull out all the electrical plugs first, and then put the electrical appliances in the home to try one by one, think about the head is big.

After hearing the story, we review our code back, first of all, this code for organizing, data acquisition, data conversion, parameter format adjustment, printing, etc all coupled together, once the production problems, we need to emergency screening, we will have to go over the whole method, also like when we check the circuit, Check all the appliances in the house, and this is poor readability.

In addition, if we want to transform the kitchen circuit, we only need to close the kitchen switch, but if there is only one main switch, we will have to close the main switch and the whole house will be cut off before rectification. For example, we need to change the date from the current date to only need days. We need to read the code from the beginning. Locating all date-related variables and date-key snippets, sorting out the context logic of the snippet, and then determining if it needs to be changed, is bad maintainability.

On the other hand, when we need to add a new date range on top of this, such as monthly check-in or quarterly check-in, it becomes more and more expensive, changing the original code with each change (all functionality in one method). In addition, when we add or modify functions, at least have to read over the original method to do, the longer the method is more difficult to understand, will be hard to change, and difficult to understand, because the original code in most cases only small refinements, or add a branch, in the hope does not affect the original function as far as possible, add a new function is more and more slow, and form a vicious circle, This is poor scalability.

type UserSign struct {
	Id int
	UserId int
	Date time.Time
}

type Calendar struct {}

func (cal *Calendar) SignLogs(userId int) {

	// Set parameters
	now := time.Now()
	index := int(now.Weekday())
	var dateArray [7]string // Question: What does the number 7 mean
	for i := 0; i < 7; i++ {
		day := now.AddDate(0.0, i - index).Format("2006-01-02")
		dateArray[i] = day
	}
	// dateArray [2021-05-30 2021-05-31 2021-06-01 2021-06-02 2021-06-03 2021-06-04 2021-06-05]
	// Parameter collation is complete

	// Data retrieval starts
	var userSignLogs []*UserSign
	db.Where(&UserSign{UserId: userId}).Where("`date` IN ?", dateArray).Find(&userSignLogs)
	// Data fetching is complete

	// Data conversion begins
	userSignLogsMap := make(map[string]int)
	for _, log := range userSignLogs {
		k := log.Date.Format("2006-01-02")
		userSignLogsMap[k] = 1 // Question: What does the number 1 mean
	}

	for i := 0; i < 7; i++ {
		if _, ok := userSignLogsMap[dateArray[i]]; ok {
			continue
		}
		userSignLogsMap[dateArray[i]] = 0
	}
	// Data conversion is complete

	// Format adjustment, print start
	jsonString, _ := json.Marshal(userSignLogsMap)
	fmt.Println(string(jsonString))
	/ / {" 2021-05-30 ": 0," 2021-05-31 ": 0," 2021-06-01 ": 1," 2021-06-02 ": 1," 2021-06-03 ": 0," 2021-06-04 ": 0," 2021-06-05 ": 0}
	// Format adjustment, print end

	// Problem: dateArray and userSignLogsMap variables run through the function, making it more difficult to read and debug
}
Copy the code

Single responsibility principle

Single responsibility principle: There is one and only one cause that causes an object to change.

Job simplification, make code easier to understand, that is to say, a class or method should be designed to complete a/class thing, if not, then when a number of functions performed by it of any changes, we will have to modify the class or method, that is a violation of the single responsibility principle.

The single responsibility principle applies not only to classes and methods, but also to properties, database fields, and so on. A very bad design is to mash up multiple pieces of information in a single field and then make conditional decisions by string separation and matching.

The general manifestation of a violation of the single responsibility principle is as follows:

1, long method, internal contain multiple functions, as a whole is difficult to reuse, divergent type 2 changes, including any related function change, all need to modify the method 3, question difficult position, there is a problem, the method is too long, the internal data extraction, transformation, processing are coupled, difficult to quickly locate code indentation in question 4, methods more than three layers, In the if... The else with the switch... Case or for... 5. One or more variables are referenced throughout the method, judged as conditions, or assigned to be modified in different places according to different conditionsCopy the code

If not timely reconstruction, left unchecked, then the code will continue to decay, become very complex and hard to understand, when no one is willing to modify the method or class and new function demand changes need to be in the original basically, people will tend to think of a copy of the original method, to change a name, and then in the function of the newly added to this new method, This is where we enter the advanced stage of violating the single responsibility principle — repetitive code, and repetitive code is the root of all evil.

Single responsibility principle -> Reduce complexity -> simplify code -> improve maintainability

Diagnose this sample code: The method is close to 30 lines, especially data extraction, conversion, assignment scattered throughout the method, high maintenance costs, dateArray, userSignLogsMap variables are created for different reasons, in the method of different logic blocks, sometimes according to the condition, scene assignment, and sometimes by other variables reference. This is one of the reasons code coupling is important when called as parameters. When we need to maintain or extend the method, we need to understand that such variables are referenced and assigned everywhere in the method, and the brain’s capacity is limited, which can cause dyslexia.

In addition, it will cost a lot to add a new time period query on this basis or to modify when requirements change. All these problems are technical debts owed before and affect not the present but the future.

It’s payback time!

Identify refactoring targets

1, eliminate invisible knowledge 2, eliminate shuttle variables 3, variable method named self-interpretationCopy the code

Hidden knowledge: knowledge is a process of learning and understanding, such as the number 7 in the code, in the context of this code, programmers need to think to understand the number 7 is the number of days of the week, which increases the difficulty of reading, hidden knowledge needs to be eliminated! (Hidden knowledge also known as magic value)

Shuttle variable: a variable in the whole method is implemented throughout, where will appear, the scope is particularly wide, when troubleshooting bugs or modify the function do not know where to use this variable when it will change, inevitably lead to reading difficulty and maintenance difficulty, shuttle variable need to eliminate!

Self-explanatory naming: an easy to understand method or variable name without ambiguity can reduce the difficulty of understanding the code, coding 5 minutes, naming 2 hours!

const (
    UnSign = iota // Static numeric format to eliminate hidden knowledge
    Signed
)

type UserSign struct {
    Id int
    UserId int
    Date time.Time
}

type Calendar struct {}

func (cal *Calendar) SignLogs(userId int) {

    dateArray := cal.getWeekDateArray()

    userSignLogs := cal.getSignLogs(userId, dateArray)

    userSignLogsMap := cal.transformSignLogsMap(userSignLogs, dateArray)

    cal.toJson(userSignLogsMap)
}

func (cal *Calendar) getWeekDateArray(a) []string {
    now := time.Now()
    index := int(now.Weekday())
    var dateArray []string
    weekdayCount := 7 // 7 days a week with variable names self-explanatory, eliminate hidden knowledge
    for i := 0; i < weekdayCount; i++ {
        day := now.AddDate(0.0, i - index).Format(cal.getDateFormatLayout())
        dateArray = append(dateArray, day)
    }
    return dateArray
}

func (cal *Calendar) getSignLogs(userId int, dateArray []string) (userSignLogs []*UserSign) {
    db.Where(&UserSign{UserId: userId}).Where("`date` IN ?", dateArray).Find(&userSignLogs)
    return
}

func (cal *Calendar) transformSignLogsMap(userSignLogs []*UserSign, dateArray []string) (userSignLogsMap map[string]int){
    userSignLogsMap = make(map[string]int)
    for _, log := range userSignLogs {
        k := log.Date.Format(cal.getDateFormatLayout())
        userSignLogsMap[k] = Signed
    }

    len: =len(dateArray)
    for i := 0; i < len; i++ {
        if _, ok := userSignLogsMap[dateArray[i]]; ok {
                continue
        }
        userSignLogsMap[dateArray[i]] = UnSign
    }
    return
}

func (cal *Calendar) getDateFormatLayout(a) string {
    return "2006-01-02" // The function name is self-explanatory, eliminating hidden knowledge
}

func (cal *Calendar) toJson(userSignLogsMap map[string]int)  {
    jsonString, _ := json.Marshal(userSignLogsMap)
    fmt.Println(string(jsonString))
}
Copy the code

At this point, we have restricted variables to their child functions, and by naming the function, the body method becomes four sentences.

GetWeekDateArray: Replaces the required data format toJson with a given date. GetSignLogs: Replaces the required data format with a given date. Given the check-in calendar data, conversion business format, output to the console, complete the functionCopy the code

Each subfunction has a single function. If there is a problem, we can change the corresponding subfunction without moving to other methods. At the method level, we achieve the goal of “single responsibility”.

Consider: dateArray pass mode todo

Note: At this point is a process oriented approach to complete function, process oriented approach in fact also nothing bad, by naming and variable separation, process oriented approach can also write a relatively easy to understand code, imagine, if the production problems, we can through the way of function switch can quickly screen problem, if you need to add new query time, The scope of modification will not be too large.

However, this is not enough, we now have a “single responsibility” at the method level. At the class level, the Calendar class takes care of the output function, the data fetching function, and the format transformation function. For example, when extending other time periods, we still need to make changes at the class level, so let’s move on…

In the actual development, the code at this time has been able to meet the business needs, so there is no problem to write, it is possible in the life cycle of the software really only need to obtain the weekly check-in calendar, the design is time, labor costs.

New requirement: Add current month check-in record

func (cal *Calendar) SignLogs(userId int, isWeek bool) {

    var dateArray []string
    if isWeek {
        dateArray = cal.getWeekDateArray()
    } else {
        dateArray = cal.getMonthDateArray()
    }

    userSignLogs := cal.getSignLogs(userId, dateArray)

    userSignLogsMap := cal.transformSignLogsMap(userSignLogs, dateArray)

    cal.toJson(userSignLogsMap)
}
Copy the code

How do you feel after watching it? Do we see code like this in everyday development code? When we add a new function to the old code, do we add an if… by extending several arguments to the original code? Or else the switch… Case branch to implement new functionality?

Open and closed principle

Open Closure principle: objects are open to extension and closed to modification.

Use polymorphism to extend new functional requirements by adding new classes in order to seal the change inside the class that only needs to change.

The open and closed principle requires that in the design, we should try to make the class good enough, and not modify it once it is written. New requirements should be solved by adding classes, and the original code will remain unchanged if it can, but it is impossible to absolutely close the modification. Therefore, we should be careful when using the open and closed principle: Refused to immature abstraction and abstract itself is just as important, that is to say, don’t cry because it is abstract, on demand or speculation about the future of excessive design as there is no design, more than three layers of business class inherits the maintenance costs are likely to outweigh the benefits, the best design is evolved according to the demand of just good implementation.

Use the open closed principle –> reduce coupling –> control the scope of change –> increase extensibility

Identify refactoring targets

Use polymorphism to isolate changes at the class level. Rename and eliminate redundant codeCopy the code
const (
    UnSign = iota // Static numeric format to eliminate hidden knowledge
    Signed
)

const DateFormatLayout = "2006-01-02"

type UserSign struct {
    Id int
    UserId int
    Date time.Time
}

type IDate interface {
    getDateArray() []string
}

type Week struct {}

func (w *Week) getDateArray(a) []string {
    now := time.Now()
    index := int(now.Weekday())
    var dateArray []string
    weekdayCount := 7 // 7 days a week with variable names self-explanatory, eliminate hidden knowledge
    for i := 0; i < weekdayCount; i++ {
            day := now.AddDate(0.0, i - index).Format(DateFormatLayout)
            dateArray = append(dateArray, day)
    }
    return dateArray
}

type Calendar struct {
    date IDate
}

func (cal *Calendar) setDate(date IDate) {
    cal.date = date
}

func (cal *Calendar) SignLogs(userId int) {

    dateArray := cal.date.getDateArray()

    userSignLogs := cal.getSignLogs(userId, dateArray)

    userSignLogsMap := cal.transformSignLogsMap(userSignLogs, dateArray)

    cal.toJson(userSignLogsMap)
}

func (cal *Calendar) getSignLogs(userId int, dateArray []string) (userSignLogs []*UserSign) {
    db.Where(&UserSign{UserId: userId}).Where("`date` IN ?", dateArray).Find(&userSignLogs)
    return
}

func (cal *Calendar) transformSignLogsMap(userSignLogs []*UserSign, dateArray []string) (userSignLogsMap map[string]int){
    userSignLogsMap = make(map[string]int)
    for _, log := range userSignLogs {
        k := log.Date.Format(DateFormatLayout)
        userSignLogsMap[k] = Signed
    }

    len: =len(dateArray)
    for i := 0; i < len; i++ {
        if _, ok := userSignLogsMap[dateArray[i]]; ok {
                continue
        }
        userSignLogsMap[dateArray[i]] = UnSign
    }
    return
}

func (cal *Calendar) toJson(userSignLogsMap map[string]int)  {
    jsonString, _ := json.Marshal(userSignLogsMap)
    fmt.Println(string(jsonString))
}


func main(a) {
    calendar := new(Calendar)
    calendar.setDate(new(Week))
    calendar.SignLogs(1)}Copy the code

At this point, we refer to a single responsibility, open closed two principles, will change in the class and method of isolated two levels, that is to say when the requirements change, if it is a Week, only need to adjust the Week class, and vice versa, if it is related to data extraction and conversion, you just need to adjust in the base class method, at the same time, If we need to add, say, a monthly check-in record, we don’t need to tweak the existing code, just extend the Month class.

Why design software?

Software is designed “over the long term” to adapt more easily to changing future requirements. Correct software design methods are designed to achieve changes in software requirements over time, better, faster, and more easily.

Requirements are constantly changing

Software functions cannot remain unchanged throughout the whole process of a project from birth to death. There are a large number of new requirements and iterative updates of old functions in the process, so the code must embrace and adapt to changes. The code that can quickly adapt to changes in requirements is good code.

Example analysis of how the refactored code responds to changes in requirements:

Modify the format - > years day day format {" 30 ": 0," 31 ": 0," 1 ": 1," 2 ": 1," 3 ": 0," 4 ": 0," 5 ": 0} analysis: Data format changes have nothing to do with parameter sorting or data query. You can quickly locate the transformSignLogsMap method, and then the userSignLogsMap variable, and modify the map key generation and assignment logic.Copy the code
Add monthly Calendar sign in record analysis: add Month class, implement getDateArray method to get the list of current Month's dates, Calendar class does not need to modify.Copy the code
Check in to get different numbers of Jingdou, return data need to add Jingdou data, compatible with the old version 1.0 return: {" 2021-05-30 ": 0," 2021-05-31 ": 0," 2021-06-01 ": 1," 2021-06-02 ": 1," 2021-06-03 ": 0," 2021-06-04 ": 0," 2021-06-05 ": 0} version 2.0 returned: {"2021-05-30":{"sign":0,"bean":0},"2021-05-31":{"sign":1,"bean":1},"2021-06-01":{"sign":1,"bean":2},"2021-06-02":{"sign" : 0, "bean" : 0}, "2021-06-03" : {" sign ": 0," bean ": 0}," 2021-06-04 ": {" sign" : 0, "bean" : 0}, "2021-06-05" : {" sign ": 0," bean ": 0}} analysis: UserSign adds bean properties, transforms the transformSignLogsMap method into a separate class, creates a new Transformer interface, defines the Transform method, and so on.Copy the code

conclusion

Single application -> Microservices -> Hierarchy -> MVC -> OOP -> Single class -> different methods for single class

The essence of designing software is to manage complexity and solve complex business requirements. There is no good way but to divide and conquer. The complex business is divided into multiple appropriate sub-businesses, which are then divided into multiple appropriate sub-modules, which are then divided into multiple appropriate classes, which are then divided into multiple appropriate methods. How does it fit? Figure it out. You’re a software guru.