Writing an anti-legacy application

In previous articles, I shared my thoughts on why UI projects turn into legacy overnight.

It all came down to two key unmet needs: instant feedback and proper design patterns. Regarding design patterns, special attention was paid to the strict separation of presentation and logic.

I even suggested that Elm MVU is the way to cover these needs.

However, despite the fact that MVU is an architecture that allows for a rigid separation of presentation and logic, I have come to the conclusion that MVU (and functional programming in general) suffers from a certain alienness to the natural process of thinking and programming.

By natural I mean something that correlates with the language we use in everyday life. Functional programming cannot always be described in such a language (for example, although monads, including Observable streams, are a relatively simple concept, we are unlikely to be able to express this concept in such a language). I've become convinced that the programming that correlates best with natural language is multi-paradigm programming, where things are neither strictly OOP nor strictly functional, but rather one or the other depending on clarity and ease of operation.

Therefore, programming the application core (model/domain layer) is not a matter of right or wrong. The model behind the application is a description of how a person understands the concept of the program. And it’s best when it’s one person, or if it’s a group, then they’re on the same conceptual page.

Of course, the conceptual understanding of different people can be very different, but this does not mean that it is necessary to do it in such a way that it is clear to absolutely everyone. If there are full-fledged blackbox tests, then a person who has adopted the “legacy” program will have the opportunity to remake it to his own understanding.

In this article, I will demonstrate the process of creating an application that will contain the necessary components of a clean architecture (Uncle Bob Martin) with a few additional ones that I personally think are important:

But enough philosophy, let's get down to business.

Development

In this article, I will demonstrate the process of creating an application in an outside-in style, where the main source of truth is the design in Figma.

We will then create a pure representation as a function of state. It will not contain any logic or state (with rare exceptions).

The logic will be created in an object-oriented style as a composition of classes through dependency injection. This will allow us to defer details such as the choice of storage or how communication with services will be carried out, etc.

The Model will be associated with the View using a display function similar to ViewModel. It will have the necessary functionality to have the necessary correspondence with the view, but will not have knowledge about the details of this view (for example, about React or the DOM, etc.)

Having a ViewModel will allow us to write a TDD style application without having to worry about complex view libraries/frameworks and even allowing us to replace them.

Since both the Model and ViewModel will be pure JS objects (like POJOs), they should also be easily convertible to other languages.

It's important to remember that this approach aims to build applications that are anti-aging (deprecation-proof = “adaptable to change” = scalable), which, as we've already discussed, requires instant feedback (for example, through Storybook or blackbox tests in Jest ) and good design patterns, which in our case are MVVM and DI.

Step 1: Design

Since the tools for converting from designs to code are still far from ideal, we should rely more on ourselves when converting components at the very beginning. However, as the design changes, we can ask LLMs (large language models like ChatGPT) to adapt the changes to the component code that already exists. This approach is much simpler, since if the components are implemented “correctly”, they are usually quite small and easily understood by the LLM.

Here link on a figma with the application design.

Step 2: Storybook

Once we convert the design into a Storybook, we can use components to represent scenarios by assembling a sequence of pre-configured pages (with specific props). And since we know which props should change for certain user interactions, we get ready to write blackbox tests.

The stories structure will look like this:

  • Components

  • Pages

  • Scenarios

    • A sequence of pages with different props so we can understand how the props should change upon interaction, allowing us to write tests

  • Application

Step 3: MVVM & TDD

As tests are written, domain logic is implemented to pass those tests.

I'll admit, I developed a sample application with very few tests, of which I only kept one as an example in the final version, and relied more on the TypeScript type system for instant feedback, so as a personal TODO I will need to master this practice myself, so as I believe this ultimately Test-driven development saves a lot of time for large projects such as this.

Although our tests should show whether the functionality works correctly, the structure of the domain logic itself is not a matter of right or wrong. An application's conceptual model is a description of how the person who wrote it understands the program conceptually, and it's best if it's one person or a group on the same conceptual page.

