At the beginning when writing code, I always rolled up my sleeves and got to work as soon as I encountered a problem, essentially writing code while debugging and simultaneously designing the solution flow. However, this approach is simply too slow, often leading to writing and deleting code, effectively resulting in a poorly designed code structure requiring refactoring, which wastes a lot of time.
In fact, the most crucial aspect of coding is not the coding step itself, but rather Think/Design. Coding is merely the implementation of the design solution, equivalent to knowing Why and seeking How. Therefore, the most important part is the process of thinking and designing.
Currently, I believe writing code can roughly be divided into the following steps:
Step 1: Also the most important step, is to analyze the problem to be addressed as meticulously as possible. This essentially involves how to break a complex problem down into simple, manageable problems. Write down the purposes of each module, implementation priority, and how to combine them (another way to say this is writing pseudocode).
Step 2: Using the design from the previous step, test the logic of the design using a set of parameters to check for any issues. It’s best to identify a few quirky examples to correct any potential problems from step one.
Step 3: Use the designs from the previous two steps to easily construct a framework for implementation. The main task in this part is to implement (incrementally fill in) those small modules defined in the design.
Step 4: Combine those small modules into a whole, conduct detailed testing, and iteratively optimize each module.
Next are a few coding habits
1. Modularity
I strongly advocate for modularizing code, even if it is just as simple as encapsulating a feature into a function, it is much better than just piling logic together. Moreover, the most important aspect of modular programming is to improve code reusability. Even if there are issues in the designs from the first two steps, the logical corrections made can utilize these small modules for refactoring, saving a lot of time. After creating many wheels, it becomes programming centered around Ctrl+C and Ctrl+V ~(haha). Undeniably, expanding one’s code library significantly enhances efficiency.
The significance of modularization is to reduce overall complexity — combining several simple modules into a complex software using clear interfaces.
The philosophy of Unix is: A program should do one thing and do it well. I believe modularization is similar; a small module should only be responsible for one task and ensure it handles its responsibilities well.
2. Quickly implement a solution; do not optimize too early
Early optimization is also one of the major pitfalls. Under a good design, one should first quickly implement a viable solution and then gradually optimize, rather than expecting to produce a high-performance solution in one go (to avoid wasting time at the initial stage). Thus, it’s important to gradually uncover existing issues during implementation and optimize in a targeted manner.
At the start of implementation, many people fall into an optimization trap — “How can this piece of code be written this way? It’s inefficient; it should be balabala…”. I believe after quickly implementing a solution, one should only gradually optimize each module after confirming that the design logic has no major flaws (because at this point, the design logic and the roles of each module can be clearly defined, allowing us to modify/optimize our code under this framework, with the precondition of needing to ensure that it works correctly).
In short — Don’t show off your skills too early! Don’t show off your skills too early! Don’t show off your skills too early!
3. Beware of implementation dependencies and side effects
Additionally, developing good coding habits is very important. One point is to be alert to operations that depend on the implementation environment or have side effects. For example:
1 | vector<string> args={"1","111"}; |
Such code is terrible and should be avoided at all costs.
When writing code, one should avoid using features dependent on the implementation environment, such as the memory layout of classes in C++, or directly manipulating vptr, etc. One should strive to ensure that the code written is unambiguous and performs well across all platforms. Don’t write code that, while functioning, looks strange and poorly readable just to show off.
Moreover, one should have a good grasp of the operator precedence in the language (fundamentals). Otherwise, when faced with a series of operator combinations, it’s easy to be bewildered.
4. Avoid special casing
This is also very important. Sometimes while writing code, there may arise a situation where: the designed solution works in most cases, but a few edge cases fail testing. I believe this is the most headache-inducing part of coding, as it often results in embedding numerous if statements to check for edge cases, making the resulting code extremely ugly.
Bugs often hide in the code handling edge cases and the interactions of operations under different special circumstances. It should ensure code transparency — being able to see at a glance what’s going on. Therefore, a good design solution should be simple and transparent.
5. Interface design should avoid being idiosyncratic
This point is mainly to vent my frustration about the FString I saw in Unreal today.
In C++, the basic interfaces of various containers in STL are consistent — or rather, the naming of member functions implementing the same functionality is consistent. This allows you to clearly remember that size()
retrieves the number of elements in a container, begin()
gets the iterator for the first element, or swap()
exchanges two containers, etc., all reflecting design consistency, preventing confusion.
When we define a class (abstract type), the first thing to avoid is idiosyncratic interfaces, and it’s best to refer to some of the most common interfaces (preferably from the standard library) for naming to lower the learning and usage cost and unify the style.
6. Exit immediately and provide sufficient errors when an exception occurs
In C/C++, a common recommendation is to assign the pointer to the reclaimed dynamic memory to NULL(nullptr)
to prevent errors from operations on dangling pointers. However, there is a serious problem here — if the dynamically allocated memory has already been released, why would it still be accessed? If this occurs, there is certainly a design flaw. I do not recommend manually assigning a released dynamic memory pointer to NULL, as this can confuse you at the actual potential points of failure — leading you to mistakenly believe that no errors will occur, thus neglecting to correct the existing issues in the design. This kind of wall-splitting solution is not worth promoting.
Thus, if one encounters unexpected problems during testing, one should immediately exit and provide sufficient error information so that real issues can be addressed from the design perspective rather than performing patchwork on a system with hidden maladies.
7. List implementation priorities and prioritize core functionalities
When I first learned C++ and created some small console demos, I spent a lot of energy handling user inputs that could possibly be erroneous — because the usage of potential users is often inconsistent. Back then, I considered every possible erroneous reading right from the start (when gathering data) and excluded them one by one (essentially early optimization), which wasted a lot of time.
Now I feel that at the design outset, one should list implementation priorities. Simply put, one should determine which part to implement first, as some parts are comparatively less important. This allows the fastest possible implementation of a solution, rather than falling into the trap of early optimization.
Additionally, it is necessary to point out: good comments should indicate what function a piece of code should implement (the intent of the code), while the code itself is responsible for accomplishing that function (the method of completion). The best approach is for the language of the comments to maintain a high level of abstraction, making it easier to understand without getting too tangled in technical details.