This article was shared by Taowen in Didi.
1.Why
For front-line developers, most of the day is spent piling code on top of existing projects. When the project is really stuck, you need to find revenue to refactor the code. Since we spend most of our time sitting in front of a monitor reading and writing code, unreadable code is killing ourselves or our colleagues, so it’s better to hone your skills and write good code in the first place.)
2.How
To improve the readability of the code, analyze the actual running environment of the code first. The code actually runs in two places: the CPU and the human brain. For THE CPU, code optimization needs to understand its working mechanism, and the code is optimized for the CPU characteristics. When we read code, the human brain acts like an interpreter, running it line by line. In this sense, to improve the readability of code, we first need to understand how the brain works.
Here’s a look at what the brain is good for and not good for:
What the brain is good at doing
Name Image Description Object identification
Unlike machine learning, which may not be able to accurately identify a cat after looking at millions of cats, the human brain can do a good job of recognizing a cat after looking at a few cats. The space decomposition
The human brain doesn’t need labels and can intuitively perceive different objects in space. The sequential predict
Was your first thought that this guy was going to get hit by a car? The sequential memory
As part of our survival instinct, the brain forms a memory of a place we’ve visited many times. Analogy that
The human brain also has an analogy function. For example, most people would choose C for this question.
Things the brain isn’t good at doing
The name picture example cannot be mapped to the abstract concept of real life experience
When we look at the left image, it’s easy for us to think about how to beat the game, but when we look at an abstract concept like the right image, where the objects are pixels, we don’t know what the hell is going on. For example, if your code is filled with variable names like Z,X,C, and V, you might get confused. Lengthy detective reasoning
The human brain is also bad at scenarios that require recursion (or cycling) to check all the possibilities and find a solution. Track multiple processes that change simultaneously
The brain is a single-threaded CPU, not good at drawing circles with the left hand and circles with the right.
Code optimization theory
Once you understand the strengths and weaknesses of the human brain, you can make your code more readable according to the characteristics of the human brain. Three theories are extracted here:
- Align Models: Data and algorithm Models in code should correspond to mental Models in the human brain
- When writing code, Shorten the “Sherlock Holmes” Process, that is, do not write large pieces of code
- Write code to Isolate Process by Process. Do not describe the evolution of multiple processes at the same time
These three models are explained in detail with examples:
Align Models
In code, the model is nothing more than data structure and algorithm, while in the human brain, the corresponding mental model, the so-called mental model is the brain’s idea of an object or a thing, we usually talk is the external expression of the mental model. To reduce the cost of mapping the code from the requirements document to the code, we should match the actual nouns in the code. For example, the noun “bank account” can be represented by many variable names, such as: BankAccount, bank_Account, account, bankAccount, BA, bank_ACC, item, row, record, model, variable names that can be linked to real objects should be used uniformly in coding.
Code naming techniques
You don’t have to write random variable names and then study them in comments.
Elapsed time = elapsed time = elapsed time = elapsed time = elapsed time = elapsed time = elapsed timeCopy the code
When naming a function, use a combination of a verb and a noun, and also make sure to identify your custom variable type:
// bad func getThem(theList [][]int) [][]int {var list1 [][]int For _, x := range theList {if x[0] == 4 {// 4 List1 = append(list1, X)}} Return flagged Cell [] // Good type Cell []int int int func (Cell) isFlagged() bool { == 4} func getFlaggedCells(gameBoard []Cell) []Cell {var flaggedCells []Cell for _, cell := range gameBoard { if cell.isFlagged() { flaggedCells = append(flaggedCells, cell) } } return flaggedCells }Copy the code
Code decomposition Techniques
If you look carefully, you can decompose the code according to the Spatial Decomposition of the Page:
/ / bad / /... Then... And then... and then ... RenderPage(Request *http. request) map[string]interface{}{page := map[string]interface{}{} name := request.Form.Get("name") page["name"] = name urlPathName := strings.ToLower(name) urlPathName = regexp.MustCompile(`['.]`).ReplaceAllString( urlPathName, "") urlPathName = regexp.MustCompile(`[^a-z0-9]+`).ReplaceAllString( urlPathName, "-") urlPathName = strings.Trim(urlPathName, "-") page["url"] = "/biz/" + urlPathName page["date_created"] = time.now ().in (time.utc) return page} Var page = map[string]pageItem{"name": pageName, "url": pageUrl, "date_created": pageDateCreated, } type pageItem func(* http.request) interface{} func pageName(Request * http.request) interface{} {// name Related process return Request.form. Get("name")} func pageUrl(request * http.request) interface{} {// URL related process name := request.Form.Get("name") urlPathName := strings.ToLower(name) urlPathName = regexp.MustCompile(`['.]`).ReplaceAllString( urlPathName, "") urlPathName = regexp.MustCompile(`[^a-z0-9]+`).ReplaceAllString( urlPathName, "-") urlPathName = strings.Trim(urlPathName, "-") return "/biz/" + urlPathName} func pageDateCreated(Request *http. request) interface{} {// Date Related process return time.Now().In(time.UTC) }Copy the code
Temporal Decomposition: The following code mixes accounting and printing bills for the whole process and can be broken up in chronological order:
// bad func (customer *Customer) statement() string { totalAmount := float64(0) frequentRenterPoints := 0 result := "Rental Record for " + customer.Name + "\n" for _, rental := range customer.rentals { thisAmount := float64(0) switch rental.PriceCode { case REGULAR: thisAmount += 2 case New_RELEASE: thisAmount += rental.rent * 2 case CHILDREN: ThisAmount += 1.5} frequentRenterPoints += 1 totalAmount += thisAmount} result += Strconv. FormatFloat(totalAmount,'g',10,64) + "\n" result += strconv.Itoa(frequentRenterPoints) return result} // good Func Statement (custom *Customer) string {bill := calcBill(custom) statement := bill.print() return statement} type RentalBill struct { rental Rental amount float64 } type Bill struct { customer *Customer rentals []RentalBill totalAmount float64 frequentRenterPoints int } func calcBill(customer *Customer) Bill { bill := Bill{} for _, rental := range customer.rentals { rentalBill := RentalBill{ rental: rental, amount: calcAmount(rental), } bill.frequentRenterPoints += calcFrequentRenterPoints(rental) bill.totalAmount += rentalBill.amount bill.rentals = append(bill.rentals, rentalBill) } return bill } func (bill Bill) print() string { result := "Rental Record for " + bill.customer.name + "(n" for _, rental := range bill.rentals{ result += "\t" + rental.movie.title + "\t" + strconv.FormatFloat(rental.amount, 'g', 10, 64) + "\n" } result += "Amount owed is " + strconv.FormatFloat(bill.totalAmount, 'g', 10, 64) + "\n" result += "You earned + " + strconv.Itoa(bill.frequentRenterPoints) + "frequent renter points" return result } func calcAmount(rental Rental) float64 { thisAmount := float64(0) switch rental.movie.priceCode { case REGULAR: ThisAmount += 2 if rental. DaysRented > 2 {thisAmount += (rental. DaysRented) - 2 * 1.5} thisAmount += float64(rental.daysRented) * 3 case CHILDRENS: ThisAmount += thisAmount += (tenner.daysrented) - 3 {thisAmount += (tenner.daysrented) - 3 * 1.5}} return thisAmount } func calcFrequentRenterPoints(rental Rental) int { frequentRenterPoints := 1 switch rental.movie.priceCode { case NEW_RELEASE: if rental.daysRented > 1 { frequentRenterPointst++ } } return frequentRenterPoints }Copy the code
Layer Decomposition:
// bad func findSphericalClosest(lat float64, lng float64, locations []Location) *Location { var closest *Location closestDistance := math.MaxFloat64 for _, location := range locations { latRad := radians(lat) lngRad := radians(lng) lng2Rad := radians(location.Lat) lng2Rad := radians(location.Lng) var dist = math.Acos(math.Sin(latRad) * math.Sin(lat2Rad) + math.Cos(latRad) * math.Cos(lat2Rad) * math.Cos(lng2Rad - lngRad) ) if dist < closestDistance { closest = &location closestDistance = dist } } return closet } // good type Location struct { } type compare func(left Location, right Location) int func min(objects []Location, compare compare) *Location { var min *Location for _, object := range objects { if min == nil { min = &object continue } if compare(object, *min) < 0 { min = &object } } return min } func findSphericalClosest(lat float64, lng float64, locations []Location) *Location { isCloser := func(left Location, right Location) int { leftDistance := rand.Int() rightDistance := rand.Int() if leftDistance < rightDistance { return -1 } else { return 0 } } closet := min(locations, isCloser) return closet }Copy the code
annotation
Comments should not duplicate the work of the code. The mapping between the code model and the mental model should be explained and why the code model should be used. The following example is an example of how not to use the code model:
// bad
/** the name. */
var name string
/** the version. */
var Version string
/** the info. */
var info string
// Find the Node in the given subtree, with the given name, using the given depth.
func FindNodeInSubtree(subTree *Node, name string, depth *int) *Node {
}
Copy the code
The following examples are positive textbooks:
// Impose a reasonable limit - no human can read that much anyway
const MAX_RSS_SUBSCRIPTIONS = 1000
// Runtime is O(number_tags * average_tag_depth),
// so watch out for badly nested inputs.
func FixBrokenHTML(HTML string) string {
// ...
}
Copy the code
Shorten Process
To Shorten Process means to Shorten the Process by which the human brain “compiles code”. Avoid writing long, convoluted code that looks like a lost mouse. Long and convoluted code can be traced across expressions, across multi-line functions, across member functions, across files, across compilation units, or even across code repositories.
The corresponding methods include: introducing variables, splitting functions, returning early, and narrowing the scope of variables. The ultimate goal of these methods is to give the brain a break and not keep track of it for too long. Again, let’s look at some specific examples:
example
In the following code, multiple compound conditions are put together, and you may not be able to see exactly which conditions are true and which are false.
// bad func (rng *Range) overlapsWith(other *Range) bool { return (rng.begin >= other.begin && rng.begin < other.end) || (rng.end > other.begin && rng.end <= other.end) || (rng.begin <= other.begin && rng.end >= other.end) }Copy the code
But break it down and treat each condition individually. So the logic is clear.
// good
func (rng *Range) overlapsWith(other *Range) bool {
if other.end < rng.begin {
return false // they end before we begin
}
if other.begin >= rng.end {
return false // they begin after we end
}
return true // Only possibility left: they overlap
}
Copy the code
Here’s another example. When you start writing code, you might just have an if… else… Then PM asked you to add permission control, so you can happily continue to set a layer of if in the if. When the patch is finished, the code will look like this:
Func handleResult(reply * reply, userResult int, permissionResult int) { if userResult == SUCCESS { if permissionResult ! = SUCCESS { reply.WriteErrors("error reading permissions") reply.Done() return } reply.WriteErrors("") } else { reply.WriteErrors("User Result") } reply.Done() }Copy the code
This kind of code is also easier to change, generally reverse write if condition return judge no logic can be:
// good func handleResult(reply *Reply, userResult int, permissionResult int) { defer reply.Done() if userResult ! = SUCCESS { reply.WriteErrors("User Result") return } if permissionResult ! = SUCCESS { reply.WriteErrors("error reading permissions") return } reply.WriteErrors("") }Copy the code
The code problem with this example is a bit more subtle. The problem is that everything is placed in the MooDriver object.
// bad
type MooDriver struct {
gradient Gradient
splines []Spline
}
func (driver *MooDriver) drive(reason string) {
driver.saturateGradient()
driver.reticulateSplines()
driver.diveForMoog(reason)
}
Copy the code
A better approach is to minimize global scope and use context variables instead.
// Good ExplicitDriver struct {} // Use context pass func (driver *MooDriver) Drive (Reason String) {gradient := driver.saturateGradient() splines := driver.reticulateSplines(gradient) driver.diveForMoog(splines, reason) }Copy the code
Isolate Process
The defect of the human brain is that it is not good at tracking multiple things at the same time. If it “tracks” multiple changes of things at the same time, it does not conform to the structure of the human brain. But putting logic in a lot of places is also bad for the brain, which needs to scramble to see all of it. So there’s a classic line that every college computer science student has heard. Your code should be highly cohesive and low coupled, and that’s awesome! -_ – | | |, but you have to ask people what is saying this high cohesion and low coupling, he may have to consider, here is to consider the by some examples.
Let’s start with the metaphysics. If you write your code like this, it won’t be very readable.
In general, we can try to modify the code to look like this, depending on the business scenario:
To give a few examples, the following code is very common, where version means that different versions on the client need different logic processing.
func (query *Query) doQuery() { if query.sdQuery ! = nil {query. SdQuery. ClearResultSet ()} / / version 5.2 control. If the query sd52 {query. SdQuery = sdLoginSession.createQuery(SDQuery.OPEN_FOR_QUERY) } else { query.sdQuery = sdSession.createQuery(SDQuery.OPEN_FOR_QUERY) } query.executeQuery() }Copy the code
The problem with this code is that multiple code flow logic Merge due to version differences, resulting in a bifurcation phenomenon in the middle of the logic. It is also simple to encapsulate an Adapter, pull the version logic out of an interface, and implement the specific logic based on the version.
For another example, the following code also forks depending on the logic of expiry and maturity, so your code will look something like this:
// bad type Loan struct { start time.Time expiry *time.Time maturity *time.Time rating int } func (loan *Loan) duration() float64 { if loan.expiry == nil { return float64(loan.maturity.Unix()-loan.start.Unix()) / 365 * 24 * float64(time.Hour) } else if loan.maturity == nil { return float64(loan.expiry.Unix()-loan.start.Unix()) / 365 * 24 * float64(time.Hour) } toExpiry := float64(loan.expiry.Unix() - loan.start.Unix()) fromExpiryToMaturity := float64(loan.maturity.Unix() - loan.expiry.Unix()) revolverDuration := toExpiry / 365 * 24 * float64(time.Hour) termDuration := fromExpiryToMaturity / 365 * 24 * float64(time.Hour) return revolverDuration + termDuration } func (loan *Loan) unusedPercentage() float64 { if loan.expiry ! = nil && loan.maturity ! = nil {if loan.rating > 4 {return 0.95} else {return 0.50}} else if loan.maturity! = nil { return 1 } else if loan.expiry ! = nil {if loan.rating > 4 {return 0.75} else {return 0.25}} Panic ("invalid loan")}Copy the code
The best practice for solving multiple product logic is Strategy pattern, as shown in the following figure. Create different policy interfaces according to the product type, and then implement the methods Duration and unusedPercentage respectively.
// good type LoanApplication struct { expiry *time.Time maturity *time.Time } type CapitalStrategy interface { duration() float64 unusedPercentage() float64 } func createLoanStrategy(loanApplication LoanApplication) CapitalStrategy { if loanApplication.expiry ! = nil && loanApplication.maturity ! = nil { return createRCTL(loanApplication) } if loanApplication.expiry ! = nil { return createRevolver(loanApplication) } if loanApplication.maturity ! = nil { return createTermLoan } panic("invalid loan application") }Copy the code
But the reality is not so simple, because different things in your eyes is a multi-process multi-threaded operation, such as the product of logical example above, although with some design patterns to perform logic isolation in different places, but the code, as long as the contain a variety of products in code when executed or there will be a product selection process. Logic happens at the same time, in the same space, so the words naturally need to be written together:
- A concurrent process may occur when multiple information needs to be displayed during function presentation
- When writing code, the business includes functional and non-functional requirements, as well as normal and exception logic handling
- When considering runtime efficiency, we consider asynchronous I/O, multithreaded/coroutine for efficiency
- Merged Concurrent processes are also created when process reuse is considered due to version differences and product policies
For a mix of functions, such as the RenderPage function above, the solution is not to lump everything together, but to bring the pieces together and couple them together as a unit.
For multiple SIMULTANEOUS I/O operations, coroutines can be used to separate the kneading process:
Func sendtoffffiles () {httpSend("bloomberg", func(err error) { if err == nil { increaseCounter("bloomberg_sent", func(err error) { if err ! = nil { log("failed to record counter", err) } }) } else { log("failed to send to bloom berg", err) } }) ftpSend("reuters", func(err error) { if err == DIRECTORY_NOT_FOUND { httpSend("reuterHelp", err) } }) }Copy the code
For this kind of concurrent I/O scenario, the best solution is to write a separate evaluation function for each function, where the code actually runs “simultaneously,” but separately in the code.
Func sendToPlatforms() {go sendToBloomberg() go sendToReuters()} func sendToBloomberg() {err := httpSend("bloomberg") if err ! = nil { log("failed to send to bloom berg", err) return } err := increaseCounter("bloomberg_sent") if err ! = nil { log("failed to record counter", err) } } func sendToReuters() { err := ftpSend("reuters") if err == nil { httpSend("reutersHelp", err) } }Copy the code
Sometimes the logic must be incorporated into a Process, such as when buying or selling a commodity and logically checking the parameters:
// bad func buyProduct(req *http.Request) error { err := checkAuth(req) if err ! = nil { return err } // ... } func sellProduct(req *http.Request) error { err := checkAuth(req) if err ! = nil { return err } // ... }Copy the code
The classic way to do this is to write a Decorator to handle the permission validation logic alone, then wrapper the formal logic:
Func init() {buyProduct = checkAuthDecorator(buyProduct) sellProduct = checkAuthDecorator(sellProduct)} func checkAuthDecorator(f func(req *http.Request) error) func(req *http.Request) error { return func(req *http.Request) error { err := checkAuth(req) if err ! = nil { return err } return f(req) } } var buyProduct = func(req *http.Request) error { // ... } var sellProduct = func(req *http.Request) error { // ... }Copy the code
Your code should look something like this:
Of course, common logic does not only exist in the header. If you think about the so-called strategy and Template pattern, they do such logical processing elsewhere in the logic.
This one has a new concept called signal-to-noise ratio. SNR is a relative concept of information that is useful to me; Noise. It’s not gonna work for me. What logic the code should put together depends not only on who the reader is, but also on what the reader is hoping to accomplish at the time.
Take the following C++ and Python code:
void sendMessage(const Message &msg) const {... } def sendMessage(msg):Copy the code
If you’re doing business development right now, you might find Python code to read cleanly; But if you’re doing some performance tuning now, the C++ code will definitely give you more information.
For example, the following code, from the business logic, the development seems very clear, is to go through the book to get Publisher.
for _, book := range books {
book.getPublisher()
}
Copy the code
However, if you look at the following SQL log, you are confused, thinking that the OOM is really **, really is executing SQL line by line, this line of code may cause the DB alarm, your DBA colleagues up in the middle of the night to fix the DB.
SELECT * FROM Pubisher WHERE PublisherId = book.publisher_id
SELECT * FROM Pubisher WHERE PublisherId = book.publisher_id
SELECT * FROM Pubisher WHERE PublisherId = book.publisher_id
SELECT * FROM Pubisher WHERE PublisherId = book.publisher_id
SELECT * FROM Pubisher WHERE PublisherId = book.publisher_id
Copy the code
So if you change your code to something like this, you’ll probably understand a little bit better that this code is actually calling entities in a loop.
for _, book := range books {
loadEntity("publisher", book.publisher_id)
}
Copy the code
To sum up:
-
First try to give each Process its own function rather than merge it together
-
Try disassembling the interface into components
-
Try to split the order into multiple documents and track multiple processes independently
-
Try to express concurrent I/O using coroutines instead of callbacks
-
If you have to deal with multiple relatively independent things in a Process
-
Try copying a copy of code rather than reusing the same Process
-
Try an explicit insert: state/ adapter/ strategy/template/ visitor/ Observer
-
Try implicit insertion: decorator/ AOP
-
Improving the SIGNal-to-noise ratio is relative to a specific target. Improving the signal-to-noise ratio of one target reduces the signal-to-noise ratio of another target
3. Summary
When making fun of the code’s poor readability, don’t simply chalk it up to a lack of comments or poor readability. Instead, use the following image to identify the code’s problems and try to improve it.
Recommended reading
Why alibaba’s programmer growth rate so fast, read their internal data I understand
What you don’t know about violent recursive algorithms
Three things to watch ❤️
If you find this article helpful, I’d like to invite you to do three small favors for me:
Like, forward, have your “like and comment”, is the motivation of my creation.
Follow the public account “Java Doudi” to share original knowledge from time to time.
Also look forward to the follow-up article ing🚀