ideas Code standarts

Ideal software design

Our world isn't perfect, and we encounter very few things that meet our expectations completely. Developers may notice that as they progress in their careers, their code gets worse with time either because they look at it more critically or because ongoing maintenance undermines their initial good intentions. However, in this context, we should avoid subjective attitudes and instead look for objective indicators of good and high-quality code.

The principles of agile development provide us with some guidance. Code should be flexible to accommodate potential changes to specification requirements in the future. However, this may sound abstract. Predicting any future changes while trying to design solutions that survive the changes with minimal impact becomes challenging, particularly given the time and resource constraints we experience as developers. Incorporating the "You Ain't Going to Need It" (YAGNI) principle helps us to be more pragmatic by implementing only the essential functionality necessary to cover the identified use cases. Hence, ideal code should be pragmatic, resource-efficient, and easily extendable.

An ideal code is not over-engineered, excessively sophisticated, or unnecessarily fancy. Practices have shown that such code results in steep learning curves for new team members and prolonged implementation of simple tasks. It may also lead to temptations to take shortcuts that could compromise the code's integrity. Therefore, an ideal code must be simple enough to allow developers with diverse skills to work with it effortlessly. The code should be easy to comprehend and understand, regardless of the team member's experience or familiarity with the codebase.

An ideal code is far from the "spaghetti style" code. Rather, it embodies good practices of software architecture, adhering to SOLID principles, design patterns, and highly reusable components. SOLID principles ensure that code is modular, easy to understand, and maintainable. Design patterns provide proven solutions to common coding challenges, while reusable components help in reducing redundancy and ensuring scalability. Overall, an ideal codebase should be well-organized, easy to maintain, and adaptable to future requirements.

Ideal software is the ultimate compromise, striking a balance between business needs and principles of flexibility, simplicity, reusability, and software design best practices. The software should be adaptive enough to meet business requirements while maintaining simplicity, reusability, and adhering to best practices. Achieving this balance requires continuous feedback and collaboration between the development team and stakeholders throughout the development process. Ultimately, a successful software solution meets both the functional and technical requirements of the business, whilst being easy to maintain, adaptable to future needs, and built on sound software design principles.

This means achieving maximum code quality while minimizing resource costs.

On one hand, skimping on quality can lead to significant maintenance problems in the future, but on the other hand, investing an excessive amount of human hours into a perfectly structured software product may be a waste of money if certain features are unlikely to be utilized. The ideal software solution lies somewhere in the middle - pragmatically speaking, it is the result of efficient resource allocation (mainly maintenance time) in all foreseeable scenarios. This rule can be applied to any existing understanding of ideal software design.

Let us examine some attributes that contribute to making our code ideal.

Code Flexibility

Making minor changes in the specifications should result in minimal modifications in the code, which saves time during implementation. Sometimes, adding only a few lines of code can improve the existing functionality significantly. Other times, meeting new specifications does not require a lot of work. 

There is no one-size-fits-all solution for achieving code flexibility, as this largely depends on each company's specific requirements and business model.

To achieve good code flexibility, we can use several strategies, such as:

  • Designing our code structure and logical units (modules, functions, classes) to accurately correspond to the business entities, their relationships, and properties according to the domain model. In doing so, our code will evolve alongside changes made to the business model.

  • Developing specifications that include information about future plans for the associated features and requirements.

  • Respecting the Dependency Inversion Principle by clearly separating high-level units from their concrete implementations, enabling a flexible variation between implementations. For instance, if our business logic requires sending emails, it should not be coupled with a concrete sending implementation such as SMTP so that we can easily vary sending methods.

  • Continuously asking ourselves the question "What if this changes in the future, and how much effort will it take to adjust the implementation on the running system?" to ensure that we proactively incorporate the potential for future changes.

  • Respecting the Don't Repeat Yourself Principle, which involves avoiding repeating logical units that make it hard to adjust when the requirements change.

  • Following the Single Responsibility Principle to prevent "God" modules. This requires that each responsibility corresponds to a single quantum of possible change or simple business module, so that if we want to change invoice generation, it won't affect email sending or order processing.

  • Strictly separating code as business logic (high level), infrastructure, data (models, property objects, DTOs), UI, and IO to support clean code architecture and maintainability.

Code Reusability

