Scaling Android Architecture #1: Động não đơn giản hơn với Class Diagram
Chào mừng bạn đến với tập đầu tiên của loạt bài Mở rộng kiến trúc Android 🚀. Trước khi bắt đầu sử dụng Android Studio và tạo một số đoạn code, hãy xem công cụ này rất hữu ích khi bạn đưa ra các quyết định về kiến trúc trong dự án. Công cụ này được gọi là UML Class Diagram.
UML là cái gì ❓
Một số bạn có thể không biết UML là gì hoặc chỉ nghe thấy cái tên này ở đâu đó mà không có ngữ cảnh rộng hơn. Hãy để tôi bắt đầu với một giới thiệu ngắn.
UML là viết tắt của Unified Modeling Language và được sử dụng rộng rãi trong nhiều lĩnh vực của ngành phát triển phần mềm. Nó bao gồm hàng chục loại biểu đồ khác nhau giúp chúng ta thể hiện các giải pháp phần mềm theo cách đồ họa.
Just no diagrams, just let me code 😰
Có những công ty, projects và teams nơi mọi thứ được ghi lại bằng các loại sơ đồ khác nhau. Trong phương pháp Waterfall, toàn bộ hệ thống được mô tả đầu tiên bằng các tài liệu chính thức bao gồm các sơ đồ UML. Chỉ sau đó, nó mới có thể được chuyển sang giai đoạn triển khai nơi các nhà phát triển chuyển đổi tài liệu thành code. Bạn thậm chí có thể tìm thấy các giải pháp phần mềm doanh nghiệp lớn có khả năng tạo ra một ứng dụng đang hoạt động từ các cấu trúc UML.
Nghe có vẻ tuyệt phải không? 😄 May mắn thay, chúng ta đang sống trong thế giới Agile nơi UML vẫn được coi là một công cụ rất hữu ích, nhưng theo một cách khác và thân thiện hơn.
ℹ️ Sơ đồ dễ hiểu hơn nhiều so với từ ngữ. Hình thức trực quan cho phép chúng ta minh họa rõ ràng cho những người còn lại trong team những gì chúng ta đang nghĩ. Ít hiểu lầm hơn == giao tiếp đơn giản hơn.
Bằng cách này, chúng ta có thể đề xuất các giải pháp khác nhau, thảo luận với nhóm và xác minh xem mọi người có thấy chúng hoạt động như mong đợi hay không. Tất cả những điều đó mà không cần viết một dòng code nào.
Sơ đồ là nhanh và đơn giản để sử dụng khi chúng ta không coi chúng là tài liệu hoàn chỉnh của toàn bộ hệ thống. Ngay cả khi hệ thống lớn và phức tạp, chúng ta có thể thu hẹp nội dung của sơ đồ để phù hợp với bối cảnh thảo luận, bỏ qua những phần không quan trọng và đơn giản hóa những gì có thể đơn giản hóa.
Biểu diễn kiến trúc bằng Class Diagram
Qua nhiều sơ đồ UML khác nhau, chúng ta có thể tìm thấy một sơ đồ rất hữu ích khi nói về kiến trúc và cấu trúc ứng dụng. Sơ đồ này được gọi là Class Diagram. Hãy bắt đầu với một số khái niệm cơ bản.
Class
Như cái tên bạn có thể đã nhận thấy, Class Diagram được sử dụng để biểu diễn các classes của ứng dụng. Ở dạng đơn giản nhất, UML class được biểu diễn dưới dạng hình chữ nhật với tên bên trong. Sơ đồ đơn có thể chứa nhiều class khác nhau với các tên khác nhau. Mỗi class đó đại diện cho một class thực tế từ codebase của chúng ta.
class SearchProductViewModel {
...
}
class ProductDetailsViewModel {
...
}
class ProductsRepository {
...
}
Sự kết hợp (Association)
Mỗi ứng dụng hoạt động nhờ sự giao tiếp giữa các class. Khi một class sử dụng một class khác, chúng ta nói rằng có một Association giữa chúng.
Trên các loại Association khác nhau, loại cơ bản nhất được gọi là Unidirectional Association. Association này xảy ra khi một class có tham chiếu đến một class khác. Ví dụ: khi ViewModel
của chúng ta có tham chiếu Repository
được chuyển vào từ hàm tạo.
Loại Association này được thể hiện dưới dạng một đường liền nét với một mũi tên ở một đầu. Hướng của mũi tên này rất quan trọng. Mũi tên phải luôn trỏ từ class chứa tham chiếu đến class được tham chiếu.
class SearchProductViewModel(
private val productsRepository: ProductsRepository
) {
...
}
class ProductDetailsViewModel(
private val productsRepository: ProductsRepository
) {
...
}
class ProductsRepository {
...
}
Tổng quát hóa (Generalization)
Trong lập trình hướng đối tượng, đôi khi chúng ta giới thiệu một abstractions chung cho các class của chúng ta. Một điều rất nổi tiếng là androidx.lifecycle.ViewModel
abstractions mà chúng ta sử dụng rộng rãi trong các ứng dụng. Các ViewModel của chúng ta phụ thuộc vào abstractions này nhưng chúng không có tham chiếu đến nó.
Thay vì nó, chúng mở rộng class base Lifecycler ViewModel để kế thừa một số hành vi phổ biến của nó. Trong ký hiệu UML, kế thừa được biểu diễn dưới dạng kiểu Liên kết đặc biệt được gọi là Generalization.
Ở đây chúng ta đã sử dụng một ký hiệu đặc biệt để đánh dấu View Model là một abstract class
. Theo mặc định, tất cả các khối trong UML Class Diagram đại diện cho một class
. Nếu chúng ta muốn chỉ ra rằng đây là một loại đơn vị đặc biệt nào đó, chúng ta sử dụng ký hiệu <<*>> ở đầu tên.
abstract class ViewModel {
...
}
class SearchProductViewModel(
private val productsRepository: ProductsRepository
) : ViewModel() {
...
}
class ProductDetailsViewModel(
private val productsRepository: ProductsRepository
) : ViewModel() {
...
}
class ProductsRepository {
...
}
Hiện thực hóa (Realization)
Truyền một instance của class tới hàm tạo của class khác không phải là cách duy nhất để các class có thể phụ thuộc vào nhau. Bạn có thể biết nguyên tắc Dependency Inversion từ bộ nguyên tắc SOLID nổi tiếng. Chúng ta thường đặt các interface
trong codebase của mình để đảo ngược một số phụ thuộc.
Khi một class
implement một interface
thì cũng có một Association giữa chúng. Giống như đối với tính kế thừa, class không có tham chiếu đến một instance của interface
, vì vậy chúng ta không thể sử dụng Unidirectional Association ở đây.
Để đại diện cho trường hợp này, có một loại Association tiếp theo được gọi là Realization. Nói một cách đơn giản, nó cho chúng ta biết rằng class
đã cho nhận ra một hành vi được xác định bởi interface
.
class SearchProductViewModel(
private val productsRepository: ProductsRepository
) {
...
}
class ProductDetailsViewModel(
private val productsRepository: ProductsRepository
) {
...
}
interface ProductsRepository {
...
}
class FakeProductsRepository : ProductsRepository {
...
}
Attributes và Methods
Các Associations giúp chúng ta hiểu cách các classes nhất định sử dụng lẫn nhau. Class Diagram của chúng ta có thể chi tiết hơn thế. Nó có thể biểu thị dữ liệu chính xác nào được lưu giữ bởi class và những hoạt động nào nó cung cấp. Dữ liệu được giữ bên trong class được gọi là Attribute trong khi các hoạt động được biểu diễn dưới dạng Methods.
Để thêm Attributes và Methods cho class, chúng ta thêm các phần riêng biệt vào khối hình chữ nhật. Đối với các class, khối đầu tiên được đảo ngược cho Attributes và khối thứ hai cho Methods. Các interface không giữ dữ liệu nên chúng chỉ có một phần bổ sung dành riêng cho Methods.
Ngoài ra, chúng ta có thể bao gồm các công cụ sửa đổi khả năng hiển thị trước tên Attributes hoặc Methods. Chúng hầu hết giống như chúng ta thường thấy trong ngôn ngữ lập trình: public (+), private (-), protected (#).
class SearchProductViewModel(
private val productsRepository: ProductsRepository
) {
var searchInput by mutableStateOf("")
private set
val matchingProducts: StateFlow<List<Product>> ...
fun searchProduct(name: String) ...
fun clearSearch() ...
}
class ProductDetailsViewModel(
private val productId: String,
private val productsRepository: ProductsRepository
) {
val product: StateFlow<Product> ...
fun addToCart() ...
fun showComments() ...
}
class ProductsRepository {
suspend fun getAllProducts(): List<Product>
suspend fun getProduct(id: String): Product
suspend fun searchProduct(name: String): List<Product>
}
class FakeProductsRepository : ProductsRepository {
private val productsCache = Cache<Product>()
override fun getAllProducts(): List<Product> ...
override fun getProduct(id: String): Product ...
override fun searchProduct(name: String): List<Product> ...
}
Packages và Modules
Khi xây dựng một ứng dụng trong thế giới thực, chúng ta luôn cố gắng group code của mình để được tổ chức tốt hơn và dễ hiểu hơn. Với mục đích này, chúng ta thường dựa vào các Packages và Modules. Cả hai đều có thể được phản ánh trên diagram theo cùng một cách, cho phép chúng ta đặt nhiều class vào một nhóm duy nhất.
Biểu diễn model bằng Class Diagram
Định nghĩa một architecture bằng cách sử dụng Class Diagram là một trường hợp sử dụng tuyệt vời cho nó. Thứ hai là mô tả data model. Đôi khi chúng ta làm việc trên ứng dụng dành cho ứng dụng khách dày hơn ứng dụng mỏng. Trong trường hợp như vậy, chúng ta không chỉ load data từ Backend và hiển thị nó trên màn hình. Chúng ta cần đại diện cho một số vấn đề business như một mô hình ứng dụng của chúng ta. Khi xây dựng một mô hình, có một số ký hiệu UML đặc biệt hữu ích.
Association Multiplicity
Multiplicity của Association xác định có bao nhiêu thể hiện của class được tham chiếu. Theo mặc định, khi chúng ta không chỉ định bất kỳ Multiplicity nào thì điều đó có nghĩa là chỉ có một phiên bản được tham chiếu. Nếu chúng ta muốn phản ánh các trường hợp khác nhau, chúng ta có thể sử dụng ký hiệu sau.
0..1
có nghĩa là không hoặc một instance của class. Chúng ta có thể coi nó như một tài liệu tham khảo không bắt buộc.*
có nghĩa là không hoặc nhiều instance của class. Nó đề cập đến việc passing một số collection của một type nhất định. Collection này có thể empty hoặc chứa nhiều instance của cùng một class.1..*
có nghĩa là 1 hoặc nhiều instance của class này. Ở đây chúng ta có thể sử dụng collection như trước đây nhưng chúng ta cần áp dụng một số xác thực bổ sung để kiểm tra xem collection có ít nhất một phần tử hay không.
data class Product(
// One instance
val price: Price,
// Zero or one instance
val image: Image?,
// Zero or many instances
val comments: List<Comment>,
// One or many instances
val colors: List<Color>,
) {
init {
// Check if we have at least one Color
if (colors.isEmpty()) {
error("Product needs to have at least one Color")
}
}
}
Aggregation và Composition
Khi nghĩ về data, chúng ta cũng cần xem xét vòng đời của data này. Ví dụ: chúng ta có một class Category
đề cập đến nhiều instances của class Product
.
Bây giờ điều gì sẽ xảy ra khi chúng ta xóa một đối tượng Category
? Chúng ta cũng có nên xóa tất cả Product
được Category
này tham chiếu không? Hoặc có thể chúng ta muốn giữ chúng để chúng có thể được chuyển sang một Category
khác?
Để thể hiện những mối quan tâm này, ký hiệu UML cung cấp thêm hai loại Association: Aggregation và Composition. Cả hai đều được thể hiện dưới dạng một đường liền nét với một hình thoi ở cuối.
Aggregation có nghĩa là các instance được tham chiếu có vòng đời độc lập với phiên bản gốc. Sau khi bản gốc bị xóa, chúng vẫn có thể tồn tại trong hệ thống. Ở đây hình thoi Association được lấp đầy.
Mặt khác, các Compositions được biểu thị bằng hình thoi được lấp đầy chỉ ra rằng tất cả các instance được tham chiếu đều có chung vòng đời với phiên bản gốc. Khi bản gốc bị xóa khỏi hệ thống, chúng cũng sẽ bị hủy.
⚠️ Trong Aggregation và Compositionm, hình thoi nằm ở phía đối diện của liên kết — ở phía sở hữu, không phải phía được tham chiếu.
Image
có thể tồn tại độc lập với Product
. Khi chúng ta xóa Product
, chúng ta vẫn giữ một Image
trong hệ thống để có thể đính kèm Image
đó vào một Product
khác .
Comment
có cùng vòng đời với một Product
. Khi chúng ta xóa Product
, không có lý do gì để giữ lại các Comments
liên quan. Product
mới trông giống nhau có thể có chất lượng được cải thiện nên sẽ không có lý do gì để giữ lại những Comment cũ.
UML tools
Và cuối cùng, tôi muốn giới thiệu cho bạn một số công cụ mà bạn có thể sử dụng để tự tạo các diagram UML.
Diagrams.net
Một công cụ miễn phí mà bạn có thể nhanh chóng kết nối với Google Drive. Chúng ta có thể tạo sơ đồ mới trực tiếp từ Drive bằng cách mở menu tạo tệp, đi tới Thêm và chọn tùy chọn diagrams.net. https://app.diagrams.net/
Lucidchart
Đối với những người tìm kiếm một số giải pháp thay thế tốt hơn, tôi thực sự khuyên bạn nên kiểm tra ứng dụng Lucidchart. Trong phiên bản miễn phí, nó chỉ cung cấp một số chức năng và dung lượng tệp hạn chế. Tuy nhiên, UX của trình chỉnh sửa rất mượt mà và các sơ đồ trông sáng bóng. https://www.lucidchart.com/pages/
Nguồn: https://medium.com/@maruchin/simpler-brainstorming-with-the-class-diagram-7b4b184fadcd
All rights reserved