Flutter architecture design series — Bài 2. Clean architecture — kịch bản ấy đẹp, tiếc là…

Nguyễn Văn Biên
13 min readNov 10, 2023

--

Quy trình phát triển của một developer: Gấu chê nghèo → Buồn bã → Muốn nhảy việc → Đi phỏng vấn → Bị chê yếu → Rớt phỏng vấn → Chầm kẽm → Tìm hiểu kiến thức bị yếu → Đã hiểu → Vui mừng → Viết bài chia sẻ lại → Phỏng vấn lại → Nhảy việc thành công.

Đấy là ai chứ không phải mình, mình chỉ đơn giản là muốn viết lại những kiến thức này — vừa để hiểu chúng cặn kẽ hơn, vừa góp 1 phần sức lực bé nhỏ cho cộng đồng Flutter Việt Nam. Cống hiến đi anh em, nhảy việc là gì :D

Đây là bài thứ hai trong series Flutter architecture design. Ở bài này, chúng ta sẽ tìm đáp án cho những câu hỏi sau:

  • Why: Tại sao project nào cũng cần phải có kiến trúc? Clean archiecture (CA) giải quyết vấn đề gì?
  • Where: Khi nào nên dùng CA?
  • How: Dùng CA như thế nào?

Bắt đầu nào (Press F5 to continue)!

Một số series mình đang thực hiện:

Mục lục

Tóm tắt bài trước
1. Why: Tại sao CA lại ra đời?
1.1. Tại sao project nào cũng phải có một kiến trúc rõ ràng?
1.2. CA giúp giải quyết vấn đề gì?
2. Where: Khi nào nên dùng CA?
3. How: Dùng CA như thế nào?
3.1. Biểu đồ
3.2. CA ư? Kịch bản ấy đẹp, tiếc là…
3.3. Thảo luận liên quan
4. Tổng kết
Tham khảo

Tóm tắt bài trước

bài trước mình đã giới thiệu qua về MVVM và Clean architecture (viết tắt: CA), mình sẽ tóm tắt lại một chút.

What: Clean architecture chia ứng dụng của chúng ta thành nhiều tầng, tầng bên trong không biết gì về tầng bên ngoài và tầng bên ngoài phụ thuộc vào tầng bên trong.

Hình 0.1. Clean architecture (1) (nguồn ảnh: The Clean Code Blog)
Hình 0.2. Clean architecture (2) (nguồn ảnh: Coding Blocks)

Nhìn hình, chú Bob đưa ra 4 layer cho Clean architecture của mình:

  • Entities: Thực thể, tầng sâu nhất và ít bị thay đổi nhất. Ví dụ: UserModel, ProductModel, CategoryModel… (entities nên được viết bằng Dart thuần).
  • Use cases: Mang ý nghĩa tương tự như UML use case trong môn công nghệ phần mềm software engineering — tức là các chức năng/hành động mà hệ thống cung cấp cho actor (người dùng). Ví dụ, actor có thể thực hiện hành động “thêm sản phẩm vào giỏ hàng”, “xoá sản phẩm khỏi giỏ hàng”, “đặt hàng”…
  • Interface adapters (Controllers/Presenters/Gateways): Là layer trung gian giữa Use casesEntities. Ví dụ, ta có Use case “thêm sản phẩm vào giỏ hàng” và Entities “ProductModel” & “CartModel”. Interface Adapters Gateways sẽ gọi RESTful API để backend server thực hiện nhiệm vụ thêm sản phẩm vào giỏ hàng.
  • Frameworks & Drivers (Devices/Web/UI/External Interfaces/Database): Là các thư viện và framework sử dụng để xây dựng ứng dụng của mình. Ví dụ như Flutter (Dart chỉ là ngôn ngữ lập trình thôi nhé), React Native, ReactJS, Angular, NodeJS, Python Flask Server, Firebase…
Hình 0.3. MVVM (nguồn ảnh: Wikipedia)

Model-View-ViewModel là cải tiến của MVP nhằm khắc phục nhược điểm của mối quan hệ 1–1 giữa View và Presenter. MVVM mang hơi hướng của data binding/event-driven, trong đó:

  • View: Render UI và nhận tương tác từ người dùng. View sẽ gửi user event cho ViewModel. View lắng nghe (observe) state changes từ ViewModel và từ đó câp nhập lại UI.
  • ViewModel: Nhận user event từ View. Sau đó, VM sẽ update Model rồi nhận sự thay đổi từ Model và đổi trạng thái (state changes). Bất cứ View nào đang lắng nghe (observe) sự thay đổi trạng thái này thì nó sẽ biết được tín hiệu cập nhập lại UI.
  • Model: Chứa business logic cốt lõi của ứng dụng và còn dùng để thao tác với dữ liệu từ data source.

