logoMột hệ thống khó hiểu thì cũng khó thay đổi
Class Diagram: Những điều cơ bản

UML

Class diagram

Class diagram là dạng sơ đồ thường gặp nhất trong UML. Nó phổ biến như vậy một phần vì nó chứa dựng nhiều khái niệm mô hình hóa nhất.

Đây là một dạng sơ đồ thể hiện mối quan hệ tĩnh của các thành phần trong hệ thống. Trong bài viết này, chúng ta sẽ bàn về những nội dung cơ bản nhất về Class diagram để sao cho ta có thể dễ dàng sử dụng được nó trong công việc của mình:

  • Thuộc tính - Properties
  • Hành vi
  • Tổng quát hóa - Generalization
  • Phụ thuộc - Dependency
  • Ràng buộc
  • Aggregation và composition

Properties

Thông thường, khi nhắc tới properties của một class, chúng ta hình dung về các field của class đó. Đó là trường hợp attributes hay dùng và khi ta muốn đơn giản hóa những thông tin không quá quan trọng, ta sẽ làm vậy. Thực tế có những thứ ta cần nhấn mạnh hơn nữa, hoặc nó có cấu trúc phức tạp, ta cần cách biểu diễn kiểu khác với attributes.

Attributes

Hình 1: Properties được biểu diễn dưới dạng Attributes

Hình 1 thể hiện một class có tên Order cùng một số attributes id, status, createdAt, lineItems. 

