Follow me on my blog shymean.com

The draft of this blog was created on 15/10/17, and every time I write something to post, I always feel that something is missing. Now that I’ve been writing code for four or five years and working on many projects, from simple activity pages to complex business logic, it’s time to reflect on some of the bad code I’ve written.

Here is the text.

Every time I look back at my code from a month or two ago, I feel like it sucks. At first I thought IT was because I didn’t master the gestures of efficient programming, such as unit testing, breakpoint debugging, etc. After reading some books recently, I stopped and thought: Avoiding bad code isn’t about how much programming skills you have, or how many language features you know. It all starts with “how do you feel about writing code?”

Reference:

  • The wisdom of programming – Wang Yin
  • State-of-the -art-shitcode
  • All that crap about bad code, it’s really good
  • How do bugs come about
  • Ideal application framework, very interesting

What is bad code

Everyone’s personality, skill level and work experience are different, and the criteria for “good Code versus bad Code” are also different. Below is a well-known picture of Code quality in Code Review

Logic confusion, complex implementation

Code readability is poor, mainly including

  • All kinds of magic numbers, strange variable names
  • There is no annotation or annotation meaning is vague, often words do not express meaning, ask just know is forgot to update annotation
  • Chaotic code structure, conditional judgments, spaghetti code, nested callbacks
  • Redundant and complex implementations, such as “the four ways to write anise”, will not help the whole system to make a qualitative leap
  • Not concise, unable to write concise and elegant code due to personal limitations; Or they are not familiar with the library and framework used in the system. They do not know that they have similar functions, and they have implemented a relatively complex package

That’s pretty much how I was when I started writing code for the first year, when jQuery was popular and the page was full of $xx.parent. Parent. Find (xx).nextsiblings. Code is used to describe logic, and messy code is a source of bugs.

Today, with IDE or Lint tools, it’s possible to avoid all sorts of weird writing and keep your code style consistent.

Not robust enough to test

We can’t predict how the product will change, or how users will use the product, and when the application doesn’t cover all the scenarios, there will be bugs, right

  • Various unconsidered boundary cases, external dependency exceptions, and occasional errors or crashes
  • Fixed one bug and introduced 10 more
  • Lack of exception handling makes it difficult to locate bugs

Error handling, at its most basic, is using try… Catch, and I’ve seen a lot of misused code in historical projects.

try… Catch does not mean that we swallow errors, but rather that the system does not crash if we fail to override them while coding. The absence of obvious errors does not mean that there are no errors. On the contrary, if an error is silently caught, we cannot get the real problems of the system.

One measure of robustness is unit test coverage: if you’re afraid of bugs, you should let them appear early, and the easiest way is to test them.

We all know the benefits of unit testing, but I’ve worked on projects that rarely see test cases, except for a few base tool and component library projects. Even the projects I’m currently working on are often patched in with basic test cases as an afterthought, and coverage is pitifully low.

On the one hand, the development task is relatively tight, and there is no time to write test cases in advance;

On the other hand, historical code has many places that contain global variables or external dependencies, making it difficult to unit test

// To test the following method, you also need to mock the external variable name
function test(){
	return `hello ${name}`
}

// Conversely, if the logic of a function depends only on arguments, it is easy to test
function test2(name){
	return `hello ${name}`
}
Copy the code

The simplest judgment is this: The more dependencies, the harder it is to test. Therefore, you also need to ensure good module partitioning and avoid circular dependencies, which brings up another topic: how to layer project code

Difficult to maintain

“Maintenance” means fixing old code and developing new features, and “difficult to maintain” means it is difficult to modify old code and add new features. All the features of bad code end up being hard to maintain, and code that is easy to maintain needs to meet the following requirements:

  • The clarity of the process, whether it’s backwards from the results (such as the view presentation) or backwards from the entry (initializing the application), allows maintainers to understand the flow of the entire code
  • It’s easy to find things to change, to find an if judgment in a 1000-line function and then change it
  • Maintainers can clearly assess the scope of the change, and there are no omissions
  • Maintainers can clearly assess the scope of the change without unintended consequences
  • Has a good code style and maintains consistent style and maintainability after each change
  • Easy to expand, easy to add features

