avatarbo leo

Summary

A seasoned programmer reflects on the misconceptions and realities of a decade-long career in software development, emphasizing the challenges of writing good code, the creative nature of programming, and the importance of managing complexity.

Abstract

The author, with over ten years of experience in the tech industry, shares insights into the complexities of programming that transcend the initial excitement and perceived simplicity of coding. They dispel the myth that experience makes programming straightforward, highlighting the rarity of well-written code in the industry. The essence of programming is identified as creation, which fuels both professional growth and innovation. The article underscores the necessity of an environment conducive to efficient trial and error, the pitfalls of code perfectionism, and the often overlooked significance of understanding people in software development. The author advocates for continuous learning with a focus on cost-effectiveness and suitable materials, early adoption of unit testing, and the proactive management of complexity as key strategies to thrive in the field.

Opinions

  • Writing good code is a complex skill that does not become simple over time, and it involves readability, maintainability, and appropriate design choices.
  • The programming environment should facilitate efficient trial and error, akin to the experience of solving algorithmic problems on platforms like LeetCode.
  • Programmers should avoid the trap of perfectionism, as the pursuit of perfect code can be counterproductive.
  • People and organizational dynamics are crucial in software development, influencing the evolution and design of software systems.
  • Lifelong learning in programming should be strategic, focusing on cost-effectiveness and selecting learning materials that match one's current skill level.
  • Early adoption of unit testing is recommended for its benefits in code design, documentation, and creating a safe space for making mistakes.
  • Complexity is the greatest enemy of programmers, and it must be managed through clean code practices, appropriate use of design patterns, and systematic refactoring.

I Have Been in This Industry for 10 Years, and I Still Find Programming Difficult

Many years ago, when I was still a senior student majoring in computer science, I would browse various job postings online all day long, hoping to find a suitable internship position for programmers.

In addition to the internship positions, I occasionally clicked on some job postings for “senior engineers.” Looking back at those posts now, aside from the dazzling technical terms, the deepest impression on me was the requirement for years of experience stated in the first line: “This position requires at least 5 years of work experience.”

As a rookie who had never worked a single day before, these experience requirements seemed extremely exaggerated. However, while feeling overwhelmed and sighing with admiration, sometimes I would secretly fantasize in my heart: “Programmers with five years of work experience must be so amazing! Is coding as simple for them as eating?”

Time flies by and more than ten years have passed in an instant. Now looking back, I have also become an honorable worker with 10 years of work experience. After struggling and gaining hands-on experience in the software development industry since my senior year of college, I have discovered that many things are quite different from what I imagined back then. For example:

  • As experience grows, programming does not become much simpler. “As simple as eating” only happens in dreams.
  • Writing code for many “big projects” is not only boring but also dangerous. It is far more interesting to solve an algorithm problem on LeetCode.
  • Thinking about problems solely from a technical perspective cannot make one a good programmer. Some things are far more important than technology.

Upon further reflection, there are still many thoughts about programming. I have organized eight of them and written this article. If any of these viewpoints resonate with you, I would be very happy.

1. Writing code is simple, but writing good code is difficult

Programming used to be a highly challenging professional skill. In the past, the most common way for an ordinary person to learn programming was through textbooks and books. However, most programming textbooks were very difficult and obscure, making them unfriendly for beginners. As a result, many people gave up halfway before experiencing the joy of programming.

But nowadays, learning programming is becoming easier and easier. Learning is no longer limited to just studying books; there are now many new ways available. Watching instructional videos, participating in Codecademy’s interactive courses, or even directly learning programming by playing games on CodeCombat — everyone can find a learning method that suits them.

“Mom, I wasn’t playing games, I’m learning programming! Look at the right side of the screen!”

In addition, programming languages are becoming more and more user-friendly. Classic languages like C and Java are no longer the first choice for most beginners. Many simpler and easier-to-learn dynamic typed languages have become popular, and related tools such as IDEs have also become increasingly sophisticated. These factors further lower the learning barrier for programming.

In conclusion, programming has long shed its mysterious veil. It has transformed from a skill that only a few could master into an ordinary craft that anyone can learn.

However, lower learning barriers and friendlier programming languages do not mean that everyone can write good code. If you are already working and have participated in some projects, I would like to ask you a question: “What is the code quality of these projects you encounter daily? Is there more good code or bad code?”

I don’t know how you would answer this question, but let me share my answer first.

Good code is still rare

