Did it ever happen that you developed a new feature, then later new requirements came in, and you needed to change the components a bit, tweak some logic, extend some data structures? It's a widespread situation while working in agile environments, right? That's why it is essential to realize from the beginning that the changes can occur, and while working on an initial feature, you should design your solution so it can be effortlessly extended in the future.
It's necessary to see the difference between designing a solution with openness for extending and actual coding elements that may be helpful in the future but aren’t needed right now. I am encouraging you to don't write unnecessary code - you can’t predict future changes. The point is to plan and implement an architecture that will simplify extending, no matter how the requirements evolve.
That's why you need to focus on a proper design solution before starting to code it. You have to allow yourself to see the bigger picture, don't focus for a while on specific details. Instead, try to see how the components should correlate, what will be the information flow, what classes will be involved in the process, and what their responsibilities will be.
When planning architecture for a new feature, it is beneficial to know the most common cases - called the Design Patterns. They are basically a set of common architecture guides that should apply in most cases. To save your time - you should definitely get to know them. I'm not implying that you should learn them by heart right away - fluency in using them will come naturally while working with them, so do not worry about that. Instead, try to think about your features and tasks, having in mind various patterns that can help you.
Crucial Design Points
I want to highlight a few crucial points from my point of view. When planning architecture for a new feature, I'm always trying to make decisions on those points, preferably before starting to code. These key points are:
- What components, services, and other classes will be used (do I need to create new ones? are there any classes that I can reuse?)
- What are the responsibilities of those classes? Are they limited enough?
- What is the data flow between those classes, especially the components?
- What are the dependencies in the components?
- Is my plan open for extension in the future? (this is very similar to "O" in the SOLID principle)
The pattern that I want to introduce to you is called the Container-Presentation, and it clearly answers all those points above. Why have I chosen that particular pattern? I think this is an instrumental design that can be applied on an almost everyday basis - I believe many of you already intuitively use it in your feature design which is excellent. I want to describe it clearly and show step by step how it can be applied and extended. Although the use case for that pattern is very common, developers tend to omit important parts of that pattern which I will highlight and try to convince you to include in your implementation.
Before digging into the pattern architecture, I would like to highlight the key reasons why this pattern could be useful for you:
- it provides flexible and open for extension split of the components
- separation of the components makes it easy to apply efficient change strategy to each type of the component
- component’s dependencies are limited and well organized, which makes them easy to manage, preferably some of the components don’t have any dependencies at all!
- most of the components are highly generic, and the pattern itself compels you to create them so that they can be easily reused in future work
- logic and UI are separated, which means it’s pretty safe to change the UI without causing any issues with the implemented logic for the feature
So let's get started!
This particular pattern is well suited for the Agile development approach, and I will prove it to you.
The rest of the article will be split into stages, to show you how the pattern evolves while remaining the same structure and how easy it is to extend the approach when the environment changes - new requirements come in.
Example - CRUD view
Abstract solutions are always hard to follow, so that I will use a simple example for the sake of that article. Imagine that a new feature to develop is a basic CRUD view for users. Consequently, the result will be a fully working view that presents a list of the users, with the ability to edit and add new ones.
The requirements for the app are listed below and will be answered by the following sections of the article.
- View for presenting a list of the users
- Ability to edit an existing user by selecting it from the list and edit using a view next to the list
- Ability to create a new user and add it to the list of the users
Foundations of Container-Presentation pattern
What is the first step you need to consider when thinking about the Container-Presentation pattern? First of all, you need to split your UI into components. How to do that? Start from the most obvious one, the main one most likely, and then apply the divide & conquer paradigm to split the task into smaller ones which should go along with creating new components dedicated for more precise jobs.
That’s the theory. However, it’s often the case that it’s relatively hard to plan everything before implementing any of these components. So instead, I would suggest starting from implementing roughly the main component or a few main ones - that depends on your preferences and complexity of the feature and then use the divide & conquer paradigm iteratively, which will ultimately lead to splitting the task into appropriate components.
When you implement your first component or set of components (which probably won’t follow the pattern yet), you can start thinking about how to refactor them to follow the Container-Presentation way.
When you have base components, it's simply a matter of considering which of them will be a "Container" component and a "Presentation" component. How to distinguish that? You should think about these two types of components as the components that do actions and distributes the data across, and the components that simply show some data and optionally handle user input - like mouse click for instance. The common mistake here is believing that if a component receives an event from a user, it needs to react with exact action, call a service, process some data, etc. In our pattern, it is the other way around. In most cases, the component should only inform its Container about the user event, no need for doing much else. The Container is responsible for handling the event appropriately.
To sum up that very shortly:
- Container: takes care of the data, distributes that, handles service calls and most of the logic
- Presentation: present data, sometimes receives events from the users and passes them to Container component
The image below presents the basic flow using the Container-Presentation pattern:
The pattern doesn't require any complex logic, classes, or any other complicated layers of abstraction. In the simplest case, it only uses Input/Output component communication. In the basic flow, the only Service required is the one that populates the data for the Container component.
Stage 1: a bunch of variables
Let's get back to our example: CRUD view for users. Imagine that the first requirement for that feature is to create a view that presents a list of users in the form of a table fetched from the API. Flats below present the idea:
So how to plan the architecture, keeping in mind we want to apply the Container-Presentation pattern?
I will start by implementing the most obvious solution - so a single component will do the task.
The component class could look like this:
The template will simply present the users:
We could simply leave it as a single component that will do everything. What are the cons?
The component class will grow by time rapidly, the business logic will be mixed up with the presentation logic. It would probably be tough to use an efficient Change Detection strategy because of the many trigger points from different places. Moreover, the view will be hard to extend because the logic will be very coupled in this single component - an example of a poorly planned part.
Let's try to break it down. We need a component to present user info - that's for sure. Does this component need to fetch the data? Well, it looks like a different responsibility, right? So we should create another class - a wrapper Component for that purpose. We now have two components - one for presenting user info, second for fetching the data. Clearly, we ended up with one Presentation Component and one Container Component. That's great!
Here is a simple diagram to make sure we are on the same page:
No complex communication between the components is needed for now - it’s just a uni-directional flow. Container Component fetches data via Service, resolves it, and passes to Presentation Component via Input property. Finally, the Presentation Component displays the data, and requirements are fulfilled!
Alright, enough of the theory, now the code:
I will start from the Presentation component because the implementation shouldn't rely on the Container design, so I always start by implementing these components with a fresh head.
We need a component which will display simple data in form of a tile, the data should be delivered via Inputs and that's all that we should care on this stage of implementation.
It looks straightforward, and that's our aim! Why overload components with logic, complex processing, or other stuff - we should keep components as simple as possible, and such Presentation component is very clear to read and maintain. It’s worth to mention this component doesn't rely on any advanced Types. It uses primitives in Inputs, doesn't implement any interface, and finally, it can use an effective Change Detection strategy, OnPush.
Simple, efficient, maintainable component!
There are no surprises in the template - as in design flats, we are displaying the user’s full name, along with the tags assigned and two buttons that will be handy later.
Now we can jump into the Container component. It has to fetch the data using Service, and it has to show the list of the users. Well, we already have a component for presenting a user, so it should be pretty straightforward.
I believe no explanation is needed - very clear and simple component with data stored in users property.
The template is simple as well! It just renders the list of user-tile components and correctly passes the data.
Guess what? That is all the code needed to fulfil our requirements, and this is the only code required for the Container-Presentation pattern.
In the following sections, I will prove that the pattern is useful when requirements change and the whole solution gets more complex, but as long as we follow the pattern, we will be safe.
So, to sum up, that first step, we did:
- We created a simple Presentation component that has as simple Inputs as possible and OnPush Change Detection Strategy - which results in a very efficient component with a clear and straightforward way to use, which in addition is not coupled with any existing dependencies in the application.
- We created a Container component responsible for getting the data and using presentation components to render that, by passing correct properties into it.
Stage 2: Model
The new set of requirements just came!
First, we need to allow users to edit a user, resulting in presenting editable details of that user and the ability to save revised data. Also, the currently selected user should be highlighted on the list.
Let's take a look at the flats:
Based on what we know from the previous step, it should be clear how to apply changes to the architecture design we already have.
- Container (users-component) - has to keep track of user which is currently being edited
- Presentation (user-tile-component) - has to be highlighted when it's the one being edited
What should we do about the editing view on the right? First, there should be another Presentation component. Second, it should receive the data needed to be present, allow data to be in the editable form, and finally inform the Container about possible changes.
Why couldn't it just save the edited user itself? Well, let's see what would happen. The presentation component would need to work closely with the Data Service. It would need to save a user, wait for the response and then inform the parent about that to refresh the table view. It is unnecessarily complicated! We should keep the editing component as the Presentation component. Therefore it will not be coupled with any Service, it will not have to do any complex logic and processing of the data, and finally, it will not have to notify others that they need to refresh themselves. The container is the only class that requires a dependency which is our Service, and it's the best place to identify when and how to refresh the view.
I will once again start with implementing Presentation components, and first of all, I will begin by making changes to the user-tile component. Next, I need to pass additional information about whether the tile is selected and needs to be highlighted.
Then the template could react on that like this:
That would work nicely, but did you realize that I was forced to add additional Input property when requirements changed? That's not the best, because later I can end up with dozens of those, which are not readable and easy to maintain. That is when the Model from the section title comes in handy. It’s often the case we are dealing with properties that belong to a specific context, and such context is usually called a Model. The Model definition usually represents a business model in the application. That approach is commonly used in the Domain Driven Design, where the Domain Entity is a foundation block for that concept, and it’s a base for a Model. In various situations, it comes with different names/approaches, but the idea stays the same.
How can it apply in our case? Well check that class implementation:
Why is the property called vm? VM is the conventional abbreviation for View Model, a convention to use a single Input property, an object that consists of multiple properties. Thus, the name of the property is in some cases called "vm".
What are the benefits? It's a single Input property, so that it will trigger Change Detection only once. It will tell you if you pass input in the wrong type, and you won't forget to pass any needed property. Of course, you can extract that definition into your own Type to make it even more readable. It will be tricky to introduce active property into the vm object later, so I left him outside, but you get the idea.
It looks like a strategy to receive data is in place. We need to add code to inform the Container about licking on the "Edit" button. There is no need for that Presentation component to do anything with the information that the user clicked on that button but sending an event outside. The presentation component could be used in different scenarios in different use cases, so the exact logic of what happens next can differ. That’s why we only inform about such events and leave implementation for the Container.
Presentation components, as I said before, use Outputs properties to inform Containers about the events. Then, of course, the button from the template has to call the select function on click. That’s it.
The second part of that task was to introduce an ability to edit a user. For that I will create a template with simple form:
This component will be a Presentation component as well, so as you may already realize - Inputs and Outputs will be handy. No logic hidden inside, simply receive data and pass the event through.
This is the point when you should spot one issue with this code. Do you see that?
It's about passing the data - we will use an object representing the Model to gather all of the input data together, so we will then change the values implicitly. The vm object is passed by reference. This will result in mutating the original data. So when we edit, for instance, the name, it will change in both input and the table (which has the original data)!
That's obviously wrong. We shouldn't mutate original data. Instead, we should make a copy of it, edit it, save it, and refresh the data in the table.
Who should be responsible for making a copy? As we said in the Container-Presentation pattern, only one component is responsible for distributing correct data, making the Container accountable.
For shallow cloning, I will propose to use a spread operator. For deep cloning, you may find it helpful to use external libraries like lodash or clone or use JSON object built-in web API.
In our example, I will use a simple spread operator to clone the data.
Let's take a look at the Container implementation. The template looks pretty standard:
I added our new Presentation component for editing selected users.
Look closely at how the method selectUser changed. It now does a little more than just assigning selectedUser value. It uses a spread operator to clone the data first, so wherever we use selectedUser, it won't be referenced to the original data.
The newly added method save is pretty straightforward. It gets the edited data, uses the service to edit the user, and updates data at the end.
That's all - we again fulfilled the requirements, using very little code, and what's more important, we again used the Container-Presentation pattern. It didn't require many changes, as you saw. It was simply a matter of adding new elements, not really changing the ones implemented before. That's, in my opinion, the best sign that our architecture is well planned and implemented. We are open for extension and close to modifications!
To sum up, the key things that come from that section:
- Model - a definition representing a domain/business model, commonly used as a single Input in Presentation Components. Making it more readable, type-safe, and ensuring it will have all of the required properties.
- vm - abbreviation for View Model, which can be used to represent a Model
- cloning - the need for cloning the data is a common situation. There are different techniques - for shallow cloning, use spread operator.
Stage 3: Model Class
New requirements came in! The creation of users is required as the next step.
Let's look at what we have on the plate currently. There is a list view and the form view that handles the edit. The creation of users will use pretty much the same component as for edit. The only difference is, we need to be able to distinguish between editing of existing users and the creation of new ones. How to do that? Many options here, but I will use the id approach. So imagine each user has its own unique id, which differentiates it from a user who is just being created - it won't have the id unless it is submitted.
This takes us to three conclusions. One is that the user definition is getting bigger. Secondly, some properties will exist or not depending on the context, and at last, we need to be able to create a blank user and fill it with data during the creation process.
This leaves us with no choice but to learn how to introduce this to our architecture easily. Actually, it's straightforward. First, we have to add a Class. That Class will keep all properties together, provide information about the optional and required properties, and finally, make it very easy to create a blank object.
That's how the Class can be implemented based on our needs and previous assumptions.
That class has all three properties that we've used before, plus an optional id. It can be created via a constructor like that:
Which will result in a blank user, ready for the creation process. That's all we need.
Now we can move on with the components adjustments:
The Container will need a button for starting the creation process, and it will need a method that creates a blank user for the Presentation form component.
The button is in place, now the logic:
As you see, I also added a User type to the component class to make it more safe and bulletproof.
The Container is ready. What needs to be changed inside the Presentation form component?
Well, nothing! It works the same. It receives a User (this time blank), it emits an event when the user wants to submit it, and that's it. That's why I want you to learn this pattern because every time the requirement changes - it needs only a slight extension, and everything else works perfectly fine.
Stage 4: VM class with extras
At this stage, I won't bring any new requirements. Instead, I will focus on few improvements in our code and app. Implementation of all these changes will follow our pattern design.
These will be:
- extracting cloning logic
- improving access for name and last name
- improving how the empty users are being created
- implementing a validation
Cloning - Do you remember we needed to clone the user object for editing purposes? It was nicely done, but I think the responsibility of cloning could live somewhere else. It is currently implemented in the Container, but is it really its responsibility to know how to clone the object? Not really, it must clone the object but not necessarily provide a cloning implementation algorithm.
How it looks now?
I will move the cloning implementation to the class itself and leave the Container free of that problem.
Now the Container class code is more superficial and without unnecessary dependency. It also is a bit more readable.
Now, do you remember how are we displaying name and last name in the list? Let's take a look:
It may often be the case that these two properties will need to be rendered together. To remove possible code duplication, we can create a single getter function that will always resolve these properties in a correct format. How to do it? We could create a Pipe for that, but we already have vm Class we could use! Let's keep in the same place then:
Now the template of the Presentation tile component can be simplified:
Isn't that great? We are only adding tiny functions into the vm Class, and the whole solution works great without the need for much refactor.
I think you now understand how the vm Class should be used, but just to clarify and show its full potential - two more examples.
Remember how we create a blank user object inside Container for creation purposes? It's the same situation as for cloning. There is no reason for Container to take care of creating a blank user - let's extract that logic into the vm class!
The Container will simply call the static function in order to create a blank object - no need to care about how it's done.
The last improvement is about validation - in our Container component, we aren’t checking whether the submitted user is a valid one. Assume the valid user should have a not empty name and last name. Where should such logic be implemented? Of course, in our vm Class!
Then it can be simply used in the Container to check whether the object can be submitted for creation or edition:
Believe me, such techniques will help you very often, and by designing the application properly, you won't counter many issues with new requirements or refactor. It's all about extending the core pattern every time you need it.
Stage n: complex scenario with Service
What if a single Container is not enough? What if the feature grows and we need to introduce more complex logic, split it into multiple smaller sub-features, and as a result, we end up with a bunch of components that are nested within each other for a few levels deep? For sure, It's not a problem with our pattern! It just needs some adjustments, as always. The core - so the relation between the Containers and Presentation components stays identical - that's the key. Don't be scared of creating many Containers - it is the right thing to do in complex scenarios. The main question is whether passing the data through Inputs and Outputs still makes sense.
In some cases, for some component groups, it will but, maybe not for the entire solution. For instance, there could be cases when multiple Models have to be combined to fulfill some requirement. Such processing could happen in Services, and they can be used for Containers to provide specific fragments of the data needed for that part of the feature.
However, if you use the pattern, it will be relatively easy to add that layer. Moreover, there are a few approaches to tackle such cases. For example, in some situations, all you need is a Model class done with the OOP approach or service that manipulates POJOs, which will use a functional approach and make the data containers immutable. Such approaches are an interesting topic, and I will explain them in the following articles.
Possibilities are enormous, it can grow and grow, but the core principles are the same as for the rule of extending implementation when needed.
I hope you enjoyed that little story about the Container-Presentation pattern and learned new stuff. I’m sure you were familiar with most of those techniques, but bringing them together to make a solid architecture is the key.
What were the main ingredients in this guide:
- Container Component- responsible for retrieving data and distributing them to Presentation Components
- Presentation Component - responsible for rendering data and optionally informing Container about events (for instance, user actions)
What should be used for communication between these two types of components?
- Input / Output for most cases
- Service for rather complex scenarios
The other concepts I used:
- vm - View Model convention for wrapping Input properties into a single object. It should be typed to increase safety.
- Change Detection OnPush - the most efficient strategy for most cases, usually can be used without issues in Presentation Components
- vm Class - if the model grows, or it needs to keep some logic to either remove code duplication or extract the logic from other places, it is recommended to create a Class with appropriate properties, getters, functions, etc.
- cloning techniques - it's usually important to clone the objects that we work with, not work on the same data instances. You need to think if your solution requires shallow or deep cloning and use the appropriate algorithm. Spread operator can be useful for shallow cloning.
Lastly, I want to remind you of the key idea that goes through this article and is essential. Introducing such patterns in your architecture will make the solution more solid, concise which will help you with:
- creating a code without duplication
- providing clear responsibility for each class
- being ready for future requirement changes - which means it will be easy to extend your architecture without the need to modify existing code
If you are looking for the final code, here is a stackblitz.
Inspiration for that article came to me during this year's ng-conf. The pattern caught my attention at a workshop run by John Papa and Dan Wahlin - thank you, guys!