A lot of code in the initial version, often is clear and legible, with the function iteration, requirements change, gradually deviate from the original design, and finally become bad code. Why is my code rotting? This is also the main thinking and exploration of this paper

When I first started, I heard a common joke about the product: this requirement is very simple, I don’t care how to achieve it.

From the perspective of development: the product does not know technology, and he does not know that XXX is not designed in this way in my code. It seems that only XXX is needed to realize this requirement. In fact, I need to change many places and return to Balabala.

From the point of view of the product: I am most familiar with the functions of the entire application. According to the previous product design, this change is logical and should be implemented soon.

Which begs the question: What went wrong?

Recently, after I had developed a feature, I was demonstrating it to the product in the local development environment and confirming the flow of the feature, when I suddenly had some confusion:

Why did IT take me a day to develop and two or three minutes to demonstrate? After all, it’s not like “enter a URL into the address bar and display the entire page”, which takes two seconds to implement.

I kind of get the idea of this need

  • It takes a day to develop, which is a real hassle
  • The feature demo only took two minutes and there were no major changes, so it was pretty simple

Why, then, does development take so much time to modify a simple requirement that the product understands? Is there a difference between code design and product design?

On the other hand, the question now becomes: Why is it so hard to maintain the code we write? Is there a way to fully reverse-engineer a product design, where features can be easily added and modified?

Structured programming habits

When I first started learning to code, I was so upset that I didn’t have a clue what to do with it that I even read books like Think Like a Programmer (which was good in itself, of course). I learned the easiest way to do this: figure out what the code is going to do, write XX first, then XX, and that’s it.

The logic of programming is actually a description of the specific process of executing a product requirement, with conditions or loops that may be used in the process until the final goal is achieved.

At a certain point in the history of software engineering, as requirements became more and more complex, they introduced structured programming, advocating only sequential, selective, and repetitive structures to express logic, and discarding Goto; It’s a great innovation, and with just these three structures, you can do most of the logic.

Structured programming requires us to

  • If you want to understand the logic of a piece of code, you need to start at the entry point
  • If you want to add or remove a feature, you need to find where the logic is in the code in a structured order and make changes.
  • And with each change, the structure of the code is affected

However, the actual business process may be very long, and even cross-project and multi-personnel joint maintenance, such as client -> server -> RPC service -> server -> client. The code we see in front of us may only be the tip of the iceberg, which leads to the difficulty of sorting out the whole code process, resulting in “only in this mountain, Cloud deep do not know place “do not know how to start the situation.

The following pseudocode describes the process of “making mapo tofu”

Prepare tofu and bean paste () Fire () heat () oil () stir-fry () plate ()Copy the code

It looks very clear, and then add some judgment, dealing with the lack of ingredients and the salty taste

Prepare tofu and bean paste () + if no ingredients then buy ingredients () Fire () Put oil () stir-fry () Taste bad: + if light then put salt () + elif salty then put tofu () stir-fry () Plate ()Copy the code

It seems barely legible, until we gradually add some special logic to it

# Special treatment Xiaoming + If Xiaoming then prepare tofu and Pixian bean paste () + else Prepare tofu and bean paste ()Copy the code

As this kind of modification increases, the linear structure becomes submerged in logic large and small. While this code works for the business, it takes longer than the previous change to find the changes that need to be made, and the impact of one change will add up to the next.

This is why a simple feature becomes difficult to maintain when new features and special logic are added one after another.

Accumulated technical debt

Each piece of our code was written in a particular scenario: it might be well-scheduled, well-tested; Or it could be an AD hoc hack, tackling a particular problem on fire. It could be that you’re in a good mood at the time, or that you’re not working. I believe most of my peers have a basic work ethic, and at least in my career I haven’t seen a bunch of bugs written intentionally out of venting, revenge, or other personal reasons.

However, for various reasons, at some point, he knew that the code was not elegant, but he could fulfill the requirements, so he finally submitted, and at most wrote down a Todo for comfort. This creates a technical debt, and technical debt is hard to pay off, as the code sits quietly in a commit somewhere, waiting to be refactored or taken offline to end its mission