Mối quan hệ giữa View-ViewModel là mối quan hệ nhiều-1. Trong Flutter project, các state-management như BloC hay GetX mà chúng ta code từ trước tới nay đều mang hơi hướng của data binding và mô hình MVVM.

1. Why: Tại sao CA lại ra đời?

1.1. Tại sao project nào cũng phải có một kiến trúc rõ ràng?

Bởi vì:

  • Dự án có thể được tách ra thành nhiều module nhỏ hơn (modularization).
  • Hệ thống phải có khả năng mở rộng, đáng tin cậy và dễ bảo trì (Scalability, Reliability, Maintainability).

Xây dựng sản phẩm phần mềm cũng giống như việc tạo ra chiếc MiG-21 — cần rất nhiều nguồn lực và nỗ lực kết hợp giữa hàng chục kỹ sư phần mềm với nhau. Nếu bạn là Product Owner, làm sao bạn có thể quản lý được dự án khổng lồ này?

Hình 1.1. MiG-21 (nguồn ảnh: [W1])

Đúng vậy, Product Owner sẽ chia nhỏ dự án thành nhiều module khác nhau. Mỗi module sẽ được thực thi bởi một team nhỏ hơn. Giờ đây Product Owner sẽ biết được tiến độ dự án tổng thể dựa vào tiến độ của các module, từ đó có thể gõ đầu từng team leader và khiến dự án được trôi chảy.

Hình 1.2. Các bộ phận của MiG-21 (nguồn ảnh: HistoryNet)

MiG-21 được sản xuất từ những năm 50 và được tin cậy sử dụng đến tận hơn nửa thế kỷ sau đó, hơn 50 quốc gia trên 4 lục địa từng sử dụng loại máy bay này [1]. Hơn nửa thế kỷ và hàng chục quốc gia, vô số biến thể sinh ra để phù hợp với nhu cầu và học thuyết chiến tranh của họ. Nhiều biến thể của MiG-21 sinh ra với những nâng cấp về hệ thống radar, số lượng tên lửa mang theo, súng máy gắn ngoài… Những lần mở rộng, bảo trì và sửa chữa. Đúng thế, đây chính là tư tưởng tiếp theo của software architecture design: Phần mềm phải có khả năng scale, đáng tin cậy và dễ bảo trì.

Sau khi nghe mình bốc phét, bạn thấy đấy, kỹ sư phần mềm và kỹ sư tiêm kích có nhiều điểm chung hơn bạn tưởng. Mọi sản phẩm ta thiết kế ra đều phải mang những đặc điểm kiến trúc sau:

  • Dự án có thể được tách ra thành nhiều module nhỏ hơn.
  • Hệ thống phải đáng tin cậy, có khả năng mở rộng và dễ bảo trì.

Bay một vòng quanh thế giới, không thiếu những bài chê trách và nói software architecture là thứ đbrr (Software Architecture Is For Suckers [O1]). Bạn đừng vội đánh giá, một vài luận điểm mà họ đưa ra khá thú vị. Mình sẽ viết về chúng ở bài sau. Nhớ theo dõi mình nhé ;)

1.2. CA giúp giải quyết vấn đề gì?

  1. Đi tìm việc: Đúng rồi đấy, architecture design là kiến thức phổ thông nên có thể transfer qua nhiều ngôn ngữ lập trình khác chứ không riêng gì Flutter, như Java/Kotlin, Swift, cả back-end lẫn front-end đều có. Biết để còn đi phỏng vấn :D
  2. Dễ hiểu/dễ bảo trì/dễ mở rộng: Mỗi layer đều có một nhiệm vụ riêng biệt. Trong layer cũng chia làm vài thành phần làm chức năng khác nhau. Nắm bắt được concept của chúng, ta sẽ thấy CA thật sự dễ hiểu. Mặt khác, việc chia layer giúp ta dễ dàng debug, viết unit test, dễ mở rộng…

2. Where: Khi nào nên dùng CA?

