🧽 Clean Architecture: Revolutionizing the Front-End
Description: Discover how Clean Architecture can transform and optimize front-end development.
Introduction:
In an increasingly complex computing world, structuring your code effectively is crucial. It relies on the clear separation of an application's components, facilitating testing and reducing dependence on external tools. In this article, we will explore how Clean Architecture can transform your projects by examining its principles, structure, benefits, and its application to a front-end project.
What is Clean Architecture?
Conceived by Robert C. Martin, this software design philosophy places the business concerns of the application at the center of attention. It organizes the code into concentric layers, each with a specific and independent role.
Source : https://miro.medium.com/
Clean Architecture - Divide and Conquer
The primary goal of Clean Architecture is the separation of code into different layers, each with its own responsibility.
Here are the elements that make up this methodical structure:
Entities: Also called the Domain, these are the central elements representing the business logic and rules of the application, independent and can be used by all layers of the application.
Use Cases: They encapsulate all the specific business logic of the application and orchestrate the flow of data between the entities and the outer layers.
Interface Adapters: Also called controllers, presenters, or gateways, they convert data between the formats appropriate for the use cases and entities.
User Interfaces (UIs) and Frameworks & Drivers: They manage interactions with external elements like user interfaces, databases, and external systems.
Adapting to the Front-End
By adapting Clean Architecture to front-end development, we clarify the structure of the code. Entities define data models and business rules, use cases manage user interactions, interface adapters take care of the connection between the UI and the data, and the outer layer interacts with frameworks and external services. This well-defined organization facilitates the maintenance and evolution of front-end applications. Next, let's discover how this approach brings significant benefits to front-end development.
Clean Architecture on the Front-End
The Benefits
The benefits of Clean Architecture for the front-end are similar or identical to those for a back-end application.
- Framework Independence: Frameworks are then used as tools rather than the other way around.
- UI Independence: The architecture does not depend on the user interface.
- Database Independence: It remains unattached to any specific database.
- Granularity of Tests: It is easy to precisely target a layer/component for testing.
Thanks to this flexibility and increased maintainability, Clean Architecture is a very good choice for large-scale front-end applications.
Implementing this type of architecture indeed incurs a higher cost at the beginning of the project, but the advantages of the method allow for quickly saving time in the future.
Preparation for Implementation
On the back-end side, business rules are fairly straightforward and often intuitive, representing the product's functionalities (creating an account, booking a book, making a payment, etc.). On the front-end side, it's not as evident because the domain (Entities) consists of display rules and the rendering behavior by a browser. Moreover, nothing is defined or conventionalized for the application of Clean Architecture on the Frontend: everything is modular and adaptable according to needs. A small application can very well use only two layers, such as the entities and the outermost layer.
Let's take an example with a task management app. A user has expressed a need, which is translated by our Product Owner into a set of rules that define the steps when the user uses the application.
1. Task Selection: When a user clicks on a task to select it:
- If the task is already selected, it should be deselected.
- Otherwise, it is marked as selected.
2. Task Categorization:
- If a task is categorized as "to do immediately," it moves to the top of the list.
- If the task is marked "to do later," it is placed in a separate section.
3. Sub-task Management:
- If a main task is selected, all its associated sub-tasks appear.
- The user can select or deselect individual sub-tasks.
- If a main task is deselected, all its sub-tasks are also deselected.
If you're observant, you can already discern a simple description of an algorithm taking shape. That is the domain.
In Practice
Let's revisit a simple task management application.
Here is the structure of our application:
If we apply the Clean Architecture model, here is what it would look like:
Let's proceed step by step, starting from the center of our architecture by defining our entity Task.ts
interface TaskData {
id: string;
title: string;
description: string;
completed?: boolean;
}
class Task {
id: string;
title: string;
description: string;
completed: boolean;
constructor({ id, title, description, completed = false }: TaskData) {
this.id = id;
this.title = title;
this.description = description;
this.completed = completed;
}
}
Next, let's add an adapter
that allows us to use our entity. This adapter is very similar to services in a more conventional architecture.
import axios from "axios";
import Task from "../entities/Task";
const TaskService = {
async fetchTasks() {
try {
const response = await axios.get("https://your-api-endpoint.com/tasks");
return response.data.map((taskData: Task) => new Task(taskData));
} catch (error) {
console.error("Error fetching tasks:", error);
throw error;
}
},
async addTask(taskData) {
try {
const response = await axios.post('https://your-api-endpoint.com/tasks', taskData);
return new Task(response.data);
} catch (error) {
console.error('Error adding task:', error);
throw error;
}
},
};
export default TaskService;
Between these two elements, we also need a use case that establishes the link between the entity and the adapter.
// ManageTasks.ts
import Task from '../entities/Task';
import TaskService from '../adapters/TaskService';
class ManageTasks {
async addTask(taskData: Task): Promise<Task> {
try {
return await TaskService.addTask(taskData);
} catch (error) {
console.error('Error adding task:', error);
throw error;
}
}
async fetchAllTasks(): Promise<Task[]> {
try {
return await TaskService.fetchAllTasks();
} catch (error) {
console.error('Error fetching tasks:', error);
throw error;
}
}
}
export default new ManageTasks();
This allows us, finally, to add this logic into the Task List component, which acts as an adapter facilitating the connection between our internal layers and the Vue framework.
<template>
<div>
<ul>
<li v-for="task in tasks" :key="task.id">
{{ task.title }}
</li>
</ul>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref } from "vue";
import ManageTasks from "@/use_cases/ManageTasks";
import Task from "@/entities/Task";
onMounted(() => {
loadTasks();
});
const tasks = ref<Task[]>([]);
async function loadTasks() {
try {
tasks.value = await ManageTasks.fetchAllTasks();
} catch (error) {
console.error("Failed to load tasks:", error);
}
}
</script>
Conclusion
The use of Clean Architecture in the front-end, as demonstrated in this example, highlights its numerous advantages.
- Framework-Independent Layers:
Entities: This core layer of the architecture contains business logic and data models. It is completely independent of Vue.js or any other framework, meaning the code here can be reused regardless of the front-end framework.
Use Cases: This layer manages application-specific logic and is also independent of Vue.js. It processes data and interactions without concern for how data is presented or how user inputs are received.
-
Interface Adapters:
This layer acts as a bridge between the framework-independent layers and Vue.js-specific layers. Here, we find components that link the external and internal layers. By isolating Vue.js-specific interactions in this layer, the rest of the application remains independent of the framework.
-
Frameworks & Drivers:
The outermost layer interacts directly with Vue.js. This includes Vue components and views, directives, filters, and other framework specifics. By concentrating all Vue.js-specific interactions in this outer layer, you reduce your business logic and use cases' dependency on the framework. This makes your application more flexible and easier to test, maintain, or even migrate to another framework if necessary.
By following this structure, Clean Architecture enables the construction of front-end applications where the choice of framework (in this case, Vue.js) does not influence the business logic or the application's rules, offering greater flexibility and improved testability.
🚀So, are you convinced, ready to take the step in your next front-end projects?
Top comments (1)
Issues in your implementation
Why I think it's wrong
It kind of starts with your directory structure. If you have different layers, you should have a directory structure that clearly shows that. Currently, everything is just in /src.
Then you have a
Task
class, and there is nothing wrong with its definition. But there is something wrong with how you use it. The Task is the only data model you have; it's used in all layers, whereas it should only be used in the domain layer. Yet it's used in your networking and in your presentation. This means Task has more reasons to change; it's become a more fragile component than it should be. Your entities should have one reason to change: a change in the Enterprise Business Logic.There's nothing much wrong with the TaskService, other than using the domain model.
Then you start at the use case,
ManageTasks
. Which by no definition is a use case, because use cases are only responsible for doing one thing; your "use case" does two things. Again, this means it has more reasons to change. What you essentially did here is combine two use cases into one. But it doesn't end there; the use case has a dependency on yourTaskService
. In other words, your use case is dependent on the data layer. This directly violates the Dependency Rule. Dependencies should point to the business rules. Also, once more, this makes your use case more fragile than it should be, because ifTaskService
changes, your use case needs to change as well.Then you use the "use case" in your vue file, which is seen more often when mixing it with MVVM, which some frameworks, like Vue, require. It's not too bad, but it means you can't test the view without the business logic. But in your case, the problem cascades further, because your use case also depends on your networking. So you can't test with mock data either.
Why it currently isn't a problem, but it certainly will become one
Currently, this example is so small that this incorrect Clean Architecture implementation isn't really a problem. It's also because the entity contains very simple data, just booleans and strings. It would start to fall apart once you add a single property
date
, of typeDate
, toTask
:You could probably work around this issue in various ways, but you should be able to see that the networking and presentation layers have different needs, so they need a different data structure.
This entire codebase is also not testable. Which should be a characteristic of a properly implemented Clean Architecture. Say you wanted to test your "use case"; you couldn't because it depends on a concrete type.
How you could fix it
Task
stays the same, but place it in:src/domain/entities/Task
Write two use cases:
src/domain/AddTaskUseCase
src/domain/FetchAllTasksUseCase
Make those use cases depend on an interface within the domain layer called:
src/domain/interfaces/TaskRepository
Implement the
TaskRepository
in the data layer:src/net/TaskService
This
TaskService
then uses a data structure in its layer:src/net/models/response/TaskResponse
This means you have to map from the
TaskResponse
object to theTask
object.Then you can use the UseCase in the vue file. But here you should also make a separate display object, like
TaskDisplay
, that contains data formatted for displays, like a properly formatted date string using the correct locale.Here is a class diagram of how it would be a lot closer to proper Clean Architecture:
(please note that your
TaskView.vue
also depends onAddTaskUseCase
, but that would make this diagram less readable)And even this implementation has some shortcomings. But it mostly aligns with the most important aspects of Clean Architecture.
Having said all that, I still commend you for making this and bringing attention to this topic that I have not really seen a lot of in web development.