May 22, 2023

How to write good software

Cover image

I want to share my view on how to write good software.

It’s not a step by step instruction.

It’s just a general principles that can be applied to any language/technology you use.

Software development can be divided into three stages:

  1. Preparation
  2. Development
  3. Support

Preparation

During this stage, our focus lies in creating a comprehensive work plan and defining our desired outcomes. To achieve this, we must prioritize understanding the business requirements, addressing crucial questions such as the purpose, significance, and specific problem(s) we aim to solve. It is imperative to avoid unnecessary complexities and resist the temptation to develop a one-size-fits-all solution. Instead, we should direct our efforts towards effectively addressing current tasks using the most optimal approaches available. Our success relies on a profound comprehension of the project's business requirements. With these in mind, we can meticulously examine viable real-use cases (as opposed to speculative ones) and meticulously compose a technical specification. Subsequently, utilizing this specification, we can construct the project's architecture, break down tasks into manageable components, and meticulously devise an execution plan.

Various approaches exist to comprehend business requirements and analyze essential use cases:

  • Customer Journey Mapping: Customer Journey Mapping visually illustrates a user's complete experience when interacting with a product or service. It captures their interactions, emotions, and touchpoints throughout their journey, providing valuable insights into their needs, pain points, and potential areas for enhancement. By understanding the user's perspective, Customer Journey Mapping facilitates the development of user-centric solutions, ultimately elevating the overall user experience.
  • Domain-Driven Design (DDD): DDD emphasizes the close collaboration between domain experts and software developers. It aims to align software design with the domain model and language, resulting in a better understanding of the problem space and the delivery of software that accurately reflects the real-world domain.
  • Contextual Inquiry: Contextual Inquiry involves observing users within their natural environments as they perform tasks related to the software. This method offers valuable insights into user behaviors, needs, and challenges, effectively informing the design process and uncovering opportunities for improvement.

What to Do When Project Requirements Constantly Change?

Dealing with constantly changing project requirements is a normal and fairly common situation. It's essential to recognize that everything in this world evolves, and being prepared for it is crucial. Nonetheless, it is important to always have a clear understanding of the problem we are attempting to solve and the existing requirements and constraints. Without this understanding, it is premature to start writing software.

When requirements change throughout the development process, it is equally important to document those changes. Creating a written specification is instrumental in avoiding misunderstandings and ensuring a shared understanding of our approach and solutions. The responsibility of writing the specification lies with the developer, analyst, or technical lead—the individuals who possess a solid grasp of how the task should be accomplished. Generally, project managers should refrain from writing the specification unless they possess the necessary expertise.

If modifications need to be made to previously written code, it is crucial to approach refactoring thoughtfully, avoiding makeshift solutions or hasty fixes. It is imperative to carefully review the updated business requirements (specified in the document) and, based on that, meticulously plan the revised implementation. Anything that does not align with the updated concept should be rewritten to maintain consistency.

Implementation

So, all the preparatory work has been completed, and we have a clear understanding of what and how we want to implement. It is now time to proceed with actual programming.

First and foremost, it is crucial to establish a unified coding style for the project, especially since some languages/frameworks lack strict conventions in this regard. Before diving into writing the project code, it is essential to determine the coding style that will be used. This will ensure consistency and avoid code discrepancies.

Additionally, it is vital to adhere to the principles of software development. These principles can include:

  • Single Responsibility Principle (SRP): Each class or module should have a single responsibility or purpose. By separating concerns into smaller, focused components, we improve modularity and testability.
  • KISS (Keep It Simple, Stupid): Aim for simplicity in design and implementation. Keep the codebase as simple and straightforward as possible, minimizing complexity and unnecessary abstractions.
  • DRY (Don't Repeat Yourself): Avoid code and functionality duplication. Encapsulate common behaviors in reusable components or functions to promote code reuse and reduce maintenance overhead.
  • YAGNI (You Ain't Gonna Need It): Avoid implementing unnecessary features or functionality until they are actually required.
  • Composition over Inheritance: Favor composition, combining smaller components, over inheritance, extending existing classes. This approach enhances flexibility, reusability, and loose coupling between components.
  • Separation of Concerns (SoC): Divide the software into distinct modules or classes, with each responsible for a specific concern or functionality. This improves code organization, readability, and maintainability.
  • Law of Demeter (LoD): Also known as the principle of least knowledge, it states that an object should have limited knowledge and interactions with other objects. Avoid tight coupling between objects and instead communicate through well-defined interfaces.

Remember to consider design patterns such as Factory, Observer, Strategy, and MVC (Model-View-Controller) as they provide proven solutions to common software design problems. Familiarity with design patterns greatly assists in creating scalable and maintainable software architectures.

Furthermore, it is crucial to avoid reinventing the wheel unnecessarily. There are numerous services available that excel in performing specific tasks. Therefore, it is not worth investing significant time to save a mere $5 per month.

Writing tests is of utmost importance. Always allocate time for writing tests, especially for the most critical parts of the project. Make it a rule that a feature is considered incomplete until it has been thoroughly tested. When planning development, factor in the time required for writing tests. Tests help mitigate unforeseen issues and difficulties as the project progresses and new functionality is added.

Documentation is essential. Leave meaningful comments in complex parts of the code. This not only aids other team members (including new ones) in understanding the code but also helps you when revisiting the project after a substantial period.

Strive to write understandable code. Ensure that function and variable names reflect their purpose clearly. Avoid using "magic" numbers in the code to achieve desired results. Functions should be concise, straightforward, and serve a specific operation. Follow the principles of DRY, KISS, and SRP, as they significantly simplify working with the code.

Utilize version control systems (hopefully, by 2023, this is already common practice). Regularly commit code with meaningful comments. Commit code in small portions that address specific tasks. Include at least a brief description of the changes in pull requests. This greatly simplifies future work with the project's history and code changes.

Before committing code, thoroughly test everything locally. Avoid the temptation to cut corners and go through the entire workflow diligently. Test all variations meticulously. It is a common mistake for developers to assume that they have written everything correctly right away, but usually, something is overlooked. Local testing helps identify and rectify these issues before they are discovered by someone else.

Handle errors diligently and utilize bug trackers. I have witnessed numerous instances where developers either neglect to handle errors properly, silently ignore them, or return generic responses. Both approaches are incorrect and lead to difficulties in debugging when encountering bugs.

Support

Keep your system up to date.

Stay informed about the latest packages and libraries.

Perform regular code reviews and address technical debt promptly. Avoid postponing it indefinitely. Technical debt is akin to a snowball rolling down a hill. With time, it accumulates and grows at an accelerated pace. Eventually, it reaches a point where it's more convenient to rebuild the project from scratch rather than attempting to update and refactor it.

Monitor logs on a regular basis and promptly address any errors recorded in the bug tracker. Avoid accumulating or disregarding them. In addition to updating the code, also focus on the relevancy of the environment, whether it's the operating system version on the server or the package manager version on the client. Otherwise, when updating the project, you might encounter compatibility difficulties, wasting time debugging unnecessary issues.


originally posted on linkedin.com

CV