In 2013, I switched jobs to a large internet company.

Before joining this company, I had only worked in small companies with about ten people. Therefore, I had high expectations for the new company in all aspects, especially software quality. At that time, what I thought was probably like this: “This is a ‘big’ project that supports products with millions of users. The code quality must be leaps and bounds better than before!

It wasn’t until after working at the new company for a week that I realized how wrong I was. The code quality of the so-called “big” project was far from my expectations. Opening the IDE, there were hundreds of lines of functions and mysterious numeric literals everywhere; it felt like climbing Mount Everest to develop even a small requirement.

Later on, after working in more companies and being exposed to more software projects, I concluded: that regardless of how big or impressive a company or project may be, encountering good code in actual work is still an unlikely event.

What are the elements of good code?

Speaking of which, what kind of code is considered good code? In this regard, Martin Fowler has a quote that is often quoted by everyone:

“Any fool can write code that a computer can understand. Good programmers write code that humans can understand.”

I think it can serve as the starting point for evaluating good code: good code must be readable, easy to read, and easy to understand. The first principle of writing good code is to prioritize human readers.

In addition to readability, there are many other dimensions for evaluating the quality of code:

  • Fit programming language: Has the recommended writing style of the current programming language been used? Are the language features and syntactic sugar used appropriately?
  • Easy to modify: Has the code design considered future requirements changes? When changes occur, is the code easy to modify accordingly?
  • Reasonable API design: Is the API design reasonable and easy to use? A good API is convenient to use in simple scenarios and can be expanded according to needs in advanced scenarios.
  • Reasonable API design: Is the API design reasonable and easy to use? A good API is convenient to use in simple scenarios and can be expanded according to needs in advanced scenarios.
  • Avoid overdesign: Does the code suffer from the problems of overdesign and premature optimization?

In short, good code is not something that can be easily obtained by programmers at any level. Write good code, it requires repeated trade-offs and careful design in many dimensions, and finally continuous polishing.

So, if you want to quickly master the craft of coding, are there any shortcuts?

The shortcut to writing good code

On many levels, I believe that programming and writing are very similar. Both use text and symbols to express ideas, just in slightly different ways.

Speaking of writing, I want to ask a question about writers: “Have you ever heard of a writer who doesn’t read? Have you ever heard of a writer who says they never read other people’s works, only their own?” I guess the answer should be no.

If you look up relevant information, you will find that the daily lives of many professional writers revolve around reading and writing. They spend a lot of time every day reading various texts before they start writing.

Similarly as “word workers,” programmers often neglect reading. However, if we want to improve our programming skills quickly, reading is an indispensable part. In addition to the projects we encounter in our daily work, we should read more classic software projects and learn from them about API design, module architecture, and code-writing techniques.

Not only code and technical documentation but also regularly reading some computer-related professional books is recommended to maintain the habit of reading books. In this regard, I think Jeff Atwood’s article written 15 years ago titled “Programmers Don’t Read Books — But You Should” is still relevant today.

The shortcut to improving programming skills lies within the endless cycle between “reading <-> programming.”

2. The essence of programming is “creation”

In the daily work of programmers, many things can bring a sense of accomplishment and even make people exclaim “Programming is wonderful”. For example, fixing a difficult-to-locate bug, improving code performance by using a new algorithm, and so on. But among all these things, none can compare to the feeling of “creating something with your own hands”.

When you are programming, opportunities to create new things are everywhere. It’s not just about releasing new software. Writing a reusable utility function or designing a clear data model can also be considered as “creation”.

As programmers, it is crucial to maintain enthusiasm for “creation” because it can help us:

  • Learn more efficiently: The most efficient way to learn a new technology is to develop a real project using it, learning during the creation process yields the best results.
  • Encounter something extraordinary by chance: Many world-changing open-source software projects were initially created purely out of interest by their authors, such as Linus Torvalds and Linux, Guido van Rossum, and Python.
In the Christmas holiday of 1989, Dutchman Guido van Rossum typed the first few lines of code for the Python language. Initially, Python was only expected to be a successor to the ABC language, but later it “swallowed” the whole world.

Although there are many benefits to “creating” and programmers have plenty of opportunities to do so, many people often lack the consciousness of being a “creator”. Just like the widely circulated story says: a philosopher asked a bricklayer who was building bricks, some people know that they are building a cathedral, while others think they are just laying bricks. Many programmers are “only seeing bricks, not the cathedral”.

