Chào mừng các bạn đã quay trở lại với series “Lộ trình học ASP.NET Core“! Sau khi đã cùng nhau khám phá những nền tảng cốt lõi như ngôn ngữ C#, hệ sinh thái .NET, .NET CLI, quản lý mã nguồn với Git, các giao thức mạng như HTTP/HTTPS, cấu trúc dữ liệu, làm việc với cơ sở dữ liệu quan hệ (bao gồm Stored Procedures, Constraints, Triggers) và NoSQL, hiểu sâu về các ORM như Entity Framework Core (với các khía cạnh như Migrations, Change Tracking, Loading Related Data, Second-Level Caching) cũng như so sánh với Dapper/RepoDB và NHibernate, các kỹ thuật Caching (Redis, In-Memory vs Distributed, Memcached), quản lý phụ thuộc với Dependency Injection (Scrutor, Testability), logging (NLog), xây dựng các loại API (RESTful, GraphQL, OData, gRPC), ánh xạ đối tượng (Mapperly), phát triển ứng dụng thời gian thực (SignalR, WebSockets), các kỹ thuật kiểm thử (Integration Test, BDD với SpecFlow, End-to-End với Playwright, Fake Data với AutoFixture/Bogus, Mocking với Moq/NSubstitute), lập lịch tác vụ (Quartz.NET vs Coravel), API Gateway, Messaging, Docker, Kubernetes, CI/CD với GitHub Actions, đến Cloud-Native với .NET Aspire và Dapr, giờ là lúc chúng ta đi sâu vào một trong những chủ đề quan trọng nhất khi xây dựng các ứng dụng phức tạp, bền vững và dễ bảo trì: Kiến trúc Phần mềm.
Trong thế giới phát triển phần mềm, đặc biệt là với các dự án lớn và có tuổi đời dài, việc code chạy đúng là chưa đủ. Chúng ta cần code dễ hiểu, dễ mở rộng, dễ kiểm thử và quan trọng nhất là dễ thay đổi khi yêu cầu kinh doanh thay đổi. Đây là lúc các mẫu kiến trúc (Architectural Patterns) phát huy tác dụng. Một trong những mẫu kiến trúc phổ biến và được ưa chuộng hiện nay là Clean Architecture.
Mục lục
Kiến Trúc Sạch (Clean Architecture) là gì?
Khái niệm Clean Architecture được Robert C. Martin (hay còn gọi là Uncle Bob) giới thiệu và phổ biến. Mục tiêu chính của nó là tạo ra các hệ thống phần mềm:
- Độc lập với Frameworks (Frameworks Independent)
- Dễ kiểm thử (Testable)
- Độc lập với UI (UI Independent)
- Độc lập với Cơ sở dữ liệu (Database Independent)
- Độc lập với bất kỳ tác nhân bên ngoài nào (Independent of any external agency)
Nghe có vẻ lý tưởng, phải không? Cốt lõi của Clean Architecture xoay quanh một nguyên tắc đơn giản nhưng mạnh mẽ: Quy tắc Phụ thuộc (Dependency Rule).
Nguyên tắc này nói rằng mã nguồn ở vòng trong (các lớp trừu tượng, cốt lõi) không được phép biết về mã nguồn ở vòng ngoài (các chi tiết cài đặt, frameworks). Phụ thuộc luôn luôn hướng vào trong.
Hãy hình dung kiến trúc này như những vòng tròn đồng tâm:
(Hình ảnh minh họa Kiến trúc Sạch, được phổ biến bởi Uncle Bob)
Từ trong ra ngoài, các vòng tròn đại diện cho các lớp (Layers) khác nhau của ứng dụng:
- Entities (Lớp Doanh nghiệp): Chứa các quy tắc nghiệp vụ cốt lõi (Enterprise Business Rules). Chúng là các đối tượng C# thuần (Plain Old CLR Objects – POCOs) không phụ thuộc vào bất kỳ framework, cơ sở dữ liệu hay giao diện người dùng nào. Đây là phần ổn định nhất của ứng dụng.
- Use Cases (Lớp Trường hợp sử dụng): Chứa các quy tắc nghiệp vụ dành riêng cho ứng dụng (Application Business Rules). Lớp này định nghĩa và sắp xếp các luồng dữ liệu vào và ra khỏi Entities. Use Cases điều phối các Entities để thực hiện các tác vụ cụ thể của ứng dụng. Nó định nghĩa các Interface (giao diện) mà các lớp ngoài sẽ cần cài đặt.
- Interface Adapters (Lớp Bộ điều hợp Giao diện): Chứa các adapter chuyển đổi dữ liệu từ định dạng phù hợp với Use Cases và Entities sang định dạng phù hợp với các framework hoặc thiết bị bên ngoài (và ngược lại). Đây là nơi chúng ta tìm thấy các Controller trong ứng dụng web, Gateway để truy cập cơ sở dữ liệu hoặc các hệ thống bên ngoài, Presenter để định dạng dữ liệu cho UI. Các lớp trong này phụ thuộc vào lớp Use Cases và Entities.
- Frameworks & Drivers (Lớp Frameworks và Trình điều khiển): Là vòng ngoài cùng, chứa các chi tiết cài đặt như cơ sở dữ liệu (EF Core, Dapper), framework web (ASP.NET Core), UI framework (Blazor, React), các dịch vụ bên ngoài, v.v. Lớp này phụ thuộc vào các lớp bên trong thông qua việc cài đặt các interface được định nghĩa ở các lớp trong.
Tại sao cần Kiến Trúc Sạch? Vấn đề với Kiến trúc truyền thống
Nhiều ứng dụng web ASP.NET Core, đặc biệt là các dự án nhỏ hoặc khi mới bắt đầu, thường tuân theo một kiến trúc phân lớp đơn giản hơn (ví dụ: 3-Tier hoặc N-Tier).
(Ví dụ về Kiến trúc N-Tier truyền thống)
Trong kiến trúc N-Tier điển hình:
- Presentation Layer (Tầng Trình bày): Chứa UI hoặc API. Phụ thuộc vào Business Logic Layer.
- Business Logic Layer (Tầng Logic nghiệp vụ – BLL): Chứa các quy tắc nghiệp vụ. Phụ thuộc vào Data Access Layer.
- Data Access Layer (Tầng Truy cập dữ liệu – DAL): Chứa logic tương tác với cơ sở dữ liệu.
Vấn đề thường gặp là sự phụ thuộc chảy ngược hoặc xuyên tầng. Ví dụ, trong một ứng dụng web đơn giản sử dụng EF Core:
- Controller (Presentation) gọi một Service (BLL).
- Service (BLL) trực tiếp sử dụng một
DbContext
hoặc Repository được cài đặt bằng EF Core (DAL). Lúc này, BLL phụ thuộc vào DAL và thậm chí là framework DAL (EF Core). - Entities (thường nằm trong BLL hoặc một lớp riêng) có thể bị “nhiễm bẩn” bởi các thuộc tính hoặc annotation dành riêng cho EF Core (như
[Table]
,[Column]
).
Sự phụ thuộc này làm cho BLL khó kiểm thử một cách độc lập (Unit Test) vì nó gắn chặt với cơ sở dữ liệu thật hoặc mock thủ công phức tạp. Thay đổi DAL (ví dụ: chuyển từ SQL Server sang PostgreSQL hoặc NoSQL) sẽ ảnh hưởng lớn đến BLL. Thay đổi framework web (ví dụ: chuyển từ Web API sang gRPC) cũng có thể lan tỏa sâu vào logic nghiệp vụ.
Clean Architecture giải quyết vấn đề này bằng cách đảo ngược hướng phụ thuộc. Lớp Application (tương ứng với BLL) không phụ thuộc vào Infrastructure (tương ứng với DAL). Thay vào đó, lớp Application định nghĩa các Interface (ví dụ: IRepository
, IApplicationDbContext
). Lớp Infrastructure cài đặt các Interface đó. Nhờ Dependency Injection, Presentation layer (Composition Root) có thể kết nối các cài đặt cụ thể từ Infrastructure với các Interface ở Application, cho phép Application layer thực thi logic nghiệp vụ mà không cần biết chi tiết về cách dữ liệu được lưu trữ hay lấy ra.
Áp dụng Clean Architecture trong ASP.NET Core: Cấu trúc dự án
Cách phổ biến nhất để áp dụng Clean Architecture trong ASP.NET Core là sử dụng cấu trúc dự án đa lớp (multi-project solution). Mỗi lớp kiến trúc thường tương ứng với một hoặc nhiều project (assembly) trong Visual Studio hoặc Rider.
Một cấu trúc dự án điển hình có thể trông như sau:
- YourApp.Domain: Chứa các Entities, Value Objects, Domain Events, Exceptions tùy chỉnh. Đây là project core, không phụ thuộc vào bất kỳ project nào khác trong solution.
- YourApp.Application: Chứa logic nghiệp vụ cụ thể của ứng dụng (Use Cases). Bao gồm các Command/Query (thường dùng MediatR), Command/Query Handlers, Interfaces cho các dịch vụ Infrastructure (ví dụ:
IRepository
,IDateTimeService
,IEmailService
). Project này chỉ phụ thuộc vào YourApp.Domain. - YourApp.Infrastructure: Chứa các cài đặt cụ thể của các interfaces được định nghĩa trong YourApp.Application. Ví dụ: EF Core
DbContext
và Repository implementations, các dịch vụ gửi email, các client gọi API bên ngoài. Project này phụ thuộc vào YourApp.Application và các thư viện/framework bên ngoài (EF Core, các client HTTP, thư viện gửi email…). - YourApp.Api (hoặc YourApp.Web): Lớp Presentation. Chứa các Controller (hoặc Minimal APIs, Razor Pages), các DTOs (Data Transfer Objects), các cấu hình liên quan đến framework web (Routing, Authentication, Authorization). Đây là nơi Dependency Injection được cấu hình (Composition Root), kết nối các cài đặt từ Infrastructure với các interfaces ở Application. Project này phụ thuộc vào YourApp.Application và YourApp.Infrastructure (ở tầng cấu hình DI).
Mối quan hệ phụ thuộc (Project References) sẽ là:
YourApp.Api
phụ thuộc vàoYourApp.Application
YourApp.Api
phụ thuộc vàoYourApp.Infrastructure
(chỉ để cấu hình DI tại Composition Root)YourApp.Infrastructure
phụ thuộc vàoYourApp.Application
YourApp.Application
phụ thuộc vàoYourApp.Domain
YourApp.Domain
không phụ thuộc vào bất kỳ project nào khác trong solution.
Sự đảo ngược phụ thuộc xảy ra khi một interface được định nghĩa ở lớp trong (Application) nhưng được cài đặt ở lớp ngoài (Infrastructure). Lớp ngoài phụ thuộc vào lớp trong về mặt định nghĩa interface, nhưng lớp trong không biết về lớp ngoài.
Cài đặt các thành phần chính trong .NET
Hãy xem xét cách các khái niệm của Clean Architecture được hiện thực hóa trong môi trường .NET và ASP.NET Core.
YourApp.Domain
Đây là nơi chứa các quy tắc nghiệp vụ quan trọng nhất. Các lớp ở đây nên là POCOs. Ví dụ về một Entity Product
:
namespace YourApp.Domain.Entities
{
public class Product
{
public Guid Id { get; private set; }
public string Name { get; private set; }
public decimal Price { get; private set; }
public int Stock { get; private set; }
// Private constructor for EF Core or mapping
private Product() { }
public Product(string name, decimal price, int stock)
{
Id = Guid.NewGuid();
Name = name;
Price = price;
Stock = stock;
}
// Domain logic methods
public void UpdatePrice(decimal newPrice)
{
if (newPrice <= 0)
throw new DomainException("Price must be positive."); // Custom exception
Price = newPrice;
}
public void IncreaseStock(int quantity)
{
if (quantity <= 0)
throw new DomainException("Quantity must be positive.");
Stock += quantity;
}
}
}
Lưu ý: Entity này không có bất kỳ thuộc tính hay annotation nào liên quan đến EF Core hoặc bất kỳ framework nào khác. Logic nghiệp vụ (như kiểm tra giá > 0) được đặt ngay trong Entity.
YourApp.Application
Lớp này chứa logic ứng dụng. Đây là nơi các “Use Cases” được định nghĩa. Pattern CQRS (Command Query Responsibility Segregation) thường được áp dụng ở đây, sử dụng MediatR để xử lý các Command (ghi dữ liệu, thay đổi trạng thái) và Query (đọc dữ liệu).
Ví dụ về một Command để tạo sản phẩm và Handler của nó:
Đầu tiên, interface của DbContext (định nghĩa ở Application):
namespace YourApp.Application.Interfaces
{
// Interface chung cho DbContext hoặc Unit of Work
public interface IApplicationDbContext
{
// DbSet<T> hoặc phương thức Get<T> / Set<T>
// Ở đây ta có thể expose DbSet<T> hoặc dùng Generic Repository
// Để đơn giản, giả sử expose DbSet<Product>
Microsoft.EntityFrameworkCore.DbSet<YourApp.Domain.Entities.Product> Products { get; set; }
Task<int> SaveChangesAsync(CancellationToken cancellationToken);
}
}
Command (định nghĩa ở Application):
namespace YourApp.Application.Features.Products.Commands.CreateProduct
{
// MediatR IRequest<T>
public class CreateProductCommand : MediatR.IRequest<Guid>
{
public string Name { get; set; }
public decimal Price { get; set; }
public int Stock { get; set; }
}
}
Handler (định nghĩa ở Application), phụ thuộc vào Interface IApplicationDbContext
:
namespace YourApp.Application.Features.Products.Commands.CreateProduct
{
public class CreateProductCommandHandler : MediatR.IRequestHandler<CreateProductCommand, Guid>
{
private readonly IApplicationDbContext _context;
public CreateProductCommandHandler(IApplicationDbContext context)
{
_context = context;
}
public async Task<Guid> Handle(CreateProductCommand request, CancellationToken cancellationToken)
{
var product = new Domain.Entities.Product(request.Name, request.Price, request.Stock);
_context.Products.Add(product);
await _context.SaveChangesAsync(cancellationToken);
return product.Id;
}
}
}
Handler này chỉ biết về IApplicationDbContext
, không biết nó được cài đặt bằng EF Core hay Dapper. Logic nghiệp vụ tạo sản phẩm và lưu vào “database” được thực hiện ở đây.
YourApp.Infrastructure
Lớp này chứa cài đặt cụ thể của các interfaces từ Application. Đây là nơi Entity Framework Core DbContext, các Repository cài đặt bằng EF Core, các dịch vụ gọi API ngoài, gửi email, v.v., sẽ sống.
Cài đặt của IApplicationDbContext
(định nghĩa ở Infrastructure):
using YourApp.Application.Interfaces;
using YourApp.Domain.Entities;
using Microsoft.EntityFrameworkCore;
namespace YourApp.Infrastructure.Persistence
{
public class ApplicationDbContext : DbContext, IApplicationDbContext
{
// Constructor cho EF Core
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) { }
// Cài đặt DbSet<Product> từ interface
public DbSet<Product> Products { get; set; }
// Có thể override SaveChangesAsync nếu cần logic phức tạp hơn
// public override Task<int> SaveChangesAsync(CancellationToken cancellationToken = new CancellationToken())
// {
// // Add custom logic here if needed
// return base.SaveChangesAsync(cancellationToken);
// }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Cấu hình EF Core cho các entities
modelBuilder.ApplyConfigurationsFromAssembly(System.Reflection.Assembly.GetExecutingAssembly());
base.OnModelCreating(modelBuilder);
}
}
}
```
Project Infrastructure sẽ phụ thuộc vào EF Core NuGet package và YourApp.Application.
YourApp.Api
Lớp Presentation. Chứa ASP.NET Core Controllers hoặc Minimal APIs. Lớp này phụ thuộc vào YourApp.Application để gọi các Use Cases (thường thông qua MediatR hoặc các Service Interface cụ thể) và phụ thuộc vào YourApp.Infrastructure tại Composition Root để cấu hình Dependency Injection.
Ví dụ về Controller (phụ thuộc vào MediatR):
using YourApp.Application.Features.Products.Commands.CreateProduct;
using MediatR;
using Microsoft.AspNetCore.Mvc;
namespace YourApp.Api.Controllers
{
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
private readonly IMediator _mediator;
public ProductsController(IMediator mediator)
{
_mediator = mediator;
}
[HttpPost]
public async Task<IActionResult> Create([FromBody] CreateProductCommand command)
{
var productId = await _mediator.Send(command);
return CreatedAtAction(nameof(GetById), new { id = productId }, productId);
}
// ... các endpoints khác ...
}
}
```
Controller này chỉ biết về Command (một class ở Application) và IMediator (ở Application hoặc một thư viện trung gian được sử dụng ở Application). Nó không biết gì về Entity Framework Core hay cách sản phẩm được lưu.
Composition Root (Cấu hình DI)
Đây là nơi “ma thuật” xảy ra. Trong ASP.NET Core, Composition Root thường nằm trong Program.cs
(hoặc Startup.cs
ở các phiên bản cũ hơn) của project YourApp.Api. Tại đây, chúng ta đăng ký các services vào container DI.
// Trong Program.cs của YourApp.Api
using YourApp.Application; // Chứa các extension method cấu hình Application layer
using YourApp.Infrastructure; // Chứa các extension method cấu hình Infrastructure layer
using YourApp.Api; // Chứa các cấu hình riêng của API
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddApplicationServices(); // Cấu hình MediatR, các handler, v.v.
builder.Services.AddInfrastructureServices(builder.Configuration); // Cấu hình DbContext, Identity, External services
builder.Services.AddApiServices(); // Cấu hình Controllers, Swagger, CORS, v.v.
var app = builder.Build();
// Configure the HTTP request pipeline.
// ... (middlewares như Routing, Authentication, Authorization, etc.)
app.Run();
```
Trong các extension methods như AddApplicationServices
và AddInfrastructureServices
(thường nằm trong các project tương ứng), chúng ta sẽ đăng ký các dependency cụ thể. Ví dụ, trong AddInfrastructureServices
của YourApp.Infrastructure, chúng ta đăng ký ApplicationDbContext
và map nó tới IApplicationDbContext
:
// Trong YourApp.Infrastructure/DependencyInjection.cs
public static class DependencyInjection
{
public static IServiceCollection AddInfrastructureServices(this IServiceCollection services, IConfiguration configuration)
{
// Cấu hình DbContext và đăng ký IApplicationDbContext
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(configuration.GetConnectionString("DefaultConnection")));
services.AddScoped<IApplicationDbContext>(provider => provider.GetRequiredService<ApplicationDbContext>());
// Đăng ký các repository hoặc services khác
// services.AddScoped<IProductRepository, ProductRepository>();
// services.AddTransient<IEmailService, SmtpEmailService>();
return services;
}
}
```
Đây chính là sự “đảo ngược phụ thuộc” được hiện thực hóa nhờ DI. Application layer cần một IApplicationDbContext
, và tại Composition Root ở lớp ngoài cùng (Api), chúng ta “tiêm” vào đó cài đặt cụ thể là ApplicationDbContext
từ lớp Infrastructure.
Ưu điểm của Kiến Trúc Sạch
Áp dụng Clean Architecture mang lại nhiều lợi ích đáng kể:
- Độc lập: Hệ thống cốt lõi (Domain, Application) không phụ thuộc vào các chi tiết bên ngoài như cơ sở dữ liệu (SQL Server, MongoDB), framework web (ASP.NET Core MVC/API), UI (Angular, React). Điều này giúp dễ dàng thay đổi các thành phần này trong tương lai mà ít ảnh hưởng đến logic nghiệp vụ cốt lõi.
- Dễ kiểm thử: Lớp Domain và Application có thể được kiểm thử một cách dễ dàng bằng Unit Test mà không cần khởi tạo database, web server hay các dịch vụ bên ngoài. Chúng ta có thể sử dụng các thư viện Mocking để thay thế các phụ thuộc Infrastructure bằng các đối tượng giả. Điều này giúp các bài kiểm thử chạy nhanh và đáng tin cậy hơn. Integration Test vẫn cần thiết để kiểm tra sự kết hợp giữa các lớp, nhưng số lượng unit test sẽ chiếm phần lớn.
- Dễ bảo trì: Mã nguồn được phân tách rõ ràng theo trách nhiệm, giúp developers dễ tìm thấy logic cần thay đổi và giảm thiểu rủi ro gây ảnh hưởng lan man sang các phần khác của hệ thống.
- Linh hoạt: Khi yêu cầu kinh doanh thay đổi, các thay đổi này thường tác động vào lớp Use Cases. Lớp Domain (quy tắc nghiệp vụ cốt lõi) ít khi thay đổi. Các lớp ngoài cùng (Frameworks & Drivers) cũng ít thay đổi liên quan đến nghiệp vụ. Sự phân tách giúp khoanh vùng ảnh hưởng của thay đổi.
Tổng kết các Lớp
Bảng dưới đây tóm tắt các lớp trong Clean Architecture và ánh xạ chúng vào cấu trúc dự án ASP.NET Core phổ biến:
Lớp Kiến trúc (Layer) | Trách nhiệm chính (Responsibility) | Phụ thuộc vào (Depends On) | Project .NET điển hình |
---|---|---|---|
Entities (Vòng trong cùng) |
Quy tắc nghiệp vụ cốt lõi (Enterprise Business Rules). Các đối tượng POCO. | Không phụ thuộc gì bên ngoài. | YourApp.Domain |
Use Cases (Vòng thứ 2) |
Logic nghiệp vụ ứng dụng (Application Business Rules). Định nghĩa Interfaces cho các dịch vụ bên ngoài. Điều phối Entities. | Entities. | YourApp.Application |
Interface Adapters (Vòng thứ 3) |
Bộ chuyển đổi dữ liệu. Controllers, Presenters, Gateways, Implementations của Interfaces từ Use Cases. | Use Cases, Entities. | YourApp.Infrastructure ,YourApp.Api (Controllers, DTOs) |
Frameworks & Drivers (Vòng ngoài cùng) |
Chi tiết cài đặt. Cơ sở dữ liệu (EF Core), Framework Web (ASP.NET Core), UI, External Services. | Interface Adapters (thông qua cài đặt Interfaces). | YourApp.Infrastructure ,YourApp.Api (cấu hình framework) |
Khi nào sử dụng (và khi nào không)?
Clean Architecture không phải là thuốc tiên cho mọi vấn đề và mọi dự án. Nó mang lại nhiều lợi ích nhưng cũng có chi phí ban đầu.
Nên sử dụng khi:
- Dự án có quy mô lớn hoặc dự kiến sẽ phát triển lớn trong tương lai.
- Logic nghiệp vụ phức tạp, yêu cầu thay đổi thường xuyên.
- Cần một hệ thống dễ kiểm thử tự động (automated testing) ở mức độ cao.
- Dự kiến sẽ có nhiều người tham gia phát triển.
- Cần sự linh hoạt để dễ dàng thay đổi các thành phần kỹ thuật (database, framework, external services).
Có thể là “quá tải” khi:
- Dự án rất nhỏ, đơn giản, chỉ là CRUD cơ bản và không có nhiều logic nghiệp vụ phức tạp.
- Ứng dụng chỉ tồn tại trong thời gian ngắn và không có kế hoạch phát triển lâu dài.
- Đội ngũ phát triển còn thiếu kinh nghiệm với các mẫu kiến trúc nâng cao.
Tuy nhiên, ngay cả với các dự án nhỏ, việc làm quen với các nguyên tắc của Clean Architecture (như Separation of Concerns, Dependency Inversion) vẫn rất có lợi cho sự phát triển kỹ năng của bạn.
Kết luận
Clean Architecture cung cấp một bộ nguyên tắc mạnh mẽ để xây dựng các ứng dụng ASP.NET Core bền vững, dễ kiểm thử và bảo trì. Bằng cách tập trung vào việc tách biệt logic nghiệp vụ cốt lõi khỏi các chi tiết cài đặt bên ngoài và tuân thủ Quy tắc Phụ thuộc, chúng ta có thể tạo ra các hệ thống linh hoạt, dễ thích ứng với sự thay đổi.
Việc áp dụng Clean Architecture đòi hỏi sự hiểu biết và thực hành. Nó có thể phức tạp hơn kiến trúc phân lớp truyền thống ban đầu, nhưng lợi ích lâu dài về khả năng bảo trì và mở rộng là rất lớn.
Trong Lộ trình học ASP.NET Core của chúng ta, hiểu và áp dụng các mẫu kiến trúc như Clean Architecture là một bước tiến quan trọng từ việc code các tính năng đơn lẻ sang việc xây dựng toàn bộ hệ thống một cách có chiến lược. Hãy dành thời gian tìm hiểu sâu hơn, thực hành xây dựng một dự án nhỏ với cấu trúc này. Đừng ngại thử nghiệm và mắc lỗi, đó là cách tốt nhất để học!
Hẹn gặp lại các bạn trong các bài viết tiếp theo của series!
“`