With a long history of technical debt, if bad code is allowed to live in the system for fear that changes will cause the system to crash, over time, the technical debt will grow.

“As long as I’m not doing it, I’m not going to make a mistake. If it’s working, why change it?” This mindset can also affect how we pay off our technology debt.

I always do not resist to modify the old code, and even have a sense of shame for the bad code I wrote. When I see it, I will try to optimize it. However, my personal energy is always limited, and some technical debt takes a lot of time, but it does not change the business.

As a result, most business code is instances over time, with more and more technical debt, more and more volume, more and more difficult to add new features, and more and more likely to break. In the end, there may only be a way of “refactoring”. However, refactoring is not a panacea. It will often die due to factors such as manpower, time and profit

I remember seeing some thinking about architecture that allows code to get better with maintenance, but in terms of “technical debt,” it would be difficult to rely solely on architecture to keep programmers from leaving technical debt. The only thing you can probably do is beg your former colleagues to leave less of a hole, and ask yourself to leave less of a hole for your later colleagues.

A sense of trust in old code

The simplest code in the world is the code you just wrote. The worst code in the world is the code you wrote a year ago.

It’s common to hear old code complaints like “Who wrote this code? It’s rubbish”, as if the developer and maintainer are on opposite sides of the fence, as if they were themselves a year ago and now.

Distrust of historical code can also lead to code design being broken, the equivalent of another kick in the door frame. There’s no doubt that DRY is right, and we treat it as a dogma and hate duplicate code. But when maintaining a legacy project, we might be afraid to reuse previous code,

  • I feel that I can’t understand the code written before and can’t maintain it. I have to write a new one
  • If you don’t know what the previous code depends on, it’s better to rewrite it than to be afraid of bugs

Then I had to add a bunch of code that I at least understood. But if this code can still be maintained by the people behind it, chances are they won’t trust our code either, over and over again.

What accounts for this lack of trust? The main reason is that we don’t know the exact scenario for writing old code.

From the maintainer’s point of view, if the logic is unclear or forgotten, it’s hard to untangle the context of old code unless you dig into it from scratch. I used to fix an old piece of code that I thought was bad, and at the end of it I thought, “Oh, he needs to do that because of XX,” and rolled it back.

Test cases seem to be an effective way to maintain trust. If there are test cases, we can run through the changes to see what doesn’t pass, so we can pinpoint the impact of our changes (if coverage is high enough), but unfortunately, a lot of historical code probably doesn’t have test cases.

CSS is not a programming language, but it’s a good example to illustrate this distrust.

Before CSS Modules or CSS Scoped, the styles of the entire application are globally scoped. If we were to implement a.title class, we would need to search the history stylesheet globally to see if the.title class already exists. Otherwise, style conflicts may occur, or other styles may be affected.

To be lazy, we can use CSS weight calculation rules for style overwriting, plus! Important or overwrite it with a tag like.xxx. title, so that the community comes up with various solutions such as BEM naming conventions to solve this situation.

There are similar trust issues with hierarchical attributes like zIndex. To avoid my popover being affected by a style in the corner of the code, I will write 9999… N nines, no matter what happens next.

Poor encapsulation

We can’t predict how code will change, but we can write code that is easy to maintain. How do we measure “maintainable” code from a maintainer’s perspective?

For a long time, I believed that code was “maintainable” as long as there were few changes. With this in mind, I made a lot of deliberate attempts at coding, such as

  • Reduce duplication of variables and manage global variables through configuration files
  • Reduce code duplication, encapsulate functions, encapsulate modules
  • Reduce logic duplication and encapsulate components

The best way to reduce change is to encapsulate unified logic. The core concept of encapsulation is to separate the parts of the system that are constantly changing from the parts that are stable

  • By physically restricting blocks of code that share the same functional logic together for easy lookup, subsequent changes will change less code
  • “Every elegant interface has a dirty implementation behind it,” and maintainers can write good code without worrying about dirty implementations
  • Encapsulation reduces the use of global and free variables and makes it easier to test