After positioning oneself as a creator, the way things are viewed will undergo earth-shaking changes. For example, when adding error prompt text to an API, creators can break free from the mindset trap of “just quickly meeting requirements” and take a step forward by asking themselves more important questions: “What kind of product experience do I want to create for users? What kind of error message would help me achieve that goal?”

Just like any useful programming pattern, “creator thinking” can also become a tremendous driving force in your career. Therefore, try asking yourself one question now — “What will be my next creation?”

3. Creating an environment for efficient trial and error is crucial

I have participated in the development of an internet product that is beautifully designed and rich in features, with a large number of users using it every day.

However, despite being a successful product from a market perspective, the engineering quality is very poor. If you open its backend project and turn all directories upside down, you won’t find any unit test code at all, let alone other automated testing processes. Moreover, the business logic is extremely complex. In the end, there are unexpected couplings between project codes like countless hairs on a cow’s back. Developing a new feature easily breaks old functionalities.

Therefore, every time the project is released, both the development and product teams have to be on high alert, creating a very tense atmosphere. The entire release process is also very thrilling, with emergency rollbacks occurring. Working in such an environment not only promotes technical growth but also undoubtedly tests one’s mental resilience.

Programming was originally a fun job, but programming for such a project eliminates any sense of enjoyment. What exactly has taken away the joy of programming?

The ideal programming experience ≈ “brushing questions” LeetCode is a famous programming learning website that provides many programming questions covering various difficulties, most of which are related to algorithms. Users can choose the questions they are interested in and directly write code (supporting dozens of programming languages) and execute it in the browser. If all test cases pass, it is considered a successful solution.

Doing problems on LeetCode.

It is like playing a game to solve problems on LeetCode, challenging and fun. The whole process of problem-solving perfectly demonstrates an ideal programming experience:

  • Separation of concerns: Each problem is an independent entity, allowing developers to fully immerse themselves in one problem at a time.
  • Quick and accurate feedback: Developers can quickly receive feedback on their code adjustments through automated testing.
  • Zero-cost trial and error: Writing code with syntax errors or logical issues does not have any adverse consequences, reducing psychological burden.

However, you may think I am talking nonsense in front of the screen.

What else? Solving algorithmic problems, writing small scripts, isn’t that the same experience?” You may continue to add, “Do you know how complex our company’s projects are? They are huge in scale with numerous modules. Do you understand what I mean? We serve millions of people every day. We have multiple sets of databases and three types of message queues. Of course, development can be troublesome!

Indeed, software development varies greatly worldwide; all projects can't be as easy and enjoyable as solving problems on LeetCode. However, this does not mean that we should not strive to improve our programming environment even if it’s just a little bit.

The concepts and tools available for improving the programming experience include:

  • Modular thinking: Properly design each module in the project, reduce coupling, and enhance orthogonality.
  • Design principles: At the micro level, apply classic design principles and patterns, such as the “SOLID” principle.
  • Automated testing: Write standardized unit tests, use Mock technology when necessary, and cover critical business paths with automated testing.
  • Shorten feedback loop: Switch to faster compilation tools, optimize unit test performance, and do everything possible to shorten the waiting time from “code modification” to “getting feedback”.
  • Microservices architecture: When necessary, split a monolithic system into multiple microservices with different responsibilities to distribute complexity.
  • ……

Focus on the programming environment and deliberately create a “code paradise” that allows efficient trial and error, making work as easy and enjoyable as solving problems. It is one of the best contributions experienced programmers can make to their teams.

4. Avoid the trap of code perfectionism

It is good to strive for excellence in code quality, but also be careful not to fall into the trap of perfectionism. Because programming is not artistic creation, it does not encourage people to pursue perfection indefinitely. Writers can spend years polishing a masterpiece, but programmers have a problem when they get too caught up in code.

There is no perfect code in the world. Most of the time, as long as your code meets current requirements and leaves room for future expansion, it is enough. There have been a few times when I saw candidates label themselves with “code OCD” on their resumes. Through the screen, I can feel their emphasis on code quality, but deep down inside me, I hope that they have already left the trap of perfectionism far behind them.

5. Technology is important, but “people” may be even more important

In the field of software development, the “Single Responsibility Principle” (abbreviated as SRP) is a very famous design principle. Its definition is simple and can be summarized in one sentence: “Each software module should have only one reason to change.”

To master the SRP principle, the key is to understand what the “reasons for modification” are. Programs have no life and cannot actively change themselves. Any reason for modifying a program comes from people related to it, and people are the main culprits behind modifications.