Có thể tóm tắt vài đặc điểm technical của CA:

  • Hệ thống được chia làm nhiều layer.
  • Các layer giao tiếp với nhau thông qua interface.

Chúng ta sẽ liệt kê một số nhược điểm:

  • Concept của từng phần riêng lẻ thì dễ hiểu nhưng tổng thể lại phức tạp. Bạn có tự tin rằng bản thân mình đã ghi nhớ toàn bộ mọi thành phần của nó (hình 3.1)?
  • Vì nó cồng kềnh và phức tạp, nên ngốn nhiều thời gian và nguồn lực.
  • Dễ đập bàn phím. Nghĩ mà xem, có một cái bug ở UI, bạn phải dò qua 7749 bước, gần chục file mà vẫn đếch tới phần implementation.
  • Yêu cầu nhân sự có trình độ nhất định về CA, tốt nhất là người có hơn 200 năm kinh nghiệm cân bằng… ấy chết, lộn.

Qua những điều kể trên, có thể thấy, CA chỉ thích hợp cho những dự án có đủ thời gian và kinh phí — điển hình là những hệ thống cung cấp giải pháp cho doanh nghiệp, chẳng hạn như ngành bán lẻ hoặc giải pháp chính phủ… Các hệ thống enterprise tồn tại trong thời gian rất lâu, có thể lên tới hàng chục năm (mình đang bảo trì một hệ thống như vậy). Những dự án nhỏ hay cần outsource gấp thì tốt hơn hết là đừng.

Sau bài này, mình sẽ viết tiếp về một cách setup kiến trúc Flutter project đơn giản hơn nhưng không kém phần hiệu quả. Nó vẫn chia project thành những layer nhằm đảm bảo tính bảo trì và mở rộng, nhưng đơn giản hơn, ít abstraction hơn. Nếu các bạn có hứng thú, hãy bấm follow mình nhé!

There is no best architecture. There is only architecture that’s best for your application and your needs. [A2]

3. How: Dùng CA như thế nào?

CA chỉ là một khái niệm khuyến khích ta tách các thành phần low level khỏi high level bằng cách sử dụng abstraction. Vì vậy ta có thể thay thế các thành phần một cách dễ dàng miễn là thỏa mãn ranh giới giữa chúng (interface) — trích lời ai đó trên StackOverflow.

3.1. Biểu đồ

HÌnh 3.1. Mọi người thường áp dụng 3 layer trong mobile development: Presentation, Domain và Data layer
Hình 3.2: Sơ đồ Flutter + Clean architecture (có tham khảo từ Reso Coderfive.agency)

3.2. CA ư? Kịch bản ấy đẹp, tiếc là…

Một cô gái nào đó từng nói: “… sự thật thì luôn đơn giản nhưng people make it complicated”. Thành thạo CA là một quá trình dài của sự đúc kết kinh nghiệm chứ không chỉ đọc một vài bài viết rồi tự tin nói “tôi đã master CA”. Những người mới rất dễ bị overengineering, tẩu hỏa nhập ma, thiết kế phức tạp hơn cần thiết.

Trong hơn 2 năm làm việc, mình thấy CA trong Flutter có một số điểm cần lưu ý như sau:

3.2.1. Chúng ta có thật sự cần Repository abstraction không?

Như đề cập ở sơ đồ 3.2, Domain layer là tầng ổn định nhất và ít bị thay đổi nhất, ít phụ thuộc vào framework hay thư viện. Repository abstraction không nên phụ thuộc vào một thư viện cụ thể nào (dio, http…) để lấy dự liệu từ data source, cái đó thuộc về Repository implementation. Sau này nếu implementation có thay đổi, ta không cần phải thay đổi gì ở abstraction.

Bạn có chắc là nếu implementation thay đổi thì ta không cần sửa luôn ở abstraction không? :)

3.2.2. Use case dường như hơi thừa thãi

Theo mình quan sát, các tác vụ logic thường được xử lý ở server-side — tức hệ thống back-end. Nó sẽ giúp giảm tải gánh nặng lên chiếc smartphone bé nhỏ của user, giúp trải nghiệm được mượt mà hơn.

Rất nhiều Use case được sinh ra chỉ dùng để gọi tới Repository, như đoạn code sau đây:

void changeProfileNameUC(String newName) async {
await _profileRepository.changeProfileName(newName);
}

void createPurchaseOrderUC(PurchaseOrderModel purchaseOrderModel) {
await _orderRepository.createPurchaseOrder(...);
}

Không phải là tất cả, nhưng đa số business logic hiện nay đều được đem về team back-end xử lý hết (có lẽ cũng vì vậy mà lương của họ lúc nào cũng cao hơn chúng ta, haha).

Một điểm nữa cần lưu ý, đó là “Widget’s state management” ở Presentation layer thường kiêm luôn cả việc xử lý logic luôn chứ không chỉ đơn thuần là quản lý state.

class ProfileBloc extends Bloc<ProfileEvent, ProfileState> {
// ...

ProfileBloc() : super(const ProfileInitial()) {
on<ChangeProfileNameEvent>((event, emit) {
emit(const LoadingState());
try {
await _repository.changeProfileName(event.newName);
emit(const SucceedState());
} catch (e) {
emit(const FailedState());
}
});
}
}

3.2.3. Thực tế thì như thế này

Hình 3.3. Lý thuyết và thực tế

CA chỉ là một khái niệm khuyến khích ta tách các thành phần low level khỏi high level bằng cách sử dụng abstraction. Vì vậy ta có thể thay thế các thành phần một cách dễ dàng miễn là thỏa mãn ranh giới giữa chúng (interface).

Mình không thế nói rằng cách bên trái hay cách bên phải hiệu quả hơn, cái này tùy thuộc vào góc nhìn của bạn. Năm nay bạn có thể thích cách bên phải nhưng vài năm nữa bạn lại thấy cách bên trái ổn hơn thì sao?

3.3. Thảo luận liên quan

Question 1: Ơ, blog của chú Bob có nói quái gì tới Presentation-Domain-Data layer đâu mà sao bài viết nào cũng mặc định rằng CA là phải có 3 layer này vậy?

Answer 1: Đúng rồi, CA không phải là Presentation-Domain-Data. CA là một bộ quy tắc hướng dẫn. Ngược dòng lịch sử, theo Marko Novakovic [A2] thì có vẻ như người đầu tiên “implement” bộ quy tắc này vào mobile developmentFernando Cejas [A4] vào 2014. Cách làm này được cho là khá “sạch”, đúng với tinh thần trong guideline của Uncle Bob năm 2012 và vẫn còn được áp dụng trong mobile development tới tận ngày nay.

— — — — —

Question 2: Feature nên được chia bên trong thư mục Presentation/Domain/Data hay là trong mỗi feature phải bao gồm 3 loại thư mục là Presentation/Domain/Data (hình bên dưới)?

Hình 3.4: Hai cách tổ chức thư mục dùng CA. Cách bên trái là feature nằm trong Presentation/Domain/Data. Ngược lại, cách bên phải là Presentation/Domain/Data năm trong feature (nguồn: Marko Novakovic [A2])

Answer 2: Bạn tham khảo bài viết [A2] và [A3]. Tóm tắt: Dự án quy mô lớn, có thể bổ sung thêm nhiều feature trong tương lai thì nên chọn cách bên phải.

— — — — —

Question 3: Không có hướng dẫn set-up project chi tiết hả bro?

Answer 3: Không có bro. Mấy cái này thuộc về bí mật công nghệ của từng công ty cũng như kinh nghiệm của từng cá nhân. Người thì thích dùng kế thừa như BaseView, BaseController, BaseRepository… Người thì thích dùng StatefulWidget, Scaffold… cơ bản. Hơn nữa cái này cũng có nhiều người làm rồi, các bạn có thể xem ở phần tham khảo, mục Flutter.

4. Tổng kết

Hi vọng qua bài viết này, các bạn đã nắm bắt được:

  • Tại sao ta lại cần thiết kế một kiến trúc hợp lý cho các dự án phần mềm?
  • Một kiến trúc thế nào được cho là “sạch” theo guideline của Uncle Bob năm 2012?
  • Cân nhắc thiệt/hơn khi áp dụng Clean architecture vào dự án.
  • Cách mà thế giới áp dụng guideline của Uncle Bob và tạo thành “Flutter Clean architecture” như hiện nay.
  • Các thành phần của Clean architecture, nhiệm vụ của chúng và cách chúng tương tác với nhau.
  • Một kiểu cấu trúc phổ biến hiện nay trong Flutter project.
  • Một số câu hỏi liên quan.