As a small philosophical aside, I would like to point out that Immanuel Kant revolutionized philosophy by shifting the focus from the idea that we directly perceive the world as it really is, to the idea that we comprehend the world as it appears to us . This means that when we study the world, we study how it is reflected in us rather than the world itself.

Likewise, when developing a program, we should not strive for a single correct solution. Instead, we should strive to create a program that effectively represents our understanding and concepts. The quality of this understanding may vary, but if the program follows the SOLID principles, is testable, and works correctly, then we have achieved our goal.

Of course, different people’s conceptual understanding may be very different, but this does not mean that it must be done in such a way that it is clear to absolutely everyone. If there are full-fledged blackbox tests, then a person who has adopted the “legacy” program will have the opportunity to remake it to his own understanding.

To illustrate, a program does not have to be object-oriented or functional, because in fact, if we could think like computers, we would write optimized machine code directly, without using programming languages.

However, I believe that every UI developer has dreamed of presenting his application in the form of simple classes that read like normal speech.

Technically, MobX allows you to do just that—represent a model as simple classes. However, this comes at a cost: classes must be specially wrapped to provide automatic reactivity, which will lock the domain into the framework. However, representing an application as simple classes does not mean that we have to rely on yet another framework.

In contrast, what MobX does can still be implemented using simple classes.

In our case, the ViewModel is a step between the conceptual view and the view, which is always associated with some kind of framework (React, Angular, Vue, Flutter, etc.). But since the ViewModel itself is not associated with the framework, we can use it as a simplified representation of this very view, which we can (and should) actually test. Because the ViewModel in our case is a boundary that will allow us to write tests in terms of user intent, where for example the user clicks or interacts with something. This will allow us to refactor and revise our conceptual understanding as often as we need.

This way, as long as there are tests, we will always have the opportunity to refactor.

Root of composition

We must remember that the final part of the application that will change the most is the composition root, where all the dependencies will be combined.

It is important to demonstrate in the code repository how our application is assembled as transparently as possible. This means that whenever someone looks at the repository and then looks at the index file, they should understand how the application is structured and what its purpose is.

Link to the root composition of the demo application

Step 4: Connect to IO

The last and coolest part is the ability to push very complex decisions about technologies for storage and other IO as far into the future as possible, allowing us to maintain momentum and implement features knowing that we still have time to make an informed decision based on the needs of our application and stakeholders.

This step will require a separate article and consideration.

Conclusion

Advantages and disadvantages

  • pros

    • Security against obsolescence

    • Abundance of information

      • Based on established practices such as OOP, MVVM and component composition

    • “Natural” programming style

  • Minuses

    • Requires developing a holistic conceptual understanding of the application

      • There is no clear algorithm for creating a model; we will have to experiment, doubt, rethink and refactor until the model fits our needs

    • Model simplification and optimization

      • While presentation and IO can be optimized separately, the main optimization to watch out for is to keep the model as conceptually simple as possible

The purpose of this article is to demonstrate how applications can be written so that they do not become legacy and adapt to changes, as real software should be (software means something soft and flexible).

The article examines UI development from the point of view of maximum decoupling from the view and through the creation of a testable view model, which allows you to write blackbox tests that will give the very feedback that was discussed.

While writing this article, I had to revise and refactor my “conceptual model” of the application many times. I realize that it is far from ideal, but I am aware that if the goal was ideal, I would never finish the article.

Therefore, first of all, I want to offer this material not as a silver bullet, but as a snapshot of the path traveled, which will provide food for thought and action.

I thank you for your attention and wish our applications a long life.

useful links

  1. Sample Application: Application created to illustrate the concepts in this article

  2. Clean Code, Robert Martin: Famous book explaining the basic principles of scalable software

  3. Dependency Injection Principles, Practices, and Patterns: A book that I consider to be a practical implementation of the concepts outlined in Clean Code

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *