Làm thế nào để sắp xếp Clean Architecture theo Modular Patterns trong 10 phút?

Introduction

Trong định nghĩa về software development thì architecture của dự án đóng vai trò rất quan trọng nhằm đảm bảo quá trình maintenance & reusability. Software architecture đảm bảo rằng phần mềm mà bạn xây dựng có bộ khung cơ bản. Từ đây, chúng ta có thể build bất cứ thứ gì chúng ta muốn.

Câu hỏi luôn đặt ra trong đầu tôi những ngày này làm cách nào để chúng ta kết hợp Clean Architecture & Modular pattern? Tôi đã code vài thử nghiệm và cuối cùng quyết định tổng hợp lại trong bài viết này. Bài viết sẽ dựa trên kinh nghiệm có được trong quá trình phát triển phần mềm và cách để phương pháp modular có thể áp dụng concept Clean Architecture vào quá trình phát triển phần mềm này.

Tìm việc làm Software Developer các công ty

Backgrounds

Modular patterns

Cách đây 5 năm, tôi đã từng làm trong 1 dự án lớn với nhiều thành viên tham gia và lúc đó, tôi đã sắp xếp architecture theo phương pháp modular. Chúng tôi nhận ra với modular chúng tôi có thể cắt software kiểu monolith lớn thành nhiều monoliths dọc nhỏ hơn & hỗ trợ team làm việc dễ dàng hơn vì mỗi team chỉ cần tập trung vào module mà họ đang làm. Liệu có ai còn nhớ được các đoạn code xung đột trong dự án lớn không? Liệu bạn có thể dành nửa ngày (hoặc nhiều) chỉ để merge code? Chẳng khác nào là ác mộng, phải không?

Vì vậy mà trong phương pháp modular, chúng ta cần phải đảm bảo rất các modules đủ độc lập để vẫn hoạt động được dù được viết bởi các developer đơn lẻ ở mỗi team khác nhau. Phương pháp này sẽ theo style design logic với những ưu điểm như:

  • Giúp hệ thống software có thể mở rộng được, reuse được, maintain được & tùy biến được
  • Phá stack dạng nguyên khối lớn thành hỗn hợp linh hoạt các modules cộng tác với nhau (theo style nguyên khối)
  • Giúp những người mới dễ dàng hiểu được các tính năng business & các chức năng của hệ thống (vì nó đủ nhỏ).
  • Mở ra cánh cửa để tích hợp vào kiến trúc Microservices

Clean Architecture

Clean Architecture ra đời từ năm 2012 bởi Uncle Bob và theo thời gian, Clean Architecture ngày càng đóng vai trò quan trọng trong thế giới kiến trúc phần mềm. Chúng ta có thể thấy được Android architecture đã sử dụng Clean Architecture bằng cách kết hợp với MVP pattern để xây dựng kiến trúc phần mềm cho ứng dụng Mobile. Một vài bài nghiên cứu còn đề nghị sử dụng Clean Architecture cho ứng dụng web. Đầu năm năm, Uncle Bob đã cho ra mắt quyển sách Clean Architecture: A Craftsman’s Guide to Software Structure and Design, đề cập đến các best practices khi sử dụng các nguyên lý SOLID, các pattern design & 1 vài mẹo khi deploy.

Giới thiệu sơ về clean architecture, nếu bạn đã biết thì có thể bỏ qua. Theo Clean Architecture, chúng ta cần phải đảm bảo 1 vài điểm quan trọng sau:

  • Có thể test được. Nguyên tắc business là có thể test được dù không có UI, Database, Web Server hay bất kì element bên ngoài nào khác
  • Tính độc lập của UI. UI có thể thay đổi dễ dàng mà không thay đổi phần còn lại của hệ thống. Ví dụ, 1 Web UI có thể được thay thế bởi 1 console UI mà không thay đổi các nguyên tắc business.
  • Tính độc lập của Database. Bạn có thể hoán đổi Oracle hoặc SQL Server với Mongo, BigTable, CouchDB… Các nguyên tắc business của bạn sẽ không gắn liền với database.

Vậy làm thế nào để Clean Architecture hoạt động hiệu quả?

Câu hỏi đặt ra là làm sao để tạo architecture theo phương pháp modular? Thực sự thì nhân tố cốt lõi ở đây xuất phát từ Domain-Driven Design (theo quan điểm của tôi, Tackling Complexity in the Heart of Software & Implementing Domain-Driven Design là những quyển sách mà bạn nên đọc) và trong trường hợp này, chúng tôi sử dụng pattern Bounded Context design pattern để phân tích & thiết kế business domain hiện tại. Nếu nhìn vào biểu đồ ở trên, bạn sẽ nhận ra chúng ta chỉ có 3 nhân tố chính cần quản lý là xác thực, blog & post. Tôi đã tách domain ứng dụng ra khỏi Access Control Context, Blog Context & Post Context (tôi chia nó thành 3 Bounded Contexts vì nó sẽ khác biệt so với các context khác do khác về kinh nghiệm domain, nhưng ít nhất cần phải phân loại yêu cầu business). Biểu đồ sẽ như thế này:

Thứ 2, folder Hosts ở giữa biểu đồ, chúng ta sử dụng để đặt các host projects ở đó. Bạn sẽ thấy chúng ta có 2 hosts: 1 host cho API (BlogCore.API) & host khác Single Page Application (BlogCore.App).

Thứ 3, folder Migrations được sử dụng để thực hiện các công việc migrating, trong trường hợp này, chúng ta migrate data cho Access Control Context, Blog Context & Post Context. Chúng ta có thể chọn cách migrate bằng cách sử dụng Entity Framework migration & đưa data cho chúng. Trái lại, bạn có thể sử dụng T-SQL scripts để thực hiện migration.

Theo tôi, chúng ta nên phân tích sau 1 vài đoạn code để hiểu hơn cách implement Clean Architecture với pattern Modular cho dự án này.

Chúng ta có Post.cs entity hoạt động như Root Aggregate trong Post Context như

namespace BlogCore.PostContext.Core.Domain
{
    public class Post : EntityBase
    {
        internal Post()
        {
        }

        internal Post(BlogId blogId, string title, string excerpt, string body, AuthorId authorId)
            : this(blogId, IdHelper.GenerateId(), title, excerpt, body, authorId)
        {
        }

        internal Post(BlogId blogId, Guid postId, string title, string excerpt, string body, AuthorId authorId) 
            : base(postId)
        {
            Blog = blogId;
            Title = title;
            Excerpt = excerpt;
            Slug = title.GenerateSlug();
            Body = body;
            Author = authorId;
            CreatedAt = DateTimeHelper.GenerateDateTime();
            Events.Add(new PostedCreated(postId));
        }

        public static Post CreateInstance(BlogId blogId, Guid postId, string title, string excerpt, string body, AuthorId authorId)
        {
            return new Post(blogId, postId, title, excerpt, body, authorId);
        }

        public static Post CreateInstance(BlogId blogId, string title, string excerpt, string body, AuthorId authorId)
        {
            return new Post(blogId, title, excerpt, body, authorId);
        }

        [Required]
        public string Title { get; private set; }

        [Required]
        public string Excerpt { get; private set; }

        [Required]
        public string Slug { get; private set; }

        [Required]
        public string Body { get; private set; }

        [Required]
        public BlogId Blog { get; private set; }

        public ICollection Comments { get; private set; } = new HashSet();

        public ICollection Tags { get; private set; } = new HashSet();

        [Required]
        public AuthorId Author { get; private set; }

        [Required]
        public DateTime CreatedAt { get; private set; }

        public DateTime UpdatedAt { get; private set; }

        public Post ChangeTitle(string title)
        {
            if (string.IsNullOrEmpty(title))
            {
                throw new BlogCore.Core.ValidationException("Title could not be null or empty.");
            }

            Title = title;
            Slug = title.GenerateSlug();
            return this;
        }

        public Post ChangeExcerpt(string excerpt)
        {
            if (string.IsNullOrEmpty(excerpt))
            {
                throw new BlogCore.Core.ValidationException("Excerpt could not be null or empty.");
            }

            Excerpt = excerpt;
            return this;
        }

        public Post ChangeBody(string body)
        {
            if (string.IsNullOrEmpty(body))
            {
                throw new BlogCore.Core.ValidationException("Body could not be null or empty.");
            }

            Excerpt = body;
            return this;
        }

        public bool HasComments()
        {
            return Comments?.Any() ?? false;
        }

        public Post AddComment(string body, AuthorId authorId)
        {
            Comments.Add(new Comment(body, authorId));
            return this;
        }

        {
            {

            }
            return this;
        }

        {
            {
                
            }
            return this;
        }

        public bool HasTags()
        {
            return Tags?.Any() ?? false;
        }

        public Post AssignTag(string name)
        {
            if (tag == null)
            {
                Tags.Add(new Tag(IdHelper.GenerateId(), name, 1));
            }
            else
            {
                tag.IncreaseFrequency();
            }
            return this;       
        }

        public Post RemoveTag(string name)
        {
            if (tag != null)
            {
                tag.DecreaseFrequency();
                Tags.Remove(tag);
            }
            return this;    
        }
    }
}

Sau đó, chúng ta có PostGenericRepository.cs

public class BlogEfRepository : EfRepository
        where TEntity : EntityBase
{
    public BlogEfRepository(PostDbContext dbContext)
        : base(dbContext)
    {
    }
}

Chúng ta cần tạo DbContext cho PostContext.cs như

namespace BlogCore.PostContext.Infrastructure
{
    public class PostDbContext : DbContext
    {
        public PostDbContext(DbContextOptions options)
            : base(options)
        {
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            var entityTypes = new List
            {
                typeof(Post),
                typeof(Comment),
                typeof(Tag)
            };

            var valueTypes = new List
            {
                typeof(BlogId),
                typeof(AuthorId)
            };

            base.OnModelCreating(modelBuilder.RegisterTypes(entityTypes, valueTypes, "post", "post"));
        }
    }
}

Trong Clean Architecture, use case rất quan trọng và nên được design thật cẩn thận. Trong dự án của mình, tôi đã đặt tên nó là ListOutPostByBlogInteractor.cs

namespace BlogCore.PostContext.UseCases.ListOutPostByBlog
{
    public class ListOutPostByBlogInteractor 
    {

        public ListOutPostByBlogInteractor(
        {
            _postRepository = postRepository;
            _pagingOption = pagingOption;
        }

        {
            var criterion = new Criterion(request.Page, _pagingOption.Value.PageSize, _pagingOption.Value);

            return _postRepository.ListStream(filterFunc, criterion, includes)
                {
                            y.TotalItems,
                            (int)y.TotalPages,
                            {
                                return new ListOutPostByBlogResponse(
                                    x.Id,
                                    x.Title,
                                    x.Excerpt,
                                    x.Slug,
                                    x.CreatedAt,
                                    new ListOutPostByBlogUserResponse(
                                            x.Author.Id.ToString(),
                                            string.Empty,
                                            string.Empty
                                        ),
                                    x.Tags.Select(
                                            tag.Id,
                                            tag.Name))
                                        .ToList()
                                );
                            }).ToList()
                        );
                });
        }
    }
}
namespace BlogCore.Api.Features.Posts.ListOutPostByBlog
{
    public class ListOutPostByBlogPresenter
    {
        private readonly IUserRepository _userRepository;

        public ListOutPostByBlogPresenter(IUserRepository userRepository)
        {
            _userRepository = userRepository;
        }

        {

            var authors = result.Items
                .Distinct()
                .ToList();

            {
                return x.SetAuthor(author?.Id, author?.FamilyName, author?.GivenName);
            });

                result.TotalItems,
                (int)result.TotalPages,
                items.ToList());
        }
    }
}

Và chúng ta cần có 1 nơi để đăng kí những đối tượng dependency này. Vì vậy, Dependency Injection đã xuất hiện, và chúng ta sử dụng module Autofac trong dự án này. Ý tưởng này là module sẽ tự đăng kí tất cả dependency.

namespace BlogCore.PostContext
{
    public class PostUseCaseModule : Module
    {
        protected override void Load(ContainerBuilder builder)
        {
            base.Load(builder);

                .SingleInstance();

                .AsSelf()
                .SingleInstance();

                .AsSelf()
                .SingleInstance();
        }
    }
}

Sau đó, chúng ta chỉ cần giới thiệu API đó cho những gì chúng ta đã làm

namespace BlogCore.PostContext
{
    [Produces("application/json")]
    [Route("public/api/blogs")]
    public class PostApiPublicController : Controller
    {
        private readonly ListOutPostByBlogInteractor _listOutPostByBlogInteractor;
        private readonly ListOutPostByBlogPresenter _listOutPostByBlogPresenter;

        public PostApiPublicController(
            ListOutPostByBlogInteractor listOutPostByBlogInteractor,
            ListOutPostByBlogPresenter listOutPostByBlogPresenter)
        {
            _listOutPostByBlogInteractor = listOutPostByBlogInteractor;
            _listOutPostByBlogPresenter = listOutPostByBlogPresenter;
        }

        [HttpGet("{blogId:guid}/posts")]
        {
            var result = _listOutPostByBlogInteractor.Process(new ListOutPostByBlogRequest(blogId, page <= 0 ? 1 : page));
            return await _listOutPostByBlogPresenter.Transform(result);
        }
    }
}

Tổng kết

Như vậy, tôi đã giúp bạn hiểu được cách giúp modular hoạt động hiệu quả trong Clean Architecture. Ít nhất chúng ta cũng biết được modular là gì, điểm nào thực sự quan trọng. Chúng ta cũng đã đi qua 1 vài overview về Clean Architecture và 1 vài điểm mạnh của nó. Và cuối cùng, biết cách implement bằng .NET Core 2.0

Hy vọng bạn có thể trả lời được câu hỏi đặt ra trên tiêu đề. Tuy nhiên, có những nội dung không được phân tích trong bài này chính là Data flowSynchronized giữa Bounded ContextsUnit TestingDeployment cho Clean Architecture.

Những điểm thú vị về nội dung

  • Biết cách modular patterns làm việc với Clean Architecture trong cùng stack
  • Hiểu thêm vài điểm mạnh & điểm yếu của Modular & Clean Architecture
  • Hiểu thêm về Clean Architecture trong thực tế
  • Biết cách sử dụng .NET Core 2.0 để implement Blog Domain

Source Code