Mình cảm thấy rất vui nếu những bài viết này góp phần vào sự phát triển của cộng đồng developer Việt Nam. Nếu bạn cảm thấy chúng hữu ích và muốn ủng hộ mình, bạn có thể mua cho mình 1 cây bút bi tại thông tin phía dưới. Sự ủng hộ của mọi người là nguồn động lực để mình ra nhiều bài viết chất lượng hơn trong tương lai. Mình xin cảm ơn!

Ủng hộ mình 1 cây bút bi:

  • Tên người nhận: Nguyễn Văn Biên
  • Tài khoản VietinBank: 0835465951
  • MoMo: 0835465951

Tham khảo

# Wikipedia

[W1] Wikipedia. Mikoyan-Gurevich MiG-21. URL: https://vi.wikipedia.org/wiki/Mikoyan-Gurevich_MiG-21. Last accessed: 7/9/2023.

# Flutter

[F1] AbdulMuaz Aqeel. Flutter Clean Architecture Series — Part 1. URL: https://devmuaz.medium.com/flutter-clean-architecture-series-part-1-d2d4c2e75c47. Last accessed: 30/9/2023.

[F2] AbdulMuaz Aqeel. Flutter Clean Architecture Series — Part 2. URL: https://devmuaz.medium.com/flutter-clean-architecture-series-part-2-bcdf9d38fe41. Last accessed: 30/9/2023.

[F3] Kamlesh Lakhani. Clean architecture | Flutter. URL: https://medium.com/@kamal.lakhani56/clean-architecture-f23b7d9c6ee7. Last accessed: 4/11/2023.

[F4] Reso Coder. Flutter TDD Clean Architecture Course. URL: https://resocoder.com/flutter-clean-architecture-tdd. Last accessed: 4/11/2023.

[F5] Nguyễn Thành Minh. Flutter — Xây dựng Base Project hoàn hảo từ A đến Á. URL: https://medium.com/@NALSengineering/list/flutter-xay-dung-base-project-hoan-hao-tu-a-en-a-c240f845f660. Last accessed: 5/11/2023.

[F6] Sxndrome. Where to perform state changes with Clean Architecture in Flutter? URL: https://stackoverflow.com/questions/66690169. Last accessed: 8/11/2023.

# Android

[A1] Denis Brandi. The “Real” Clean Architecture in Android: S.O.L.I.D. URL: https://betterprogramming.pub/the-real-clean-architecture-in-android-part-1-s-o-l-i-d-6a661b103451. Last accessed: 4/11/2023.

[A2] Marko Novakovic. Clean Architecture is not Domain-Data-Presentation. URL: https://markonovakovic.medium.com/clean-architecture-is-not-domain-data-presentation-e368d7ff8579. Last accessed: 4/11/2023.

[A3] Martin Fowler. PresentationDomainDataLayering. URL: https://martinfowler.com/bliki/PresentationDomainDataLayering.html. Last accessed: 4/11/2023.

[A4] Fernando Cejas. Architecting Android... the clean way? URL: https://fernandocejas.com/blog/engineering/2014-09-03-architecting-android-the-clean-way. Last accessed: 4/11/2023.

# iOS

[I1] Paul Allies. Clean Architecture: iOS App. URL: https://paulallies.medium.com/clean-architecture-ios-app-100539550110. Last accessed: 7/9/2023.

[I2] Oleh Kudinov. Clean Architecture and MVVM on iOS. URL: https://tech.olx.com/clean-architecture-and-mvvm-on-ios-c9d167d9f5b3. Last accessed: 4/11/2023.

[I3] Oleh Kudinov. Modular Architecture in iOS. URL: https://tech.olx.com/modular-architecture-in-ios-c1a1e3bff8e9. Last accessed: 4/11/2023.

# React

[R1] Ken Miyashita. Clean Architecture With React. URL: https://betterprogramming.pub/clean-architecture-with-react-cc097a08b105. Last accessed: 5/11/2023.

# Objection

[O1] Andrew Zuo. Software Architecture Is For Suckers. URL: https://medium.com/lost-but-coding/software-architecture-is-for-suckers-fabbea6763f. Last accessed: 7/9/2023.

[O2] Molly Rocket. The Only Unbreakable Law. URL: https://www.youtube.com/watch?v=5IUj1EZwpJY. Last accessed: 7/9/2023.

Nguyễn Văn Biên — 11/2023 — Số năm kinh nghiệm: 2 năm

--

--