Take a simple example. Which of the following two classes violates the SRP principle?

  • A dictionary data class that supports two types of operations: storing data and retrieving data;
  • An employee profile class that supports two types of operations: updating personal information and rendering a user profile card image.

In most people’s eyes, there is no problem with the first example, but it violates the SRP principle in the second example. To reach this conclusion, it seems that no strict analysis or proof is needed; just a little intuition can be applied. However, if we do some serious analysis, there are two different suspicious reasons easily found for modifying the second example:

  • The manager believes that the “personal phone number” field in the data should not contain illegal numbers and requires adding simple validation logic.
  • An employee thinks that the “name” part on the profile card image is too small and wants to increase its font size.

”It is people who request changes. And you don’t want to confuse those people, or yourself, by mixing together the code that many different people care about for different reasons.” — — “The Single Responsibility Principle”

The key to understanding the SRP principle is to first understand people and the roles they play in software development.

Here’s another example. Microservices architecture has been a hot topic in recent years. However, many people often only focus on the technology itself when discussing it, but overlook the relationship between microservices architecture and people.

The key to distinguishing microservices architectural style from other things is that after splitting a monolithic system into independent microservices, the boundaries between different modules can become clearer. Compared to maintaining a monolithic system with hundreds of people in a team, many small organizations maintaining independent microservices have higher operational efficiency.

If a specific organizational scale (i.e., “people”) is lacking as a prerequisite, talking about various technological advantages of microservices and those fancy techniques would be completely misplaced.

Technology is certainly important. As technical professionals, those magnificent architecture diagrams and ingenious code details naturally attract our attention. However, please do not turn a blind eye to another important factor in software development — “people”. When necessary, change your perspective (from “technology” to “people”), which will greatly benefit you.

6. It is a good thing to thirst for knowledge, but one must also pay attention to the methods

Nowadays, everyone is talking about “lifelong learning”, and programmers are a profession that especially requires lifelong learning. Because the iteration and update of computer technology is very fast, a framework or programming language that was popular three years ago is likely to be outdated a month ago.

What will happen in one minute? Netflix viewing time increases by 70,000 hours; three million videos are viewed on Snapchat; Google adds 2.4 million searches; a new JS framework is invented (this one is not real 🤓).

To perform well in their work, programmers need to learn a lot of things that cover various aspects. Taking the backend field that I am more familiar with as an example, a qualified backend engineer needs to master at least the following:

One or more backend programming languages / relational databases such as MySQL / common storage components like Redis / design patterns / user experience / software engineering / compiler principles / operating systems / network fundamentals / distributed systems /…

Although there is a lot to learn, from my observation, most programmers enjoy learning (or at least do not reject it), so mindset is not an issue. However, sometimes having a “thirst for knowledge” mindset alone is not enough. When learning, we especially need to pay attention to the “cost-effectiveness”.

Focus on cost-effectiveness in learning

The following image shows the relationship between learning effectiveness and input.

Relationship between Learning Effectiveness and Input Graph, with the horizontal axis representing learning input and the vertical axis representing learning effectiveness.

From the graph, it can be seen that in the early stages of learning, when there is less input, the effectiveness increases rapidly. However, once the effectiveness exceeds a certain threshold, further improvement requires an exponentially increasing amount of learning input.

Because of this, I suggest that when you are learning something new, you should first think clearly in your mind: “Where should I stop on the graph?” instead of studying blindly.

The ocean of knowledge is vast and boundless. Some things require continuous learning and constant improvement over months or even years. There are also some things where acquiring superficial knowledge is sufficient. Accurately assessing and allocating one’s limited learning energy is even more important than striving to learn.

Select suitable learning materials

After setting learning goals, the next step is to find suitable learning materials. In this regard, I would like to share a personal experience of failure.

For some time, I suddenly developed a strong interest in product interaction design and believed that I should improve in this area. So, I carefully selected a very classic professional book in the field: “About Face 4: The Essentials of Interaction Design” and brought it home with confidence that my interaction design skills could quickly improve.

However, things didn’t go as planned. When I held that classic work in my hands, I found myself unable to even smoothly finish the first chapter — as the saying goes, “It’s like crossing mountains when changing professions.”

From this failure, I have summarized some experience. That is when learning something new, it is best for us to choose materials that are easier to read and more suitable for beginners instead of just going after the most classic and authoritative ones without considering our level.