According to my understanding, encapsulation is to reuse code, but later found, often unconsciously, encapsulation and easy to maintain “run counter to the purpose of, occasionally need to modify the already packaged good code, such as to add a function parameter, give component module and exposed several methods, plus some prop, do some additional judgment, Then there will be “less change, does not mean less impact” situation, some code affects the whole body, and then need to modify the unexpected code.

Therefore, poor packaging is also the cause of affecting maintainability. Next, we’ll discuss some of the problems encountered in using encapsulation

To encapsulate

Now that we need to encapsulate a commodity component, we have two ideas

  • One is to pass in some query conditions according to the support, the component first queries the goods, and then displays, equivalent to the component needs to be responsible for the query and display
  • The other is to accept only one commodity parameter, which the caller queries and passes in by itself, equivalent to the component being only responsible for presentation

For code reuse, I’ll most likely use the first approach, which encapsulates seemingly generic logic. In some scenarios where goods are passed in directly, a second parameter is exposed and the query interface is no longer requested if this parameter is found.

This affects my practice of encapsulation and can force some seemingly repetitive logic (such as request interfaces handling responses) to be encapsulated, ignoring the impact of business changes. As a result, in different business scenarios, in order to adapt to the particularity of each logic, additional if.. The else.

I later learned that encapsulation is not about splitting code into different functions, classes, or files from a physical location, but rather about conceptually well-defined inputs and outputs.

In the example above, the logic shown is unchanged, but the change is in how the item details are obtained, so the change should be split from the same.

For code to be encapsulated, we need to think about the source of the change and find the change first so that we can determine what can be encapsulated together. But there is no way to predict all business changes in advance, and there is no way to be sure that the common logic currently encapsulated will change in the future. What logic should be packaged together?

Low modification costs

When working with a framework, if a feature is difficult to implement, we think about how to implement it, not how to modify the underlying framework to meet our needs.

As a practical example, in mobile development, rem is used for screen adaptation in many scenarios. In order to reduce the time spent manually calculating REM units, the PostCSS community provides plug-ins such as PostCSS-Px2REM, which can automatically convert the PX calculation in the style sheet to REM

However, in some cases, we want the styles under certain files or files not to be converted, so we can use postCSS-px2REM-exclude. This plug-in allows us to specify the exclude parameter to ignore automatic unit conversion of certain files

However, if we need to automatically convert most of the px units in the same stylesheet and keep a few px units (such as border), the above exclude will not be sufficient. One way to HACK is to use PX (uppercase) instead of PX

The problem is that the fast code formatting provided in ides such as Webstrom may automatically convert PX to PX, which disables HACK methods. One way to preserve hacks is to use @function of SCSS,

// Util. SCSS requires that quick formatting not be used in this file!!
// Returns the original pixel unit
@function PX($px) {
  @return# {$px}PX
}
Copy the code

Although the tools will never cover all application scenarios, we will try to extend the functionality externally rather than modify the PostCSS-Px2REM plug-in to provide a similar functionality.

But the same situation in our project, why want to arbitrarily modify the encapsulated code? You know, you put in an argument, you put in an if judgment, right?

One reason is that, as mentioned above, we encapsulate the business that might change, inducing us to modify the encapsulated code

Another reason is that the wrapped code is written by us, and unlike the code in other frameworks or libraries that have natural isolation (such as front-end projects in node_modules), the inertia from structured programming can cause us to subconsciously modify the code, making the wrapper more vulnerable to breaking

Is there any way to constrain us from making changes to encapsulated code, or making them more expensive?

The simplest thing to do is to have a single responsibility. If the code doesn’t need to be changed, then we don’t need to change it all the time

The Single Responsibility principle requires developers to write code that has one and only one reason to change. If a class has more than one reason to change, it has more than one responsibility. If we have two or more reasons to change a piece of code (a class, an object, or a method), that code violates a single responsibility

Destroy the encapsulation

Encapsulation is meant to change in the business place in isolation, to the same place to encapsulate, this will give us an ability to quickly modify the code, only need to modify a certain place, will affect all dependent, looks very tempting to add general functional, during the period of rapid iteration, we tend to can not stand the temptation.