Khai báo attribute theo cú pháp sau: visibility name: type multiplicity = default property-string

  • visibility: có các lựa chọn public (+), private (-), protected (#), default package ()
  • name: tên của attribute: id, status, createdAt, lineItems
  • type: kiểu dữ liệu của attribute: String, int, OrderLine, Date
  • multiplicity: lực lượng hay hệ số nhân thể hiện phạm vi số lượng đối tượng có thể của thuộc tính. Ví dụ, createdAt có multiplicity là 1 thể hiện attribute này phải có 1 giá trị Date duy nhất, còn lineItems với OrderLine[*] cho thấy lineItems có thể có 1 hoặc nhiều lineItems. Thông thường, nếu thông tin này không quá quan trọng thì ta có thể bỏ qua.
  • default value: là giá trị mặc định của attribute khi tạo đối tượng class mà không được chỉ định giá trị ban đầu. Nếu không chỉ định, ta có thể coi default là null.
  • : cho phép ta chỉ định thêm các tính chất bổ sung cho attribute. Ví dụ trên với lineItems cho thấy các phần tử này cần phải được sắp xếp thứ tự.

Associations

Như đã đề cập, atttributes là cách thể hiện ngắn gọn và thường dùng khi properties là không quá quan trọng, còn khi cần nhấn mạnh, ta nên sử dụng associations.

Hình 2: Property lineItems được biểu diễn dưới dạng Association

Hình 2 là biểu diễn khác của Hình 1 chỉ với thay đổi là orderLines thay vì biểu diễn bởi attributes thì đã chuyển sang association. Đó là bởi, ý đồ của người thiết kế muốn thể hiện rõ thông tin của OrderLine ra: 

  • Nó chứa hai thông tin cơ bản, và, nó không quan trọng lắm, nên họ thể hiện dưới dạng attributes: productId quantity.
  • Nó có một hành vi quan trọng cần nhấn mạnh: calculatePrice(UnitPrice): Money - tính toán giá trị của một lineItem dựa vào đơn giá của product.

Ngoài ra, trong association, ta còn thấy thông tin multiplicity được thể hiện ở hai đầu đường kết nối.

Multiplicity

Multiplicity của một thuộc tính cho biết số lượng đối tượng có thể điền vào thuộc tính đó. Những multiplicity phổ biến nhất mà bạn sẽ thấy là:

  • 1: thể hiện một giá trị duy nhất. Ví dụ: - customerId: CustomerId[1] => Một đơn hàng phải có đúng một khách hàng.
  • 0..1 : Thể hiện một thuộc tính có thể có duy nhất một hoặc không có đối tượng nào. Ví dụ: - completedDate: Date[0..1] => Một đơn hàng có thể chưa có ngày giờ hoàn thành.
  • * : Thể hiện một thuộc tính có thể có một hoặc nhiều hoặc không có đối tượng nào. Ví dụ: - lineItems: OrderLine[*] => Một đơn hàng có thể có một hoặc nhiều line items hoặc thậm chí không có line items nào.

Thông thường, khi một thuộc tính có chỉ định [*] - là số nhiều, ta nên đặt tên cho nó là số nhiều, ví dụ với lineItems trên.

Mặc định một thuộc tính có multiplicity là [1] và ta không cần chỉ định trừ khi nó quan trọng.

Bidirectional Associations

Xem lại ví dụ ở hình 2, ta thấy assocation từ Order tới OrderLine là một chiều: hướng từ Order sang OrderLine. Điều này cho thấy trong OrderLine không có thông tin nào để biết được nó thuộc Order nào.

Có nhiều tình huống, ta cần những association hai chiều.

Ví dụ, yêu cầu nghiệp vụ cần như sau: Từ một Car, ta cần biết được người sở hữu, kiểu Person, là ai; từ một người, ta cần lấy thông tin danh sách xe mà người đó sở hữu.

Hình 3: Mối quan hệ hai chiều bidirectional association giữa Person và Car. Thông tin association được đặt tên theo thuộc tính

 

Hình 4: Cách thể hiện mối quan hệ qua cụm động từ thay vì tên thuộc tính

Thông thường, việc đặt tên cho association là không nên trừ khi, một lần nữa, ta muốn nhấn mạnh thông tin mà nó mang lại.

Quan hệ trong ví dụ này được thể hiện như sau trong code Java:

public class Car {
private Person owner;
...
}
public class Person {
private List cars;
...
}
Khi đề cao tính hướng đối tượng hoặc làm việc với Domain-Driven Design (DDD), có thể Person sẽ đóng vai trò aggregate-root, và qua đó, danh sách Car không thể được truy cập tự do từ bên ngoài được mà phải thông qua giao diện của Person.
Biểu diễn mũi tên cho một association cũng nên loại bỏ nếu điều đó là không quan trọng.

Operations

Operations là các hành động mà class có thể thực hiện.

Thông thường, với những hành động get/set thuộc tính, ta không cần thể hiện chúng trên sơ đồ.

Có hai loại hành động chính trong một class:

  1. Hành động không làm thay đổi thuộc tính, ta đánh dấu là query.
  2. Hành động làm thay đổi thuộc tính, ta đánh dấu là command.

Điều này mang lại giá trị gì? Nếu ta làm nổi bật các hành động là query, ta có thể tự do thay đổi thứ tự thực hiện các query đó mà không làm thay đổi hành vi của hệ thống. Có một quy ước để viết hai loại hành động này là: query thì trả về giá trị còn command thì không (dù đôi khi khó áp dụng trong một số trường hợp, nhưng ta nên làm điều này nhiều nhất có thể).

Operation khác gì method?

Operation là thứ được gọi trên một đối tượng-là khai báo của thủ tục-còn method là phần thân của thủ tục đó. Hai thứ này khác nhau khi có tính đa hình (polymorphism). Giả sử ta có một lớp cha với ba lớp con, mỗi cái đều ghi đè operation getPrice() của lớp cha, thì ta có một operation và bốn methods triển khai nó.

Chỉ trong một số trường hợp, sự phân biệt này mới mang lại giá trị.

Generalization

Thông thường generalization liên quan tới kế thừa - inheritance. Tuy nhiên, ta cần phân biệt rõ hơn.

Ví dụ, một Customer là một khách hàng nhưng đó có thể là khách hàng cá nhân hoặc khách hàng doanh nghiệp. Do đó, ta cần một cấu trúc như sau:

Hình 5: Một supertype Customer được theo sau bởi hai subtype Personal Customer và Corporate Customer

Ở đây, ý tưởng then chốt là mọi thứ mà ta nói về một Customer - bao gồm associations, attributes, operations - đều đúng cho cả Personal Customer và Corporate Customer.

Một nguyên tắc quan trọng để sử dụng inheritance hiệu quả là tính thay thế (substitutability). Tôi nên có thể thay thế một Corporate Customer trong bất kỳ đoạn mã nào yêu cầu một Customer, và mọi thứ vẫn hoạt động bình thường. Điều này có nghĩa là nếu tôi viết mã giả định rằng tôi có một Customer, tôi có thể sử dụng bất kỳ subtype nào của Customer mà không gặp vấn đề. Corporate Customer có thể phản hồi các lệnh cụ thể khác biệt với Customer khác, sử dụng polymorphism, nhưng người gọi không cần phải lo lắng về sự khác biệt đó. (xem thêm về "L" trong SOLID của Robert C. Martin).

Mặc dù inheritance là một cơ chế mạnh mẽ nhưng nó lại là dạng có ràng buộc phụ thuộc mạnh mẽ mà không phải lúc nào cũng có thể thay thế được. Một ví dụ điển hình là vào thời kỳ đầu của Java, khi nhiều người không thích triển khai của lớp Vector tích hợp và muốn thay thế nó bằng một thứ gì đó nhẹ hơn. Tuy nhiên, cách duy nhất để tạo ra một lớp có thể thay thế cho Vector là kế thừa nó, và điều đó có nghĩa là phải kế thừa rất nhiều dữ liệu và hành vi không mong muốn.

Do đó, ta cần phân biệt giữa implementation một interfaceinheritance một superclass:

  • Subtyping: thể hiện class implement interface.
  • Subclassing: thể hiện việc class inheritance một superclass.

Dependency

Dependency là từ được nhắc tới nhiều với sự phổ biến của Spring framework: Dependency Injection.

Vậy dependency là gì?

Một class sử dụng class khác trong tham số của một operation.

Một class sử dụng class khác trong tính toán của một operation của mình.

Một class gửi một message tới một class khác.

Đó là một trong những tình huống mà class này depend vào class kia.

Khi xây dựng hệ thống, tất nhiên ta muốn dùng lại càng nhiều thứ càng tốt để tối ưu nguồn lực và nâng chất lượng sản phẩm nhưng khi đó ta càng phụ thuộc vào những thứ đó nhiều hơn.

Một điểm quan trọng là dependency chỉ tồn tại một hướng và đi từ lớp phụ thuộc sang lớp độc lập.

 

(Còn nữa)

 

Aggregation and Composition

Bình luận
Gửi bình luận
Bình luận