Looking back on my previous experiences, I think the following books are very suitable for beginners learning and offer great value for money:

Perhaps everyone’s inner desire is to become a knowledgeable person, knowing everything. However, the time and energy available for allocation are always limited. We cannot and do not need to become experts in all fields.

7. The earlier you start writing unit tests, the better

I enjoy unit testing. I believe that writing unit tests has had a tremendous impact on my programming career. To exaggerate a bit, if we use “starting to write unit tests” as the dividing line, the second half of my career is far more exciting than the first.

There are many benefits to writing unit tests, such as driving improvements in code design and serving as a form of documentation for the code, among others. In addition, comprehensive unit testing is crucial for creating an environment where mistakes can be made efficiently.

I have written several articles about unit testing, such as “5 Suggestions for Unit Testing” and “The Game “Celeste” Taught Me the Principles of Programming” So here, I don’t intend to repeat myself. I’ll just say this: if you haven’t tried writing unit tests or haven’t given testing much importance so far, I suggest you start doing it from tomorrow.

8. What is the biggest enemy of programmers?

In most programmer jokes, product managers often appear as antagonistic characters. The project requirements in their mouths are always changing, with new ideas popping up every day, making the programmers extremely frustrated.

Against the backdrop of these jokes, the product manager who constantly modifies requirements seems to have become the programmers’ biggest enemy. It seems that as long as the product does not change its requirements, everyone’s working environment will immediately become a utopia.

Although occasionally making fun of the product manager is interesting, I still want to say seriously: The product manager is not an enemy.

Because from a certain perspective, the software is born to be modified (otherwise, guess why it’s called “soft” ware?). In this sense, developing software is completely different from building houses. Because no one would say after completing a building: “Let’s tear it down and rebuild it! Same building but with 30% less steel and cement!”

Therefore, the product manager and unstable requirements are not enemies of programmers. Moreover, being able to write code that is easy to modify and adapt to changes is one of the important criteria for distinguishing ordinary programmers from excellent ones.

So then what is programmers’ biggest enemy?

Complexity is the greatest enemy

Just like what is said in “Code Complete 2”: the core problem of software development is managing complexity. Uncontrolled complexity is the biggest enemy of programmers.

Let’s take a look at the factors that contribute to the continuous growth of project complexity:

  • Increasing the number of new features: More features mean more code and more code usually means higher complexity.
  • Requirements for high availability: Additional technical components and code, such as message queues, are introduced to achieve high availability.
  • Requirements for high performance: To improve performance, caching and related module codes are introduced. Some modules are split and rewritten in high-performance languages.
  • Postponed refactoring repeatedly: Due to tight project schedules, urgent refactoring tasks are repeatedly postponed, resulting in accumulating technical debt.
  • Neglecting automated testing: No one writes unit tests or cares about testing.

One day, when the complexity of the project reaches a certain level, there will be a loud noise in the air. “Thud!” A “big pit” that everyone is unwilling and afraid to change suddenly appears in everyone’s IDE.

Guess who dug this pit?

Slow down the process of increasing complexity

Although complexity will always inevitably continue to grow, many practices can slow down this process. If everyone can do the following things, it is possible to keep complexity under long-term control within a reasonable range.

  • Proficient in current programming languages and tools, writing clean code
  • Using appropriate design patterns and programming paradigms
  • Zero tolerance for duplicate code, abstraction libraries, and frameworks
  • Applying clean architecture and domain-driven design principles appropriately
  • Writing detailed documentation and comments
  • Writing well-structured unit tests according to specifications Separating the mutable from the immutable

The requirements may seem numerous, but to summarize, the core is just one sentence: write better code.

Written at the end

In 2021, I gave a presentation to the team. The title of the PPT at that time was “Eight Reflections After Eight Years of Programming”. After sharing the materials on the company’s intranet, a colleague saw it and commented that just looking at the PPT was not enough and hoped that I could expand it into an article. I replied saying no problem. Now, three years have passed, and I finally fulfilled my promise.

When preparing for the presentation back then, I completed the entire PPT but didn’t know what to put on the last page. So, with a sudden inspiration, I created a pure white background with a line of bold black text in the middle: “Ten years is short; programming is difficult.” Now, as we approach halfway through this second decade, it seems like the latter part of this sentence still applies to me — making little progress but continuing to work hard 😅.

Programming
Python
Pytest
Unittest
Recommended from ReadMedium