By doing this, we are likely to destroy the original meaning of encapsulation and introduce some other bizarre functionality, with the result that the logic of encapsulation is no longer universal.

At the end of the day, we failed to clearly delineate responsibilities and encapsulated “possible changes”, which induced us to change the encapsulated parts of the code.

There is currently a pure UI component that takes a specific data structure, Config, and displays it. There are currently 10 pages in use,

Now we have one more requirement: we need to report data when we click on this component, and we have two choices

  • Registering click events on each page that depends on this component handles the reporting logic
  • It was too much trouble to change 10 pages. Fortunately, it was packaged as a generic component and handled the data reporting within the component

Which course of action would you choose?

Assuming we choose the second option, which is obviously too easy this time, evaluate the day’s work hours, spend half an hour doing it, and the rest of the time can be done

Changed: We added data reporting functionality to the UI component

This component contains two functions: UI presentation and burying point reporting; When the component is required to display the UI, the data reporting function is silent, and this function may not be desired by the user.

It might be possible to add a prop, such as needReport, to control whether or not users need to report logs, which is obviously not a good idea.

Change: We added a needReport prop to the UI component to control whether it needs to be reported

Suppose you now have 15 places on the system that use this UI component, 10 of which require additional logging and 5 of which require only UI presentation

So according to the current design, we need to pass in 10 places :needReport=”true”, and in 5 places :needReport=”false”. Although we can omit some of the values by setting the prop default, we have now undoubtedly broken the universality of the component, This UI component has become unusable!! The consumer needs to know what functions this component has and what parameters need to be passed to control those functions.

Isn’t this component designed to just take a config and display it? Why is this happening?

Regardless of the objective chronological order of UI components and data reporting, we wrap-around some seemingly repetitive code in order to force it, and then add more and more parameters and judgments to satisfy the specific logic of each place.

When it comes to making changes to old code, it’s important to understand why the change happened so that you can determine where to put the change and whether the current change is justified. Adding new functionality to old code, in addition to affecting old code, also limits new code.

Suppose we could go back in time and choose the first option when given the “data report” function? It also doesn’t make sense to write the data reporting functionality every time you rely on the component without modifying the old code.

For situations like this where you need to dynamically add non-component logic-related functionality, perhaps you could use a decorator to encapsulate a higher-level component that needs to be logged and displayed in the UI?

There is no problem that can’t be solved with another layer of middleware, and if so, then another layer

There are several ways to dynamically extend functionality without modifying the original code

  • Inheritance, extending the functionality of a subclass without modifying the parent class

  • Blending is a method that extends objects directly, but when objects use multiple blends, it is not easy to trace the specific origin of a method, like water and ink mixed together, and it is difficult to separate them

  • Decorators or interceptors, called before or after the logic occurs, are easy to dismantle, with the disadvantage of not being able to modify the intermediate logic

Of course, there are problems with over-abstraction and encapsulation, where you need to drill down layer by layer to understand the full functionality of the entire component.

summary

So far, I have been less likely to write the very basic bad code, such as arbitrarily defined global variables, large sections of repetitive code, as well as work efficiency is ok, get the requirements almost no delay. In addition, OVER the years, I have learned SOLID principles on and off, read books on Code, Design Patterns and Refactoring, and tried to optimize in code. However, every time I look back at my old code, I still feel a little ashamed, and I worry that others will see my code and make fun of it. I understand the importance of elegant code, but real-world development often requires trade-offs and trade-offs in various scenarios. Is there really elegant code?

Now, I have deeply realized “software development is no silver bullet” the meaning of this sentence, is no longer the pursuit of perfect code, the code itself is to serve the business, satisfy the business which is more important than the writing the so-called “elegant code”, but as a writing code, and the pursuit of a little less code to be despised or very be necessary, After all, writing code is fun.

Attached: The average time of writing code in the last 7 days (including work tasks and personal project code, mainly my own code ha ha, there is less work in the company recently), source wAKA statistics

There were a lot of things that I wanted to write before I started writing, but I couldn’t fully express my feelings after revising many times. I had to end it hastily, and then update it irregularly and reflect on it.

Finally, do what you can to reduce the amount of bad code.