Code reusability means using existing code instead of writing new code from scratch. This saves time and effort in implementing solutions. However, achieving high code reusability can be challenging because developers often create solutions that are specific to the task at hand instead of creating more general solutions that can be used across multiple tasks. This leads to repetition of code and functionality, which can be avoided by creating more abstract solutions.

Here are some ways to achieve better code reusability:

  • Follow the SRP (Single Responsibility Principle) and DRY (Don't Repeat Yourself) principles. Avoid repeating code and create smaller modules that can be easily reused.

  • All infrastructure communications such as sending emails, saving files, and storing data in cache are potentially reusable. Make sure to create reusable modules for these tasks.

  •  Maintain well-organized team communication. This means having regular daily stand-ups, discussing future tasks and features, and sharing knowledge. This increases the potential for solutions created by one developer to be reused by other team members.

  • Create a clear and logical code structure based on the business model. This means that if a developer needs to add duplicated logic, they should be able to discover the similar module quickly as it will be in the same place. This encourages the reuse of existing solutions rather than writing new ones.

  • Encourage a culture of looking for already implemented logic. Before writing new code, developers should always try to find existing solutions that can be reused. If a solution doesn't exist, the developer should look for a library before writing their code. 

  • Encourage a culture of creating or finding reusable libraries instead of creating new code from scratch. This ensures that solutions are standardized and can be shared across different tasks and projects.

Code Readability & Simplicity

Code Readability and Simplicity are essential for effective software development and maintenance. Here are some tips that can help in achieving it:

  • Write code for others, not just for yourself. This means considering whether other developers will be able to understand and modify your code in the future.

  • Use clear and meaningful names for variables, functions, modules, classes, and packages. The names should match the purpose and content of the underlying code entities. Use common acronyms rather than inventing your own, which might be unfamiliar to other developers. 

  • Write simple and straightforward code. Avoid complex and obscure solutions.

  • Avoid using exotic or unfamiliar programming language features. Use only the common ones. Design code that meets expectations. Ensure that your code is intuitive and follows common sense conventions.

  • Don't over-engineer solutions or show off advanced knowledge. Choose simple and practical solutions instead.

There is an excellent book called "Clean Code: A Handbook of Agile Software Craftsmanship," which provides principles for writing clean, simple, and reusable code.

Code testability

Starting to write tests from day one is essential for developing a culture of structuring code in a testable manner. However, postponing tests for a supposed "better" time is an anti-pattern that leads to messy and unstructured code, that is then harder to refactor.

The Test-Driven Development approach has many benefits:

  • The chance to deliver a completely not working code to production is approaching zero
  • The probability of encountering a bug in production reduces with a well-tested code.

  • You can avoid time-consuming, repetitive, and routine manual testing with every new version of your application.

  • Developers plan out their solution well in advance to gain a better understanding of the feature complexity and implementation plan.

  • Tests give simple use cases of code usage which increases understanding of some very complex code parts
  • Tests are a helpful means of documenting code since they provide clear examples of how the code can be used. This simplifies complex functionality, and makes it easier for new developers to learn.

  •  Writing code for a "mocked" testing environment provides developers with an idea of different code levels and concern separation. For example, suppose a developer writes a function that not only performs business logic but also sends an email. In that case, they can abstract the code for sending the email and replace it with some simulation since it will not be possible to send real emails in a testing environment.

  • The chances of breaking existing code while implementing changes are significantly lower as failing tests will highlight wrong assumptions and not considered uses cases.

  • Knowing that my code changes would fail the tests if they break something else makes it easier for developers to make changes to critical and complex functionality with more confidence.

  • Tests give an idea of how well my code is designed. If a single test tries to assert too many different things about a class or function, then there is something wrong with the Single Responsibility Principle. If I cannot test a business logic because it requires an external infrastructure communication (such as accessing a file in a cloud storage), then it's probably a good idea to abstract it in an interface.

Summary

Achieving an ideal code design is a difficult task that requires highly-skilled developers, strong organization, and a supportive company culture. However, the most critical component is that all team members are motivated to create a code product that is not only functional but also enjoyable to work on. It's like living in a house where each inhabitant takes responsibility for cleanliness, order, and contributing to the common good.

The advantages of having an ideal code base are substantial. Team members are motivated to stay with the company since no one wants to work in a messy, overcomplicated code base in another organization. Features and code fixes can be implemented rapidly, and new team members have low learning curves and take responsibility of working with complex functionality as it's hard to break production with new code.