Thực hành implement Clean Architecture

Trong bài trước chúng ta đã tìm hiểu lý thuyết về Clean Architecture của Uncle Bob. Như đã đề cập, các mục tiêu cốt lõi của Clean Architecture cũng giống với đối với Ports & Adapters (Hexagonal)Onion Architectures, hướng đến:

  • Độc lập các công cụ;
  • Sự độc lập của các cơ chế phân phối;
  • Khả năng cô lập kiểm thử.

cleanarchitecture-5c6d7ec787d447a81b708b73abba1680
Robert C. Martin 2012, The Clean Architecture

Trong bài viết này, chúng ta sẽ hiện thực phần implement cho kiến trúc này dựa trên Component diagram mà bác Bob đã mô tả như hình dưới đây:

cleanarchitecturedesign

Phần ứng dụng mà chúng ta làm liên quan đến Banking, cụ thể là 2 chức năng withdraw deposit. Trong một hệ thống thật, có lẽ 2 chức năng này sẽ nằm ở 2 module (2 ngữ cảnh cô lập – bounded context) khác nhau, nhưng để đơn giản, mình gom chúng thành một ngữ cảnh duy nhất gọi là BankAccount. Hãy cùng xem qua structure của project

1

Đầu tiên chúng ta sẽ xây dựng phần core ứng dụng (domain). Class BankAccount sẽ chứa thông tin về 1 account ngân hàng bao gồm account number, balance, BankAccount có 2 function chính là withdrawdeposit. Lưu ý rằng chúng ta cũng có thể thêm 1 layer là domain service để orchestrate các lớp domain nhằm tăng tính high cohesion cho domain.

public abstract class Entity {

	private int id;

	public Entity() {
	}

	public int getId() {
		return id;
	}

	public void setId(int id) {
		this.id = id;
	}

}

public class BankAccount extends Entity {

	private String number;

	private BigDecimal balance;

	public BankAccount() {
		balance = BigDecimal.ZERO;
	}

	public String getNumber() {
		return number;
	}

	public void setNumber(String number) {
		this.number = number;
	}

	public BigDecimal getBalance() {
		return balance;
	}

	public void setBalance(BigDecimal balance) {
		this.balance = balance;
	}

	public boolean withdraw(BigDecimal amount) {
		if(amount.compareTo(BigDecimal.ZERO)  0) {
			return false;
		}
		balance = balance.subtract(amount);
		return true;
	}

	public boolean deposit(BigDecimal amount) {
		if(amount.compareTo(BigDecimal.ZERO) <= 0) {
			return false;
		}
		balance = balance.add(amount);
		return true;
	}

}

Tiếp theo là phần Usecase hoặc Application Service hay theo cách gọi của Bob là Boundary. Là nơi ứng dụng sẽ expose các chức năng của ứng dụng với bên ngoài. Ta có 2 use case là withdraw và deposit trong interface BankAccountBoundary. Chúng ta sẽ public interface này ra ngoài, còn phần implement của nó thì sẽ được hide bên trong ứng dụng.

/*
 * Outer component (like MVC Controller) only know and work this boundary (with two use-cases, currently)
 *
 */
public interface BankAccountBoundary {

	public BankAccountWithdrawResponseModel wihdraw(BankAccountWithdrawRequestModel request);

	public BankAccountDepositResponseModel deposit(BankAccountDepositRequestModel request);

}

class BankAccountBoundaryImpl implements BankAccountBoundary {

	/*
	 * This is a gateway to account database
	 */
	private BankAccountGateway accountGateway;

	public BankAccountBoundaryImpl() {
		accountGateway = DependencyResolver.getBankAccountGateway();
	}

	@Override
	public BankAccountWithdrawResponseModel wihdraw(BankAccountWithdrawRequestModel request) {

		BankAccount account = accountGateway.getByNumber(request.getAccountNumber());

		boolean withdrawResult = account.withdraw(request.getAmount());

		BankAccountWithdrawResponseModel response = new BankAccountWithdrawResponseModel();

		if(withdrawResult) {
			accountGateway.save(account);
			response.setResult("Withdraw Successfully");
		}
		else {
			response.setResult("Insufficient amount");
		}

		return response;
	}

	@Override
	public BankAccountDepositResponseModel deposit(BankAccountDepositRequestModel request) {
		// ...
	}

}

Phần gateway là nơi ứng dụng giao tiếp với các service khác như Database, mail, SMS,… Tất cả chúng đều ở dạng interface, ví dụ như BankAccountGateway, phần implement của nó sẽ nằm ở layer bên ngoài. Ví dụ như BankAccountInMemoryDB, là lớp hiện thực interface BankAccountGateway nhằm cung cấp khả năng lưu trữ dữ liệu trên memory. Điều này có lợi khi chúng ta cần develop ứng dụng nhanh mà không cần setup DB, …

public interface EntityGateway  {

	public void save(T entity);

	public T getById(int id);

}

public interface BankAccountGateway extends EntityGateway {

	public BankAccount getByNumber(String number);

}

/*
 * This is a fake database.
 * There is a advantage here, I don't need to set up a real SQL Server DB to start develop.
 * Just start with an on-memory DB is enough
 *
 */
public class BankAccountInMemoryDB implements BankAccountGateway {

	private static Map accountDB = new HashMap();

	static {
		{
			BankAccount account = new BankAccount();
			account.setId(1);
			account.setNumber("001");
			account.setBalance(BigDecimal.valueOf(100000));
			accountDB.put(1, account);
		}
		{
			BankAccount account = new BankAccount();
			account.setId(2);
			account.setNumber("002");
			account.setBalance(BigDecimal.valueOf(500000));
			accountDB.put(2, account);
		}
		{
			BankAccount account = new BankAccount();
			account.setId(3);
			account.setNumber("003");
			account.setBalance(BigDecimal.valueOf(700000));
			accountDB.put(3, account);
		}
	}

	@Override
	public void save(BankAccount entity) {
		System.out.println("Save BankAccount with number: " + entity.getNumber() + ", balance = " + entity.getBalance() + " to SQL DB");
		accountDB.put(entity.getId(), entity);
	}

	@Override
	public BankAccount getById(int id) {
		System.out.println("Find BankAccount by id: " + id);
		return accountDB.get(id);
	}

	@Override
	public BankAccount getByNumber(String number) {
		Iterator iterator = accountDB.entrySet().iterator();
		while (iterator.hasNext()) {
			Map.Entry entry = iterator.next();
			if(entry.getValue().getNumber().equals(number)) {
				return entry.getValue();
			}
		}
		return null;
	}

}

Phần UI nằm ở trong package mvc. Ở đây mình giả lập MVC/MVVM pattern với các class như Controller, Presenter. Chúng ta cũng có thể thêm class ViewModel nếu muốn, nhưng ở đây để đơn giản hóa, mình sẽ không thêm vào. Thay vào đó, lớp BankAccountPresenter sẽ implement interface java.util.function.Consumer để accept response từ Controller và output ra Console View.

public class BankAccountController {

	private BankAccountBoundary bankAccountBoundary;
	private BankAccountPresenter presenter;

	public BankAccountController(final BankAccountPresenter presenter) {

		this.presenter = presenter;

		this.bankAccountBoundary = DependencyResolver.getBankAccountBoundary();
	}

	public void withdraw(String acountNumber, BigDecimal amount) {
		// Create a request model
		BankAccountWithdrawRequestModel request = new BankAccountWithdrawRequestModel();
		request.setAccountNumber(acountNumber);
		request.setAmount(amount);
		// Delegate the request to boundary (or use-case) object
		BankAccountWithdrawResponseModel response = bankAccountBoundary.wihdraw(request);
		// Start data binding
		presenter.accept(response);
	}

}

public class BankAccountPresenter implements ResponseModelConsummer {

	@Override
	public void accept(BankAccountWithdrawResponseModel response) {
		String result = response.getResult();
		// Format then pass the result to View (Web page)
		System.out.println(String.format("Output to View: %s", result));
	}

}

Và cuối cùng là hàm main để run chương trình 🙂

/*
 * This is the main entry of our application
 * I simulate withdraw action that user performed on GUI
 *
 * */
public class BankAccountMainEntry {

	public static void main(String[] args) {

		// Get request from UI
		RequrestParameter requestParam = RequestHandler.getRequestParam();

		// presenter is responsible for data converting and display onto GUI
		BankAccountPresenter presenter = new BankAccountPresenter();

		// controller is class to handle the request
		BankAccountController controller = new BankAccountController(presenter);

		// Perform withdraw action
		controller.withdraw(requestParam.get("accountNumber"), new BigDecimal(requestParam.get("amount")));

	}

}

Đó là tất cả, tất nhiên chúng chỉ mang tính concept minh họa kiểu hello world 🙂, nhưng hi vọng, với những kiến thức nền tảng, các bạn có thể tự xây dựng cho mình một system “Clean”, không chỉ áp dụng mỗi Clean Architecture mà phải biết kết hợp rất nhiều khái niệm khác nữa (DDD chẳng hạn).

Đây là bài viết trong loạt bài viết về “Tổng quan về sự phát triển của kiến trúc phần mềm“. Đây là loạt bài viết chủ yếu giới thiệu về một số mô hình kiến trúc phần mềm hay nói đúng hơn là sự phát triển của chúng qua từng giai đoạn, qua đó giúp chúng ta có cái nhìn tổng quát, up-to-date và là roadmap để bắt đầu hành trình chinh phục (đào sâu) thế giới của những bản thiết kế với vai trò là những kỹ sư và kiến trúc sư phần mềm đam mê với nghề.

Tổng hợp bởi edwardthienhoang

Thực hành đo lường các chỉ số (metric) của software với NDepend

Ngày nay, có rất nhiều vấn đề chúng ta cần suy nghĩ đến khi xây dựng ứng dụng thực tế. MAINTAINABILITY (tính dễ bảo trì), UNDERSTANDABILITY (tính dễ hiểu), DEPENDENCY (tính phụ thuộc giữa các module), COHESION (tính gắn kết) chỉ là 1 vài trong số chúng. Chúng ta phải làm việc cật lực để duy trì chất lượng (quality) của ứng dụng, làm cho mã nguồn có khả năng tự diễn đạt ý nghĩa của chúng (self-comment), tránh các lỗi phụ thuộc vòng tròn (CYCLIC DEPENDENCIES) giữa các module. Có rất nhiều tool có thể giúp chúng ta đạt được các mục tiêu đó ví dụ như CheckStyle, FindBug, JTest, Sonar, Resharper, JustCode, NDepend, JDepend. Trong bài viết này, mình sẽ lấy ví dụ với NDepend, một công cụ giúp đo lường chất lượng mã nguồn trong .NET, bài viết được tham khảo từ Future Processing, nhằm mục đích thực hành và hiểu các thông số (metric) của 1 ứng dụng đã đề cập trong bài viết trước đó “Đo lường kết cấu của kiến trúc“. Mã nguồn ví dụ được dùng trong bài viết này là một open-source project có tên GALLIO, một TEST AUTOMATION PLATFORM và MBUNIT và UNIT TESTING FRAMEWORK. Bạn có thể down load nó ở đây để cùng thực hành với mình.

NDepend là gì?

NDepend là một công cụ phân tích tĩnh (static analysis) cho các dự án .NET. Nó hỗ trợ một số lượng lớn các chỉ số (metric) và có thể hình dung các phụ thuộc bằng cách sử dụng đồ thị trực tiếp và ma trận phụ thuộc. NDepend cũng có thể thực hiện codebase snapshots để so sánh về sau. Một trong những tính năng chính của NDepend là người dùng có thể viết các quy tắc của riêng mình bằng các truy vấn LINQ (CQLinq). Ngoài ra còn có rất nhiều quy tắc mã CQLinq được xác định trước. NDepend có thể dễ dàng tích hợp với Visual Studio. Hơn thế nữa, với NDepend API và Power Tools, mọi người đều có thể tự viết những chương trình phân tích tĩnh (static analyser) hoặc tinh chỉnh các nguồn mở Power Tools hiện tại.

Phân tích mã nguồn của Gallio

Để phân tích một dự án trong NDepend, chúng ta phải tạo mới một project phân tích, và thêm vào tệp solution cũng như tất cả các assemblies cần được phân tích. Sau khi hoàn thành phân tích, bạn có thể tìm thấy bản tóm tắt các số liệu quan trọng trên bảng điều khiển (dashboard).

BASIC CODE METRICS ON DASHBOARD

Trên Dashboard (Hình 1) chúng ta có thể tìm thấy tóm tắt liên quan đến các chỉ số cơ bản cho dự án được phân tích:

LINES OF CODE

Gallio có 72121 Dòng Mã (LOC) và chỉ có 4609 LOC được tự động generated. Để so sánh, các dự án NUnit bao gồm 33460 LOC, vì vậy, chúng ta có thể nói Gallio có kích thước lớn hơn NUnit.

SỐ ƯỢNG TYPES, NAMESPACES, METHODS, SOURCE FILES VÀ ASSEMBLIES

Gallio chứa 5124 TYPES, 89 ASSEMBLIES, 409 NAMESPACES, 22185 METHODS, 4261 FIELDS và 2156 SOURCE FILES. Một số lượng lớn các method trong một dự án là một chút đáng báo động vì chỉ có 5124 kiểu dữ liệu và chúng ta nên kiểm soát sự cân bằng giữa chúng.

THIRD-PART USAGE (ASSEMBLIES, NAMESPACES, TYPES, METHODS AND FIELDS)

Gallio sử dụng 47 ASSEMBLIES, 135 NAMESPACES, 1118 TYPES, 3044 METHODS và 204 FIELDS từ các thư viện thứ ba. Tất cả những số liệu thống kê này là một phần của thông tin cho biết mức độ mà một dự án phụ thuộc vào thư viện bên ngoài.

CODE COVERAGE BY TESTS

Chúng ta không nhận được bất kỳ kết quả nào về Code coverage vì không có bất bài test nào cho dự án đó.

CODE COVERAGE BY COMMENTS

Một số assemblies trong Gallio được viết bằng một ngôn ngữ khác với C #. Trong trường hợp đó, NDepend không thể đo được tỷ lệ comment.

METHOD COMPLEXITY (IL)

Độ phức tạp trung bình của các method đối với Gallio là 1,93 nhỏ hơn giá trị được gợi ý, nghĩa là 2. Độ phức tạp (Cyclomatic complexity – CC) cao nhất là 88 và kết quả cao cho thấy rằng method đó nên được xem xét và refactored nếu có thể.

STATISTICS ON THE VIOLATED CODE RULES AND NUMBER OF VIOLATIONS FOUND IN A PROJECT

Khi nhìn vào trường Code Rules, chúng ta có thể thấy rằng có khoảng 31700 vi phạm đối với 92 quy tắc và 1207 vi phạm nghiêm trọng (critical violations) đối với 13 quy tắc. Những con số này là một chút đáng báo động và do đó chúng ta nên xem xét kỹ hơn những vấn đề này.

1-1024x552
Hình 1

Các biểu đồ trình bày bằng đồ hoạ ở trên cho thấy các chỉ số nói trên cao, trong đó chúng ta có thể thấy xu hướng và thay đổi các chỉ số theo thời gian. Điều đó có thể hữu ích vì chúng ta sẽ thấy liệu quá trình phát triển hoặc các hành động tái cấu trúc có đi đúng hướng hay không.

CQLINQ QUERIES AND PREDEFINED CODE RULES

Trước khi chúng ta xem xét các kết quả mà chúng ta nhận được cho Gallio, tôi muốn nói thêm một điều về định nghĩa các vi phạm trong NDepend. CQLinq là một ngôn ngữ truy vấn cho phép tìm bất kỳ vi phạm nào trong mã mà người dùng có thể tưởng tượng. CQLinq, như tên cho thấy, dựa trên LINQ và một truy vấn đơn giản như thể hiện trong hình 2.

2
Hình 2

Truy vấn này quét toàn bộ dự án và liệt kê các “Potentially dead fields” – không được sử dụng bởi bất kỳ method hoặc class khác. Quy tắc Potentially dead fields được đánh dấu là một điểm quan trọng vì kết quả hiển thị mã không sử dụng trong một dự án nên được xóa hoặc thay đổi.

Có hai quy tắc được xác định trước trong NDepend và chúng có thể được tìm thấy trong tab “Query and Rules Explorer” (Hình 3). Tất cả các vi phạm được nhóm và dán nhãn về mức độ nghiêm trọng.

3
Hình 3

Từ các quy tắc được xác định trước, những điều quan trọng nhất có thể được tìm thấy trong các nhóm sau:

  • Code Quality
  • Object Oriented Design
  • Architecture and Layering
  • Dead Code
  • Visibility
  • Purity – Immutability – Side Effects
  • Name Conventions

Có thể tìm thấy bản tóm tắt về vi phạm trong Gallio trên Bảng điều khiển (Hình 1). Như tôi đã đề cập, đã có khoảng 31700 vi phạm đối với 92 quy tắc và 1207 vi phạm nghiêm trọng đối với 13 quy tắc được tìm thấy tại Gallio. Trong bước đầu tiên, chúng ta nên xem xét các vi phạm quan trọng.

CRITICAL VIOLATIONS IN GALLIO

Khi chúng ta xem xét các vi phạm mà chúng ta nhận được đối với mỗi nhóm, có thể thấy rằng có rất nhiều đoạn mã nguy hiểm tiềm ẩn. Có 819 method và 143 type không được sử dụng (Dead Code), do đó chúng ta phải xem xét kỹ hơn những con số cao này. Khi phân tích các vi phạm khác, con số trong các nhóm khác không cao như Dead Code. Trong nhóm Architecture and Layering, chúng ta có thể tìm thấy Tránh không gian truy vấn phụ thuộc lẫn nhau với 41 lỗi và Avoid namespaces dependency cycles với 8. Chúng ta nên tránh tình huống khi hai namespaces phụ thuộc lẫn nhau hoặc thậm chí có phụ thuộc vòng. Theo nhiều nguồn, các namespaces phụ thuộc lẫn nhau dẫn đến cái được gọi là mã spaghetti và cho biết các class đã được nhóm lại không tổ chức chúng một cách nghiêm ngặt từ trên xuống dưới. Cách hay để phân tích những vấn đề đó là sử dụng Dependency Graph.

DEPENDENCY GRAPH

Trong Dependency Graph, chúng ta có thể thấy các namespaces hoặc các assemblies được biểu diễn bởi các hình chữ nhật và các kết nối giữa chúng. Kích thước của hình chữ nhật có thể được đặt theo kích thước LOC của namespaces, hoặc assemblies, sự phức tạp của chúng hoặc nhiều số liệu khác. Độ dày của các mũi tên có thể được đặt liên quan đến số fields, methods, types and namespaces. Chúng ta có thể dễ dàng phân tích phụ thuộc giữa các namespaces và assemblies cũng như để tìm phụ thuộc lẫn nhau và các cyclic dependency nếu có.
Trong Hình 3, có hai cặp namespaces của Gallio phụ thuộc lẫn nhau (các hình chữ nhật được kết nối bằng mũi tên song phương). Tuy nhiên, đối với một số namespaces phụ thuộc lẫn nhau đã được tìm thấy trong Gallio, Dependency Graph sẽ hơi khó nhìn. Trong trường hợp đó tốt hơn là sử dụng Dependency Matrix.

4
Hình 4

Trong Dependency Graph, khi chúng ta chỉ chọn các application assemblies, chúng ta sẽ nhận được một thông tin ngắn về tất cả các số liệu đã được tính toán:

  • lines of code
  • IL instructions
  • lines of comments
  • number of methods, fields, types and namespaces
  • rational cohesion
  • afferent coupling
  • efferent coupling

Khi chúng ta xem xét các kết quả cho từng số liệu và so sánh chúng với các giá trị được đề xuất từ tài liệu, chúng ta có thể tìm ra assembly nào có thể là vấn đề và chúng ta nên xem xét thêm ở đâu.

DEPENDENCY MATRIX

Trong Dependency Matrix, chúng ta có thể quan sát các phụ thuộc giữa các namespaces hoặc assemblies được biểu diễn bằng các dòng và cột. Ngoài ra, số trong các ô phản ánh số lượng phụ thuộc giữa chúng với các đặc tính đã chọn (types, methods, fields). Lý tưởng hơn, nếu các ô màu xanh trong ma trận nằm dưới đường chéo và màu xanh lá cây trên đường chéo. Các namespaces giữa một dependency cycle đã được tìm thấy được đánh dấu bằng một hình vuông có đường viền màu đỏ trong Dependency Matrix (Hình 5). Hơn nữa, trong trường hợp này chúng ta quan sát thấy các ô xanh và xanh nằm trong các đường biên trên và dưới đường chéo của ma trận.

6
Hình 5

Đối với một số dependency cycle, chúng ta có thể quan sát thấy rằng trong Dependency Matrix tất cả các ô trong hình vuông có màu đỏ là màu đen. Điều đó chỉ xảy ra khi phụ thuộc trực tiếp và gián tiếp giữa các namespaces.

6a
Hình 6

SRP (SINGLE RESPONSIBILITY PRINCIPLE)

Theo SRP, class không nên có nhiều lý do để thay đổi. Khi chúng ta đi đến một mức độ thấp hơn, chúng ta có thể nói liệu một phần tử mã sử dụng hàng chục các yếu tố khác (ở cùng cấp độ) như thể đó là trường hợp, nó có quá nhiều trách nhiệm. Chúng ta có thể quan sát phần tử mã này trong Dependency Matrix nếu nó chứa nhiều ô màu xanh trong một cột và nhiều ô xanh trong một đường thẳng. Khi chúng ta nhìn vào các kết quả cho Gallio (Hình 7) chúng ta không thể thấy được tình huống đó, vì vậy chúng ta có thể chắc chắn rằng SRP trong dự án đó không bị hỏng.

7
Hình 7

COHERENT ASSEMBLIES

Quy tắc thứ hai nói rằng các assemblies trong một dự án nên chặt chẽ (coherent). Component nên thực hiện một chức năng hợp lý duy nhất hoặc một logical entity duy nhất và tất cả các phần cần đóng góp cho việc thực hiện. Low cohesion có nghĩa là component này thực hiện rất nhiều hành động và không tập trung vào những gì nó nên làm. Ngược lại với điều đó là high cohesion, có nghĩa là thành phần này tập trung nhiều vào việc cần làm và tất cả các lớp trong thành phần đó có nhiều điểm chung. Trong khi nói về high cohesion, bạn nên nói đến coupling. Nó đề cập đến mối quan hệ giữa hai component và sự phụ thuộc lẫn nhau giữa chúng. Low coupling có nghĩa là thay đổi một cái gì đó trong một component không nên ảnh hưởng đến component khác; trong khi high coupling có nghĩa là sẽ khó khăn trong việc thay đổi mã bởi vì nó có thể có nghĩa là một toàn bộ hệ thống bị ảnh hưởng. Do đó, phần mềm với thiết kế tốt sẽ có high cohesion và low coupling.

Mức độ liên kết (cohesion) của các assemblies có thể được quan sát thấy trong Dependency Matrix. Các assemblies có độ gắn kết cao (high cohesion) khi các ô màu xanh lá cây và xanh được nhóm thành các ô vuông. Khi chúng ta nhìn vào các kết quả cho Gallio (Hình 7) chúng ta không thể thấy bất kỳ tế bào xanh lá và xanh dương nào được nhóm lại trong một hình vuông xung quanh đường chéo, có nghĩa là các assemblies trong Gallio có độ kết dính thấp (low cohesion). Kiến trúc và thiết kế các thành phần nên được xem xét vì trong tương lai có thể khó hiểu mục đích của một số component và duy trì mã. Để so sánh, khi chúng ta nhìn vào các kết quả mà chúng ta nhận được cho dự án NUnit (Hình 8), chúng ta có thể thấy rằng các assemblies có mức gắn kết cao hơn.

8
Hình 8

ABSTRACTNESS VS. INSTABILITY DIAGRAM

Mỗi assembly có các số liệu tính toán riêng của mình: Abstractness, Instability và Distance from main sequence. Những số liệu này được hình dung trong sơ đồ “Abstractness vs. Instability”. Nó giúp phát hiện những bộ phận nào có thể gây “đau thương” để duy trì (concrete and stable) và những bộ phận nào trong số chúng có tiềm năng vô dụng (abstract and instable). Khi chúng ta nhìn vào các cụm từ Gallio (Hình 9), chúng ta có thể thấy phần lớn chúng nằm trong khu vực “xanh lá”, chỉ có một assembly nằm trong vùng vô ích và ba nằm trong vùng đau thương. Một điều có thể là đáng báo động là hầu hết các assembly đều có hệ số bất ổn định (I) khoảng 1, có nghĩa là chúng có giá trị gia tăng của mối nối cực đại. Do đó, phần lớn các assembly phụ thuộc vào các assembly khác và trong tương lai chúng ta có thể gặp phải các vấn đề với việc thay đổi các assembly như một thay đổi sẽ buộc các assembly khác còn lại thay đổi.

9
Hình 9

Kết luận

Để tổng hợp các kết quả phân tích trong NDepend, nói chung có một vài điều trong mã mà chúng ta nên xem xét kỹ hơn. Các chu kỳ phụ thuộc giữa các assemblies đã được tìm thấy và một vài trong số chúng phụ thuộc lẫn nhau. Nó có nghĩa là một số thay đổi trong kiến ​​trúc dự án nên được thực hiện để tránh những phụ thuộc. Thứ hai, có vẻ như có rất nhiều mã không sử dụng trong Gallio và MbUnit unit testing framework. Thống kê vi phạm cho thấy 819 method không sử dụng, đó là một con số cao. Do đó, mã nên được xem xét kỹ lưỡng và tất cả các method cũ, không sử dụng, kiểu dữ liệu (type) và các field nên được xóa. Nó sẽ làm giảm các dòng mã và làm cho nó rõ ràng hơn. Một điều khác cần được tính đến là sự gắn kết thấp của các assemblies trong dự án. Nó chỉ ra rằng họ có thể có nhiều hơn một chức năng hợp lý duy nhất và nó có thể là khó hiểu được mục đích của một số assemblies. Các component nên được xem xét lại và phải đặt biên giới (boundary) cho các chức năng, giúp tăng sự gắn kết. Điều cuối cùng cần được đề cập là thực tế là các unit tests nên được tạo ra cho dự án đó vì chúng giúp tìm ra các khiếm khuyết ở giai đoạn đầu của quá trình phát triển.

Đây là bài viết trong loạt bài viết về “Tổng quan về sự phát triển của kiến trúc phần mềm“. Đây là loạt bài viết chủ yếu giới thiệu về một số mô hình kiến trúc phần mềm hay nói đúng hơn là sự phát triển của chúng qua từng giai đoạn, qua đó giúp chúng ta có cái nhìn tổng quát, up-to-date và là roadmap để bắt đầu hành trình chinh phục (đào sâu) thế giới của những bản thiết kế với vai trò là những kỹ sư và kiến trúc sư phần mềm đam mê với nghề.

Bài viết được tham khảo từ:

https://www.future-processing.pl/blog/analysing-the-quality-of-code-with-ndepend/

 

Tổng hợp bởi edwardthienhoang

Command Query Responsibility Segregation (CQRS)

Với một ứng dụng kiểu data-centric application (ứng dụng tập trung vào dữ liệu) chỉ implement các thao tác CRUD căn bản xuống DB và để business process cho user xử lý (ví dụ như cần change data nào và khi nào change), nó có lợi điểm là user có thể thay đổi business process mà application không cần phải thay đổi. Nói cách khác, nó ngụ ý rằng tất cả user đều có hiểu biết rõ ràng về business process mà họ có thể làm, nhưng vấn đề là phần lớn user đều không thể nắm hết được business process mà họ cần phải thao tác.

Trong data-centric application, application không có kiến thức về business process, vì vậy domain sẽ không có bất cứ 1 “động từ” (hành động) nào, và không thể làm gì khác ngoài việc đọc/ghi raw data. Nó trở thành một sự trừu tượng hóa của mô hình dữ liệu (a glorified abstraction of the data model). Process (quy trình) chỉ tồn tại trong đầu của người sử dụng application, hoặc thậm chí là trong các sticky note dán trên màn hình máy tính.

Một ứng dụng thực sự hữu ích khi nó có thể loại bỏ gánh nặng “process” trên vai người dùng bằng cách nắm bắt ý định của họ, làm cho nó trở thành một ứng dụng có khả năng xử lý hành vi (process) chứ không chỉ đơn giản là lưu trữ dữ liệu.

Command Query Separation

Thông thường, chúng ta bắt đầu bằng một cái gì đó như sau:

1_layered
Diagram of classic N-Layer architecture.

Đây là kiến trúc N-layer điển hình như tất cả chúng ta đều biết. Nếu chúng ta muốn thêm một số CQS ở đây, chúng ta có thể “đơn giản” tách business logic ra thành Commands và Queries

CQRS-Simple-Architecture_2_CQS_1

Separated Commands and Queries with shared domain model

Theo Martin Fowler, thuật ngữ ‘command query separation’ được đặt ra bởi Bertrand Meyer trong cuốn sách của ông “Object Oriented Software Construction” (1988) – một cuốn sách được cho là một trong những cuốn sách OO có ảnh hưởng nhất, trong những ngày đầu của OO.

Meyer nói rằng, chúng ta không nên có các method mà vừa thay đổi data và trả về data. Vì vậy, chúng ta có hai loại method:

  • Queries: Trả về data nhưng không thay đổi data, do đó không gây ra các side effects;
  • Commands: Thay đổi data và không trả về data.

Nói cách khác, đặt một câu hỏi không nên thay đổi câu trả lời và làm cái gì đó không nên trả lại câu trả lời, tuân theo Nguyên tắc Single Responsibility Principle.

Tuy vậy, cũng có 1 số pattern ngoại lệ với nguyên tắc này, ví dụ với implementation của Queue và Stack, thao tác “pop” vừa đồng thời thay đổi data và trả về data.

Command Pattern

Ý tưởng chính của Command Pattern là chuyển chúng ta từ data-centric application sang process-centric application (ứng dụng tập trung vào quy trình), application sẽ có cả kiến thức về domain và processes.

Trong thực tế, điều này có nghĩa là thay vì có người dùng thực hiện hành động “CreateUser”, theo sau là một hành động “ActivateUser” và một hành động “SendUserCreatedEmail”, chúng ta sẽ yêu cầu người dùng thực hiện lệnh “RegisterUser” , application sẽ xử lý bao gồm cả ba hành động như một business process khép kín.

Tuy nhiên, bạn nên nhớ rằng điều này không có nghĩa là không thể có command “CreateUser” đơn giản. Các trường hợp sử dụng CRUD có thể cùng tồn tại với các trường hợp sử dụng có ý đồ đại diện cho một business process phức tạp, nhưng điều quan trọng là không nhầm lẫn chúng.

Giống như tên gọi của nó, trong pattern này chúng ta sẽ phân chia việc thực hiện các commands khác nhau. Hãy xem Wikipedia nói gì về pattern này

Command pattern là một behavioral design pattern trong đó một đối tượng được sử dụng để đại diện và đóng gói tất cả các thông tin cần thiết để gọi một method. Thông tin này bao gồm tên method, các đối tượng của method đó và các giá trị cho các tham số của method..

Ví dụ, tất cả các Command sẽ có cùng một phương thức execute() để tại một thời điểm nào đó, bất cứ Command nào cũng có thể được kích hoạt mà không cần biết nó là Command gì. Điều này sẽ cho phép các Command được xếp hàng đợi và được thực thi khi có thể, có thể đồng bộ (sync) hoặc bất đồng bộ (async).

Command Bus

Việc mô hình hoá các nghiệp vụ ghi dữ liệu dưới các command cho phép che đậy tốt các logic nghiệp vụ, giúp việc mở rộng dễ dàng hơn. Đồng thời các comand đó có thể dễ dàng chuyển đổi giữa xử lý đồng bộ và bất đồng bộ thông qua lớp abstract là command bus mà không thay đổi mô hình. Giúp cung cấp một mô hình nhất quán, xuyên suốt trong bộ kiến trúc. Phần ghi dữ liệu được thực hiện qua việc send các command tới các handler thông qua command bus. Comand hanlder đóng vai trò tương tự domain service sẽ tương tác với các model để thực hiện các nghiệp vụ thay đổi dữ liệu.

Hơn nữa, chúng ta có thể implement theo dạng nhiều command không cần phải được xử lý ngay lập tức, chúng có thể được xếp queue và thực hiện bất đồng bộ. Điều này có một số lợi thế làm cho hệ thống mạnh mẽ hơn:

  • Phản hồi cho người dùng được gửi lại nhanh hơn bởi vì chúng ta không xử lý command ngay lập tức;
  • Nếu vì lỗi hệ thống, giống như một lỗi hoặc DB đang offline, một command không thành công, người dùng thậm chí không nhận ra nó. Command này chỉ đơn giản có thể được execute khi vấn đề được giải quyết.

Ngoài ra, việc sử dụng command bus còn mang lại một số lợi điểm như chúng ta có thể áp dụng thêm Aspect-oriented programming (AOP) có thể wrap thêm các common logic trước và / hoặc sau khi xử lý được thực hiện. Ví dụ, chúng ta có thể validate dữ liệu command trước khi chuyển nó tới handler, wrap các handler trong transaction logic (commit, rollback) khi làm việc với DB transaction, hoặc chúng ta có thể làm cho command bus support việc truy vấn, phân luồng các logic phức tạp và thực thi bất đồng bộ.

Cách thông thường mà command bus đạt được là sử dụng Decorator pattern wrap xung quanh command bus (một Decorator object có thể decorate cho một Decorator object khác), giống như trò matryoshka.

commandbusmatryoshka

Điều này cho phép chúng ta tạo ra các Decorators riêng của chúng ta và để cấu hình cho command bus (có thể bên thứ ba) được tạo ra bởi bất kỳ Decorator nào, bất kể thứ tự nào, thêm chức năng tuỳ chỉnh của chúng ta vào command bus. Nếu chúng ta cần quản lý command theo hàng đợi, chúng ta thêm một Decorator để quản lý hàng đợi (queue) của các command. Nếu không sử dụng các transaction DB thì chúng ta không cần transaction management Decorator làm gì, …

Command Query Responsibility Segregation

Bằng cách kết hợp các khái niệm về CQS, Command, Query và CommandBus, cuối cùng chúng ta đã đạt tới Command Query Responsibility Segregation (CQRS). Về cơ bản, chúng ta có thể nói rằng CQRS là một implementation của nguyên tắc Command Query Separation principle trong kiến trúc phần mềm. CQRS có thể được thực hiện theo những cách khác nhau và lên đến các cấp độ khác nhau, có thể chỉ có phía Command, hoặc có thể không sử dụng một Command Bus. Để hoàn thành, đây là một sơ đồ đại diện cho cách tôi nhìn thấy implementation của một CQRS đầy đủ:

2006-1-cqrs

Query side

Nếu làm theo CQS, phía truy vấn sẽ chỉ trả lại data và không thay đổi data. Vì không có ý định thực hiện business process trên data đó nên chúng ta không cần các business objects (ví dụ như các entities), vì vậy không cần một ORM trung gian. Chúng ta chỉ cần truy vấn dữ liệu thô để hiển thị cho người dùng và chính xác dữ liệu mà chúng ta cần để hiển thị đến người dùng!

Phần đọc dữ liệu được thiết kế riêng không lệ thuộc vào các model của phần ghi dữ liệu. Do đó có thể linh hoạt trong việc truy xuất database, cũng như sử dụng các data source khác nhau để tối ưu về tốc độ truy xuất.

Đây là lợi ích về hiệu suất ngay tại đây: Khi truy vấn dữ liệu, chúng ta không cần trải qua các business logic layers, chúng ta chỉ làm và nhận được chính xác những gì chúng ta cần.

Do sự tách biệt này, một cách tối ưu khác có thể là để tách biệt hoàn toàn bộ nhớ dữ liệu thành hai kho dữ liệu tách biệt: WRITE DB được tối ưu hóa cho ghi data và READ DB để tối ưu hóa cho việc đọc data. Ví dụ, nếu chúng ta đang sử dụng một RDBMS:

Việc đọc data không cần đến tính data integrity, chúng không cần các ràng buộc khoá ngoại bởi vì việc xác thực tính toàn vẹn dữ liệu được thực hiện khi ghi vào WRITE DB. Vì vậy, chúng ta có thể loại bỏ các ràng buộc toàn vẹn dữ liệu ở READ DB.

Chúng ta cũng có thể sử dụng các DB View với chính xác dữ liệu mà chúng ta cần, làm cho việc truy vấn trở nên đơn giản và do đó nhanh hơn, và do đó, tại sao chúng ta cần một RDBMS cho việc đọc dữ liệu ?! Chúng ta có thể sử dụng Mongo DB hoặc Redis, nhanh hơn. Việc thay đổi này có ích nếu ứng dụng đang có vấn đề hiệu suất về việc đọc data.

Việc truy vấn có thể được thực hiện bằng cách sử dụng một đối tượng truy vấn trả về một mảng dữ liệu mong muốn hoặc chúng ta có thể sử dụng một cái gì đó tinh vi hơn như Query Bus, ví dụ, nhận một query name, sử dụng một đối tượng truy vấn để truy vấn dữ liệu và trả về một thể hiện của ViewModel mà query cần.

Command side

Như đã giải thích trước đó, bằng việc sử dụng command, chúng ta dịch chuyển thiết kế của application từ data-centric design sang behaviour design sử dụng Domain Driven Design.

Bằng việc loại bỏ các tác vụ READ ra khỏi code xử lý command và domain, có thể giải quyết các vấn đề sau:

  • Domain objects sẽ không cần phải lộ ra (expose) các trạng thái internal của nó
  • Các Repositories sẽ chỉ còn rất ít (nếu có) các tác vụ truy vấn dữ liệu
  • Tập trung vào behaviour tương tác giữa các Aggregate boundaries (hiểu nôm na là các đối tượng boundary trong EIB) hơn

Các phụ thuộc “one-to-many” và “many-to-many” giữa các entities có thể gây ra ảnh hưởng xấu về mặt performance trong ORM. Tin tốt là chúng ta hiếm khi cần các mối quan hệ đó khi xử lý các command, chúng chủ yếu được sử dụng để truy vấn và chúng ta vừa chuyển truy vấn ra khỏi để chúng ta có thể loại bỏ các mối quan hệ thực thể đó. Tôi không nói ở đây về mối quan hệ giữa các bảng trong một RDBMS, những ràng buộc khoái ngoại nên vẫn tồn tại trong DB, tôi đang nói về các kết nối giữa các thực thể được cấu hình ở cấp độ ORM. Ví dụ, liệu chúng ta có thực sự cần một danh sách đơn đặt hàng khi truy vấn một thực thể khách hàng? Xử lý nào chúng ta cần phải sử dụng danh sách đơn hàng đó?

Giống như phía truy vấn, nếu Command không được sử dụng cho các truy vấn phức tạp, chúng ta có thể thay thế RDBMS với một cách lưu trữ như document hoặc key-value? Tất nhiên điều đó còn phụ thuộc vào nếu ứng dụng đang có vấn đề hiệu suất về ghi dữ liệu.

Business process events

Sau khi Command được xử lý, và nếu nó đã được xử lý thành công, trình xử lý sẽ kích hoạt một event thông báo cho phần còn lại của ứng dụng về những gì đã xảy ra. Các event nên được đặt tên như là Command kích hoạt nó, và một điều nữa như là quy luật với các event, nó phải là sử dụng thì quá khứ, ví dụ actionPerformed.

CQRS != EVENT SOURCING

Event Sourcing là một ý tưởng đã được trình bày cùng với CQRS, và thường được xác định là một phần của CQRS. Ý tưởng về Event Sourcing rất đơn giản: domain của chúng ta đang tạo ra các event record lại tất cả thay đổi được thực hiện trong hệ thống. Nếu chúng ta lấy mọi event từ đầu của hệ thống và phát lại chúng về trạng thái ban đầu, chúng ta sẽ nhận được trạng thái hiện tại của hệ thống. Nó hoạt động tương tự như các giao dịch trên tài khoản ngân hàng của chúng ta; chúng ta có thể bắt đầu với tài khoản rỗng, phát lại từng giao dịch và (hy vọng) có được sự cân bằng hiện tại. Vì vậy, nếu chúng ta lưu trữ tất cả các sự kiện, chúng ta luôn có thể nhận được trạng thái hiện tại của hệ thống.

CQRS_6_CQRS_ES

Event Sourcing

Trong khi Event Sourcing là một phương pháp tuyệt vời để lưu trữ trạng thái của hệ thống là không nhất thiết cần thiết trong CQRS. Đối với CQRS, điều quan trọng là Domain Model thực sự được lưu giữ như thế nào và đây chỉ là một trong những lựa chọn.

EVENTUAL CONSISTENCY

Nếu các mô hình của chúng ta được phân tách về mặt vật lý thì việc đồng bộ hoá sẽ mất một thời gian, tuy nhiên cách này cũng rất đáng sợ đối với những người làm kinh doanh. Trong các dự án của tôi, nếu mỗi phần hoạt động chính xác, thời gian khi Mô hình READ không đồng bộ thì thường không đáng kể. Tuy nhiên, chúng ta chắc chắn sẽ cần phải tính đến các mối nguy thời gian trong quá trình phát triển trong các hệ thống phức tạp hơn. Giao diện người dùng được thiết kế tốt cũng rất hữu ích trong việc xử lý sự nhất quán cuối cùng.

Chúng ta phải giả định rằng ngay cả khi mô hình READ được cập nhật đồng bộ với mô hình WRITE, người dùng vẫn sẽ đưa ra quyết định dựa trên dữ liệu cũ. Thật không may, chúng ta không thể chắc chắn rằng khi dữ liệu được trình bày cho người sử dụng (ví dụ như trình bày trong trình duyệt web) nó vẫn còn tươi.

Code sample

Việc implement CQRS cũng rất đơn giản và không cần đến bất kỳ framwork hỗ trợ nào.

Đầu tiên, chúng ta sẽ định nghĩa các class/interface cho phía Command, bao gồm ICommand, ICommandHandlerICommandDispatcher interface.

public interface ICommand
{
}

public interface ICommandHandler
    where TCommand : ICommand
{
    void Execute(TCommand command);
}

public interface ICommandDispatcher
{
    void Execute(TCommand command)
        where TCommand : ICommand;
}

Tiếp theo là phía Query side, chúng ta cũng có IQuery, IQueryHandler và IQueryDispatcher.

public interface IQuery
{
}

public interface IQueryHandler
    where TQuery : IQuery
{
    TResult Execute(TQuery query);
}

public interface IQueryDispatcher
{
    TResult Execute(TQuery query)
        where TQuery : IQuery;
}

Tiếp theo là viết 1 lớp nhằm hiện thực ICommandDispatcher interface, ta gọi đó là CommandDispatcher. CommandDispatcher có một public method là Execute(TCommand command) để thực thi bất cứ Command nào thông qua CommandHandler. CommandHandler được khởi tạo bởi IDependencyResolver đã được truyền vào constructor của ICommandDispatcher với mục đích tìm ra CommandHandler tương ứng để execute 1 Command.

public class CommandDispatcher : ICommandDispatcher
{
    private readonly IDependencyResolver _resolver;

    public CommandDispatcher(IDependencyResolver resolver)
    {
        _resolver = resolver;
    }

    public void Execute(TCommand command)
        where TCommand : ICommand
    {
        if(command == null)
        {
            throw new ArgumentNullException("command");
        }

        var handler = _resolver.Resolve>();

        if (handler == null)
        {
            throw new CommandHandlerNotFoundException(typeof(TCommand));
        }

        handler.Execute(command);
    }
}

Nữa là việc định nghĩa ra các đối tượng Command và CommandHandler cụ thể cho từng action. Ở đây ta có SignOnCommandSignOnCommandHandler. SignOnCommand là một đối tượng Command như đã đề cập ở trên, chỉ chứa data đơn giản cần thiết cho việc execute command đó. SignOnCommandHandler sẽ nhận vào SignOnCommand và execute action tương ứng với data nhận được.

public class SignOnCommand : ICommand
{
    public AssignmentId Id { get; private set; }
    public LocalDateTime EffectiveDate { get; private set; }

    public SignOnCommand(AssignmentId assignmentId, LocalDateTime effectiveDate)
    {
        Id = assignmentId;
        EffectiveDate = effectiveDate;
    }
}

public class SignOnCommandHandler : ICommandHandler
{
    private readonly AssignmentRepository _assignmentRepository;
    private readonly SignOnPolicyFactory _factory;

    public SignOnCommandHandler(AssignmentRepository assignmentRepository,
                                SignOnPolicyFactory factory)
    {
        _assignmentRepository = assignmentRepository;
        _factory = factory;
    }

    public void Execute(SignOnCommand command)
    {
        var assignment = _assignmentRepository.GetById(command.Id);

        if (assignment == null)
        {
            throw new MeaningfulDomainException("Assignment not found!");
        }

        var policy = _factory.GetPolicy();

        assignment.SignOn(command.EffectiveDate, policy);
    }
}

Để execute SignOnCommand này, chúng ta chỉ cần pass nó vào dispatcher như sau:

_commandDispatcher.Execute(new SignOnCommand(new AssignmentId(rawId), effectiveDate));

Việc implement cho phía Query cũng tương tự như vậy, ta có QueryDispatcher.

public class QueryDispatcher : IQueryDispatcher
{
    private readonly IDependencyResolver _resolver;

    public QueryDispatcher(IDependencyResolver resolver)
    {
        _resolver = resolver;
    }

    public TResult Execute(TQuery query)
        where TQuery : IQuery
    {
        if (query == null)
        {
            throw new ArgumentNullException("query");
        }

        var handler = _resolver.Resolve>();

        if (handler == null)
        {
            throw new QueryHandlerNotFoundException(typeof(TQuery));
        }

        return handler.Execute(query);
    }
}

Như tôi đã nói, việc triển khai này rất dễ dàng mở rộng. Ví dụ: chúng ta có thể handle các transactions cho command dispatcher mà không thay đổi việc thực hiện ban đầu bằng cách sử dụng các Decorator:

public class TransactionalCommandDispatcher : ICommandDispatcher
{
    private readonly ICommandDispatcher _next;
    private readonly ISessionFactory _sessionFactory;

    public TransactionalCommandDispatcher(ICommandDispatcher next,
            ISessionFactory sessionFactory)
    {
        _next = next;
        _sessionFactory = sessionFactory;
    }

    public void Execute(TCommand command)
        where TCommand : ICommand
    {
        using (var session = _sessionFactory.GetSession())
            using (var tx = session.BeginTransaction())
            {
                try
                {
                    _next.Execute(command);
                    tx.Commit();
                }
                catch
                {
                    tx.Rollback();
                    throw;
                }
            }
    }
}

Kết luận

Bằng cách sử dụng CQRS, chúng ta có thể tách rời hoàn toàn mô hình READWRITE, cho phép tối ưu hóa các thao tác đọc và ghi. Điều này làm tăng hiệu suất, làm cho codebase rõ ràng, đơn giản, phản ánh được domain, tăng tính bảo trì.

Một lần nữa, đó là tất cả về encapsulation, low coupling, high cohesion, và Single Responsibility Principle.

Tuy nhiên, cần lưu ý rằng mặc dù CQRS cung cấp một kiểu dáng thiết kế và một số giải pháp kỹ thuật có thể làm cho một ứng dụng rất mạnh mẽ, điều đó không có nghĩa là tất cả các ứng dụng phải được xây dựng theo cách này: Chúng ta nên sử dụng những gì chúng ta cần, và khi nào cần.

Đây là bài viết trong loạt bài viết về “Tổng quan về sự phát triển của kiến trúc phần mềm“. Đây là loạt bài viết chủ yếu giới thiệu về một số mô hình kiến trúc phần mềm hay nói đúng hơn là sự phát triển của chúng qua từng giai đoạn, qua đó giúp chúng ta có cái nhìn tổng quát, up-to-date và là roadmap để bắt đầu hành trình chinh phục (đào sâu) thế giới của những bản thiết kế với vai trò là những kỹ sư và kiến trúc sư phần mềm đam mê với nghề.

Bài viết được tham khảo từ:

https://herbertograca.com/2017/10/19/from-cqs-to-cqrs/

https://www.future-processing.pl/blog/cqrs-simple-architecture/

Tổng hợp bởi edwardthienhoang

Xây dựng hệ thống Ecommerce với DDD và CQRS

Trong quá trình viết về Domain Driven Design và Command Query Responsibility Segregation, mình đã bắt gặp bài viết dưới đây trên engineering blog của tiki, mình xin mạn phép chia sẻ đến các bạn để thực hành phần lý thuyết đã mô tả ở những phần trước.

Quản lý danh mục sản phẩm là một trong các thành phần quan trọng nhất của một hệ thống E-Commerce. Cùng với quản lý đơn hàng và giao vận tạo thành ba trụ cột chính của một hệ thống E-Commerce. Chính vì vậy xây dựng hệ thống quản lý danh mục sản phẩm có vai trò quan trọng trong kiến trúc tổng thể. Nó phải đáp ứng các mục tiêu sau:

  • Quản lý thuộc tính đa dạng của sản phẩm
  • Dễ dàng bảo trì, mở rộng về nghiệp vụ và hệ thống
  • Có thể đáp ứng yêu cầu cao về hiệu năng
  • Giải quyết được các vấn đề tích hợp hệ thống.

1. Product Model.

Sản phẩm là đối tượng hiển thị các thông tin để người dùng thực hiện việc mua bán. Một sản phẩm có các thuộc tính đa dạng về số lượng, loại thuộc tính, kiểu dữ liệu. Mỗi thuộc tính có các yêu cầu khác nhau tính chất dữ liệu: độ dài, kiểu kí tự, các yêu cầu về nghiệp vụ… Do đó model product là một model không có cấu trúc cố định (schemaless structure).

Cách tiếp cận phổ biến để giải quyết vấn đề này là sử dụng model dạng EAV —Entity Attribute Value. Đây là model được Magento, Word Press sử dụng để lưu trữ các cấu trúc đa dạng không định trước. Đặc điểm của cấu trúc EAV là tách phần lưu trữ giá trị thuộc tính (attribute value) khỏi phần cấu trúc các attribute của model. Nhờ đó có thể định nghĩa đa dạng các loại cấu trúc attribute khác nhau của product.

Một attribute set là một tập hợp các thuộc tính. Mỗi thuộc tính phân biệt nhau bởi mã thuộc tính, loại kiểu dữ liệu. Một thuộc tính có thể thuộc nhiều bộ thuộc tính khác nhau. Một product thuộc một bộ thuộc tính. Các giá trị thuộc tính của product tương ứng với các thuộc tính, đặc trưng bởi kiểu dữ liệu và mã thuộc tính. Giá trị thuộc tính có kiểu dữ liệu được định nghĩa bởi thuộc tính. Bằng cách tổ chức như vậy, hệ thống có thể thêm các bộ thuộc tính mới dễ dàng, cũng như bổ sung các thuộc tính vào các bộ thuộc tính đã có.

Ngoài ra một product còn có nhiều hỉnh ảnh, thuộc nhiều category và có nhiều biến thể khác nhau (có quan hệ liên hệ với nhiều product khác, vd điện thoại iphone còn có các sản phẩm biến thể theo màu sắc; hay một chiếc váy có nhiều size khác nhau, có mối quan hệ với các product váy có kích cỡ khác nhau, phân biệt bởi thuộc tính kích cỡ).

2. Thiết kế tầng nghiệp vụ.

Các nghiệp vụ sẽ được phát triển xoay quanh model đã được xây dựng. Đây là layer phức tạp nhất, chiếm phần lớn khối lượng công việc. Nghiệp vụ quản lý sản phẩm tập trung vào việc thay đổi các thuộc tính đa dạng của sản phẩm. Các mục tiêu thiết kế:

  • Đảm bảo nguyên lý Don’t Repeat Yourself tốt nhất. Các nghiệp vụ được phân tách rõ ràng, cô đọng và tập trung cao.
  • Có thể mở rộng, sửa đổi các thành phần độc lập.
  • Linh hoạt trong việc phát triển.
  • Có khả năng dễ dàng test và bảo trì

Các mục tiêu đó sẽ ảnh hưởng tới các cách thiết kế mô hình ứng dụng khác nhau. Phần dưới đây sẽ phân tích từng mô hình thiết kế phần mềm để đưa ra sự lựa chọn phù hợp nhất.

2.1. Mô hình MVC.

Mô hình MVC là mô hình phổ biến trong việc lựa chọn để xây dựng ứng dụng. Đó là mô hình đơn giản, dễ tiếp cận và nhanh chóng đưa ra các tính năng. Theo mô hình MVC, thì phần quản lý catalog sẽ có kiến trúc như sau:

Với mô hình mvc, product model sẽ chứa tất cả các định nghĩa thuộc tính của product, truy xuất database, thực thi các nghiệp vụ. Thông thường mô hình MVC sẽ sử dụng pattern Active Record để mapping với database. Đây là cách làm đơn giản, nhưng có các hạn chế sau:

  • Model product trở nên quá nặng khi chứa quá nhiều các logic từ định nghĩa thuộc tính, truy xuất dữ liệu, thực thi nghiệp vụ…
  • Do không phân tách được các tầng nghiệp vụ và dữ liệu nên khó thực hiện unit test các thành phần riêng biệt.
  • Việc thay đổi logic truy xuất dữ liệu và logic nghiệp vụ khó khăn. Do các thành phần bị phụ thuộc vào nhau.
  • Tính đóng gói của nghiệp vụ không đảm bảo. Các nghiệp vụ sẽ chỉ được cài đặt theo mô hình CRUD. Vì vậy càng về sau sự trùng lặp các logic càng cao.

Do các hạn chế đó nên đây không phải mô hình phù hợp với các ứng dụng logic phức tạp như ecommerce. Nó sẽ dẫn tới chi phí maintain về sau cao.

2.2. Mô hình ba lớp.

Hướng tiếp cận tiếp theo là phân tích và thiết kế theo phương pháp Domain Driven Design. Đặc trưng của mô hình này là:

  • Phân tách tầng nghiệp vụ khỏi tầng ứng dụng và tầng truy xuất cơ sở dữ liệu. Gọi là Domain Layer. Đây là nơi trung tâm chứa tất cả các logic nghiệp vụ.
  • Trong tầng nghiệp vụ, tập trung vào design model sao cho model phản ánh đầy đủ nhất tính chất nhất quán của nghiệp vụ. Chia tầng nghiệp vụ thành hai thành phần riêng biệt: Domain Model và Domain Service.Domain service có vai trò cung cấp các nghiệp vụ ra bên ngoài xoay quanh các domain model của hệ thống.
  • Thiết kế riêng tầng infrastructure chứa các logic về truy xuất dữ liệu, thao tác với database, messeage queue.

Với việc tổ chức như này các tầng ứng dụng, nghiệp vụ và data access sẽ chia tách riêng biệt, và chỉ tương tác thông qua interface. Các nghiệp vụ sẽ xoay quanh model product. Tầng data acccess sẽ truy xuất hoặc lưu trữ các object products.

Nhưng việc áp dụng mô hình ba lớp xoay quanh model product cũng dẫn tới một khó khăn khác là việc model cho các nghiệp vụ đọc dữ liệu. Các nghiệp vụ này rất đa dạng và cách làm hiệu quả nhất là sử dụng các raw sql. Nhưng nếu các việc truy xuất bị thắt chắt vào các mô hình ORM sẽ ảnh hưởng tới hiệu năng, cũng như khó cho việc thay đổi các logic truy xuất dữ liệu. Đồng thời nó cũng ảnh hưởng tới tốc độ phát triển, khi việc cài đặt các nghiệp vụ query đa dạng phục vụ client bị cản trở và phụ thuộc vào việc thiết kế model và tầng Repository.

2.3. Mô hình CQRS.

Mô hình CQRS (Command Query Responsibility Segregation) là sử mở rộng của mô hình trong ba lớp trong DDD. Đặc trưng quan trọng của CQRS là việc tách hai phần logic đọc và ghi dữ liệu ra hai phần riêng biệt:

  • Phần ghi dữ liệu: được thực hiện qua việc send các command tới các handler thông qua command bus. Comand hanlder đóng vai trò tương tự domain service sẽ tương tác với các model để thực hiện các nghiệp vụ thay đổi dữ liệu.
  • Phần đọc dữ liệu: được thiết kế riêng không lệ thuộc vào các model của phần ghi dữ liệu. Do đó có thể linh hoạt trong việc truy xuất database, cũng như sử dụng các data source khác nhau để tối ưu về tốc độ truy xuất.

Mô hình CQRS đã khắc phục các hạn chế đã nêu ở mục 2 bên trên. CQRS mang lại các lợi thế lớn:

  • Cho phép phát triển và tối ưu phần đọc dữ liệu riêng biệt với phần ghi dữ liệu.
  • Việc mô hình hoá các nghiệp vụ ghi dữ liệu dưới các command cho phép che đậy tốt các logic nghiệp vụ, giúp việc mở rộng dễ dàng hơn. Đồng thời các comand đó có thể dễ dàng chuyển đổi giữa xử lý đồng bộ và bất đồng bộ thông qua lớp abstract là command bus mà không thay đổi mô hình. Giúp cung cấp một mô hình nhất quán, xuyên suốt trong bộ kiến trúc.

Cả ba mô hình trên đều có ưu nhược điểm riêng:

  • Mô hình MVC đơn giản, nhưng hạn chế khi giải quyết nghiệp vụ phức tạp và testing, mở rộng
  • Mô hình ba lớp phù hợp cho việc xử lý nghiệp vụ phức tạp, nhưng có nhiều hạn chế khi tối ưu phần đọc ra.
  • Mô hình CQRS mở rộng từ mô hình ba lớp, giải quyết tốt việc chia tách đọc ghi, nhưng cũng đòi hỏi phải thiết kế phức tạp hơn.

Đối với các hệ thống ecommerce lớn thì mô hình CQRS là mô hình phù hợp nhất. Vì nó giúp giải quyết cả hai đòi hỏi lớn là xử lý nghiệp vụ phức tạp và phải đáp ứng hiệu năng cao. Phân tiếp theo sẽ phân tích sâu hơn về mặt hệ thống và một biến thở mở rộng của CQRS là CQRS — ES (Command Query Responsibility Segregation — Event Sourcing).

3. Thiết kế về hệ thống.

Một trong các đòi hỏi lớn của việc quản lý danh mục sản phẩm là phải thiết kế để đáp ứng các nhu cầu về hiệu năng và tích hợp với các hệ thống khác. Để đáp ứng các yêu cầu tốt, thì phải được thiết kế từ tổng thể ngay từ đầu. Có vậy mới tạo ra sự phát triển liền mạch, nhất quán giúp đảm bảo tính ổn định, cũng như tiến độ làm việc.

3.1. Mô hình CQRS — ES.

Mô hình Event Sourcing — ES, là mô hình thiết kế mà trạng thái của object sẽ được lưu trữ dưới dạng chuỗi các sự kiện thay đổi. Nó khác với mô hình thiết kế thông thường, khi mà chỉ lưu trữ trạng thái cuối cùng của object.

Khi object bị thay đổi, sẽ tương ứng với một versioned event lưu trữ dữ liệu thay đổi của object. Các event sẽ được lưu trữ dạng appending only vào một cấu trúc table gọi là event store. Việc lưu trữ các event thay đổi này giúp mang lại các lợi ích:

  • Lưu trữ được lịch sử thay đổi của các đối tượng.
  • Nếu các event được gửi tới các hệ thống khác, các hệ thống đó có thể sinh ra trạng thái cuối cùng chính xác của đốc tượng gốc. Do đó dễ dàng tích hợp với các hệ thống khác.

Dựa trên các event được published đi có thể xây dựng phần lưu trữ riêng cho read side, để tối ưu cho tốc độ truy xuất dữ liệu.

3.2. Event Bus — Kafka — MySql Binlog.

Các versioned event của đối tượng nếu được gửi đi sẽ giúp hệ thống dễ dàng tích hợp với nhiều hệ thống khác. Event Bus là tên gọi logic của một đường truyền chứa tất cả các versioned event dưới dạng stream. Các consumer chỉ việc subscribe event bus là có thể nhận được các event để xử lý.

Các event được lưu trữ trong mysql database. Tất cả các thay đổi trong database đều được MySql lưu vào một log file, gọi là binlog. Bằng việc extra binlog của mysql, thì hệ thống có thể bắt được mọi event phát sinh để gửi đi.

Kafka là một message broker theo mô hình log based, cho phép lưu trữ và gửi đi các event đúng với thứ tự gửi vào. Vì vậy nó là lựa chọn tốt nhất để làm tầng lưu trữ cho event bus.

3.3. Kiến trúc tổng thể.

Đên đây bức tranh của hệ thống đã trở nên rõ ràng. Từng mảnh ghép từ model, nghiệp vụ tới tích hợp đã đầy đủ. Có thể tóm tắt lại: hệ thống sẽ xây dựng nghiệp vụ quanh model product; cấu trúc theo mô hình CQRS — ES; sử dụng MySql Binlog; Kafka để publish các event thay đổi một cách ổn định; dựa trên event bus sẽ xây dựng các cấu trúc lữu trữ có tốc độ truy xuất cao như Elastic, MongoDb, cũng như tích hợp với các hệ thống khác.

4. Kết luận.

Thiết kế hệ thống quản lý danh mục sản phẩm trong ecommerce đòi hỏi phải đạt được cùng lúc nhiều mục tiêu, với một tầm nhìn xuyên suốt và nhất quán. Từ mức ứng dụng cho tới mức hệ thống đều phải có sự gắn kết liền mạch. Tất cả hướng tới hai mục tiêu quan trọng phải đạt được:

  • Xử lý được các nghiệp vụ phức tạp
  • Đảm bộ hiệu năng và độ ổn định của hệ thống.

Phương pháp phân tích thiết kế Domain Driven Desing, mô hình CQRS — ES, cơ chế replicate ổn định của MySql, sự đảm bảo thứ tự message của Kafka, cũng như các database tối ưu cho read như Elastic, Mongo… là các nhân tố chính đảm bảo chất lượng của hệ thống.

Đây là bài viết trong loạt bài viết về “Tổng quan về sự phát triển của kiến trúc phần mềm“. Đây là loạt bài viết chủ yếu giới thiệu về một số mô hình kiến trúc phần mềm hay nói đúng hơn là sự phát triển của chúng qua từng giai đoạn, qua đó giúp chúng ta có cái nhìn tổng quát, up-to-date và là roadmap để bắt đầu hành trình chinh phục (đào sâu) thế giới của những bản thiết kế với vai trò là những kỹ sư và kiến trúc sư phần mềm đam mê với nghề.

Bài viết được tham khảo từ:

Thiết kế hệ thống quản lý danh mục sản phẩm trong hệ thống Ecommerce.

Tổng hợp bởi edwardthienhoang

Clean Architecture: Đứng trên vai những gã khổng lồ

Robert C. Martin (hay còn gọi là Uncle Bob) cho ra đời ý tưởng của mình về Clean Architecture vào năm 2012, trong một bài viết trên blog của mình, và giảng dạy về nó tại một vài hội nghị, và gần đây nhất là cuốn sách Clean Architecture: A Craftsman’s Guide to Software Structure and Design xuất bản năm 2017.

Clean Architecture dựa trên các khái niệm, quy tắc, và mô hình nổi tiếng, giải thích làm thế nào để kết hợp chúng với nhau, để đề xuất cách xây dựng các ứng dụng chuẩn.

Đứng trên vai những gã khổng lồ: EBI, Hexagonal and Onion Architectures

Các mục tiêu cốt lõi của Clean Architecture cũng giống với đối với Ports & Adapters (Hexagonal) và Onion Architectures:

  • Độc lập các công cụ;
  • Sự độc lập của các cơ chế phân phối;
  • Khả năng cô lập kiểm thử.

Trong bài viết về Clean Architecture đã được xuất bản, đây là sơ đồ được sử dụng để giải thích ý tưởng tổng quát:

cleanarchitecture-5c6d7ec787d447a81b708b73abba1680
Robert C. Martin 2012, The Clean Architecture

Như ông Bob nói trong bài của mình, sơ đồ trên là một nỗ lực để tích hợp các ý tưởng kiến trúc gần đây vào một ý tưởng có thể thực hiện được.

Hãy so sánh sơ đồ Clean Architecture với các sơ đồ được sử dụng để giải thích Hexagonal Architecture và Onion Architecture, và xem chúng trùng nhau ở đâu:

hexagonal_original

4ioq9

Các công cụ bên ngoài và cơ chế phân phối

(Externalisation of tools and delivery mechanisms)

Hexagonal Architecture tập trung vào việc mở rộng các công cụ và cơ chế phân phối từ ứng dụng, sử dụng interfaces (Port) và Adapter. Đây cũng là một trong những nền tảng cốt lõi của Onion Architecture, như chúng ta có thể thấy qua biểu đồ của nó, UI, cơ sở hạ tầng và các testing đều nằm trong layer ngoài cùng của sơ đồ. Clean Architecture cũng có đặc điểm giống như vậy, có giao diện người dùng, web, DB, v.v … ở layer ngoài cùng. Trong cùng là tất cả mã lõi ứng dụng độc lập với framework / libraries.

Chiều của sự phụ thuộc

(Dependencies direction)

Trong Hexagonal Architecture, chúng ta không có bất cứ điều gì rõ ràng cho chúng ta biết hướng của các phụ thuộc. Tuy nhiên, chúng ta có thể dễ dàng suy luận nó: Ứng dụng có một Port (interface) phải được implement hoặc sử dụng bởi Adapter. Vì vậy Adapter phụ thuộc vào giao diện, nó phụ thuộc vào ứng dụng nằm ở trung tâm. Bên ngoài phụ thuộc vào bên trong, hướng của các phụ thuộc là về phía trung tâm. Trong Onion Architecture, chúng ta cũng không có bất cứ điều gì rõ ràng cho chúng ta biết hướng phụ thuộc, tuy nhiên, trong bài thứ hai của mình, Jeffrey Palermo cho biết rõ ràng rằng tất cả các phụ thuộc đều hướng đến trung tâm. Clean Architecture, lần lượt, nó khá rõ ràng trong chỉ ra rằng sự phụ thuộc hướng là hướng về trung tâm. Tất cả đều đưa ra Nguyên tắc đảo ngược phụ thuộc (Dependency Inversion Principle) ở cấp kiến ​​trúc. Vòng tròn bên trong không hề biết gì về các vòng tròn bên ngoài. Hơn nữa, khi chúng ta truyền dữ liệu qua một ranh giới (Boundary), nó luôn ở dạng thuận tiện nhất cho vòng tròn phía trong.

Các Layers

Sơ đồ Kiến trúc Hexagonal Architecture chỉ hiển thị cho chúng ta hai layer: Bên trong ứng dụng và bên ngoài của ứng dụng. Mặt khác, Onion Architecture mang đến sự kết hợp các layer ứng dụng được xác định bởi DDD: Entities, Value Objects, Application Services chứa các use-case logic; Domain Services đóng gói domain logic không thuộc về các Entities hoặc Value Objects… Khi so sánh với Onion Architecture, Clean Architecture sẽ duy trì Application Services layer (Use Cases) và Entities layer nhưng dường như nó quên mất Domain Services layer. Tuy nhiên, đọc bài của Bác Bob chúng ta nhận ra rằng ông coi một thực thể không chỉ là và Entity theo ý nghĩa của DDD mà bất cứ Domain object nào: “Một entity có thể là một đối tượng với các phương thức, hoặc nó có thể là một tập hợp các cấu trúc dữ liệu và các hàm. “. Trong thực tế, ông đã sáp nhập hai layer bên trong để đơn giản hóa sơ đồ.

Khả năng cô lập kiểm thử

(Testability in isolation)

Trong cả ba kiểu Kiến trúc, chúng đều tuân theo các nguyên tắc phân tách domain logic với các thành phần bên ngoài. Điều này có nghĩa là trong mọi trường hợp chúng ta chỉ có thể mô phỏng các công cụ bên ngoài và các cơ chế phân phối và thực hiện kiểm thử ứng dụng mà không sử dụng bất kỳ DB hay HTTP request nào.

Như chúng ta có thể thấy, Clean Architecture sẽ kết hợp các quy tắc của Hexagonal Architecture và Onion Architecture. Cho đến nay, kiến trúc sạch sẽ không thêm bất cứ điều gì mới cho phương trình. Tuy nhiên, ở góc dưới cùng bên phải của Clean Architecture diagram, chúng ta có thể thấy thêm một biểu đồ nhỏ …

Đứng trên vai những gã khổng lồ: MVC và EBI

Biểu đồ phụ nhỏ ở góc dưới cùng bên phải của Clean Architecture diagram sẽ giải thích về sơ đồ luồng tương tác (flow of control) giữa các component. Biểu đồ nhỏ này không cung cấp cho chúng ta nhiều thông tin, nhưng lời giải thích về bài đăng trên blog và các bài giảng của hội thảo được đưa ra bởi Robert C. Martin mở rộng về đề tài này.

cleanarchitecturedesign

Trong sơ đồ ở trên, ở phía bên trái, chúng ta có View và Controller của MVC. Mọi thứ bên trong / giữa các đường kẻ đôi màu đen đại diện cho Model trong MVC. Mô hình này cũng đại diện cho kiến trúc EBI (với “Boundary”, Interactor và the Entities”), “Application” trong Hexagonal Architecture, “Application Core” trong Onion Architecture, “Entities” và “Use Cases” layer trong Clean Architecture.

Theo biểu đồ luồng tương tác, chúng ta có một yêu cầu HTTP đến các Controller. Controller sau đó sẽ:

  1. Phân tích Request;
  2. Tạo một Request Model với các dữ liệu có liên quan;
  3. Execute một method trong Interactor (đã được đưa (inject) vào Controller bằng cách sử dụng interface của Interactor là Boundary), chuyển nó cho Request Model;
  4. Interactor sẽ:
    1. Sử dụng implementation của Entity Gateway (được đưa vào Interactor bằng cách sử dụng Entity Gateway Interface) để tìm các Entities liên quan;
    2. Phối hợp các tương tác giữa các Entities;
    3. Tạo Response Model với kết quả dữ liệu trả về;
    4. Tạo ra Presenter chứa Response Model;
    5. Trả lại Presenter cho Controller;
  5. Dùng Presenter để tạo ra một ViewModel;
  6. Bind ViewModel với View;
  7. Trả View về cho Client.

Kết luận

Tôi không nói rằng Clean Architecture là cuộc cách mạng bởi vì nó không thực sự mang lại một khái niệm hoặc mô hình đột phá mới. Tuy nhiên, tôi xin nói rằng đó là một công trình có tầm quan trọng lớn, bởi vì:

  • Nó khôi phục các khái niệm, quy tắc và khuôn mẫu bị quên lãng;
  • Nó làm rõ các khái niệm và quy tắc hữu ích và quan trọng;

Nó cho chúng ta biết tất cả các khái niệm, quy tắc và mẫu này phù hợp với nhau để cung cấp cho chúng ta một cách chuẩn hóa để xây dựng các ứng dụng phức tạp với sự bảo trì trong đầu.

Khi tôi nghĩ về Bác Bob làm việc với Clean Architecture, nó làm cho tôi nghĩ về Isaac Newton. Trọng lực luôn ở đó, mọi người đều biết rằng nếu chúng ta thả quả táo cách mặt đất một mét, nó sẽ di chuyển xuống mặt đất. “Điều duy nhất” mà Newton đã làm là xuất bản một bài báo đưa ra sự thật đó *. Đó là một điều “đơn giản” phải làm, nhưng nó cho phép mọi người lý giải về nó và sử dụng ý tưởng cụ thể làm nền tảng cho những ý tưởng khác.

Nói cách khác, tôi thấy Robert C. Martin là Isaac Newton trong phát triển phần mềm!

Bài viết được tham khảo từ:

https://herbertograca.com/2017/09/28/clean-architecture-standing-on-the-shoulders-of-giants/

Đọc thêm:

2012 – Robert C. Martin – Clean Architecture (NDC 2012)

2012 – Robert C. Martin – The Clean Architecture

2012 – Benjamin Eberlei – OOP Business Applications: Entity, Boundary, Interactor

2017 – Lieven Doclo – A couple of thoughts on Clean Architecture

2017 – Grzegorz Ziemoński  – Clean Architecture Is Screaming

Đây là bài viết trong loạt bài viết về “Tổng quan về sự phát triển của kiến trúc phần mềm“. Đây là loạt bài viết chủ yếu giới thiệu về một số mô hình kiến trúc phần mềm hay nói đúng hơn là sự phát triển của chúng qua từng giai đoạn, qua đó giúp chúng ta có cái nhìn tổng quát, up-to-date và là roadmap để bắt đầu hành trình chinh phục (đào sâu) thế giới của những bản thiết kế với vai trò là những kỹ sư và kiến trúc sư phần mềm đam mê với nghề.

Tổng hợp bởi edwardthienhoang

Kiến trúc củ hành (Onion Architecture)

Onion Architecture được đặt ra bởi Jeffrey Palermo vào năm 2008. Như tôi thấy, nó được xây dựng dựa trên Kiến trúc Ports & Adapters với ý đồ đặt domain vào trung tâm ứng dụng, mở rộng cơ chế phân phối (UI) và cơ sở hạ tầng được sử dụng bởi hệ thống (ORM, công cụ tìm kiếm, các API của bên thứ ba, …). Nhưng nó đi xa hơn và thêm một số layers bên trong vào nó.

Chúng ta đã đi từ Layered Architecture với 4 layer (Presentation, Application, Domain, Persistence) đến Ports và Adapters Architecture mà chỉ ngầm đề cập đến hai layer chủ đạo:

  • Một layer bên ngoài đại diện cho cơ chế phân phối (delivery mechanisms) và cơ sở hạ tầng (infrastructure);
  • Một layer nội bộ đại diện cho business logic.

Cả Ports & AdaptersOnion Architecture đều chia sẻ ý tưởng cô lập core ứng dụng ra khỏi mối quan tâm về cơ sở hạ tầng bằng cách viết mã adapter để mã cơ sở hạ tầng không rò rỉ vào lõi ứng dụng. Điều này làm cho nó dễ dàng hơn để thay thế cả các công cụ và các cơ chế phân phối được sử dụng bởi các ứng dụng, chống lại sự thay đổi về công nghệ, công cụ và các bên thứ 3 (vendor).

Nó cũng cung cấp cho ứng dụng khả năng vận hành dễ dàng mà không cần cơ sở hạ tầng thực tế cũng như các cơ chế phân phối vì chúng có thể được thay thế bằng các kỹ thuật mocking, làm cho việc test trở nên dễ dàng.

Tuy nhiên, Onion Architecture cũng cho chúng ta biết rằng, trong các enterprise applications, chúng ta sẽ có nhiều hơn hai layer (internal và external) đó, nó thêm một số layer trong business logic mà chúng ta có thể nhận ra từ Domain Driven Design:

2008-onion-architecture5

Hơn nữa, nó làm rõ ra chiều phụ thuộc (direction of dependencies) giữa các layer:

  • Layer phía ngoài phụ thuộc vào layer phía trong (Outer layers depend on inner layers);
  • Layer phía trong không hề biết về layer phía ngoài (Inner layers do not know about outer layers).

Điều này có nghĩa hướng của sự phụ thuộc (coupling) hướng về trung tâm, nơi lõi ứng dụng không phụ thuộc vào bất cứ gì. Chúng ta có sự linh hoạt của việc có thể thay đổi các layer bên ngoài mà không ảnh hưởng đến các layer bên trong, và quan trọng hơn. Nó sử dụng Nguyên tắc đảo ngược phụ thuộc (Dependency Inversion Principle), ở cấp độ kiến trúc.

Các nguyên lý chính của Onion Architecture:

  • Ứng dụng được xây dựng xung quanh một object model độc lập
  • Các layer bên trong định nghĩa các interfaces. Các layer bên ngoài implement các interface đó
  • Chiều của sự phụ thuộc (Direction of coupling) hướng tới trung tâm
  • Tất cả mã lõi của ứng dụng có thể được biên dịch và chạy tách biệt với cơ sở hạ tầng

 

  • The application is built around an independent object model
  • Inner layers define interfaces. Outer layers implement interfaces
  • Direction of coupling is toward the center
  • All application core code can be compiled and run separate from infrastructure

Jeffrey Palermo 2008, The Onion Architecture: part 3

Ngoài ra, bất kỳ layer bên ngoài nào cũng có thể gọi trực tiếp bất kỳ layer bên trong, không phá vỡ hướng kết nối và tránh tạo ra các proxy method hoặc thậm chí proxy class không có business logic, chỉ vì mục đích tuân thủ một vài sơ đồ phân lớp (layering scheme). Điều này cũng được khuyến nghị bởi Martin Fowler.

[…] các layers bên trên có thể sử dụng bất kỳ layer bên dưới chúng, không chỉ là layer ngay bên dưới.

[…] the layers above can use any layer beneath them, not just the layer immediately beneath.

Jeffrey Palermo 2008, The Onion Architecture: part 3

Kết luận

Onion Architecture được xây dựng trên Ports & Adapters Architecture để thêm một số tổ chức nội bộ vào business logic của ứng dụng dựa trên một vài khái niệm Domain Driven Design.

Một lần nữa, đây là một sự tiến triển trong việc phân tách trách nhiệm (segregating responsibilities), nhằm tạo nên tính low coupling and high cohesion, tạo điều kiện dễ dàng cho phát triển, kiểm thử và bảo trì.

Đây là bài viết trong loạt bài viết về “Tổng quan về sự phát triển của kiến trúc phần mềm“. Đây là loạt bài viết chủ yếu giới thiệu về một số mô hình kiến trúc phần mềm hay nói đúng hơn là sự phát triển của chúng qua từng giai đoạn, qua đó giúp chúng ta có cái nhìn tổng quát, up-to-date và là roadmap để bắt đầu hành trình chinh phục (đào sâu) thế giới của những bản thiết kế với vai trò là những kỹ sư và kiến trúc sư phần mềm đam mê với nghề.

Bài viết được tham khảo từ:

https://herbertograca.com/2017/09/21/onion-architecture/

Đọc thêm:

2002 – Martin Fowler – Patterns of Enterprise Application Architecture

2008 – Jeffrey Palermo – The Onion Architecture: part 1

2008 – Jeffrey Palermo – The Onion Architecture: part 2

2008 – Jeffrey Palermo – The Onion Architecture: part 3

2013 – Jeffrey Palermo – The Onion Architecture: part 4 – After Four Years

Tổng hợp bởi edwardthienhoang

 

Ports & Adapters Architecture

Trong bài viết trước chúng ta đã nói về Domain Driven Design – cái đặt nền móng cho các triết lý thiết kế trong kiến trúc hiện đại ngày nay, lấy domain làm trung tâm của ứng dụng. Tuy nhiên, Domain Driven Design vẫn còn đi theo lối mòn theo kiểu kiến trúc phân lớp (Layered Architecture). Nếu nhìn vào architecture diagram thì thấy rõ ràng Domain layer không phải nằm ở trung tâm mà lại nằm ở phía dưới cùng của ứng dụng. Trong bài viết này, chúng ta sẽ tìm hiểu về Port & Adapter Architecture, cái sẽ kế thừa ý chí từ Domain Driven Design, lấy Domain làm trung tâm và cô lập tất cả các thành phần khác ở phía bên ngoài.

Ports & Adapters Architecture (hay còn được gọi là Hexagonal Architecture, Kiến trúc lục giác) được Alistair Cockburn nghĩ ra và được viết ra trên blog của ông vào năm 2005. Đây là cách ông định nghĩa mục tiêu của nó trong một câu:

Cho phép application được thực thi, chèo lái (driven) bởi nhiều tác nhân khác nhau như user, những application khác, những bộ automated test hay batch scripts và được phát triển và kiểm thử độc lập, không phụ thuộc vào device (đầu vào / đầu ra) thật và database.

Allow an application to equally be driven by users, programs, automated test or batch scripts, and to be developed and tested in isolation from its eventual run-time devices and databases.

Alistair Cockburn 2005, Ports and Adapters

Tôi đã thấy một số bài báo nói về Ports & Adapters Architecture đã nói rất nhiều về các layers. Tuy nhiên, Alistair Cockburn, ông ấy không hề đề cập đến layers trong bài viết gốc của ông.

Ý tưởng là suy nghĩ về ứng dụng của chúng ta là trung tâm của một hệ thống, nơi mà tất cả các đầu vào và đầu ra đều đến và đi thông qua một cổng (port) – cái sẽ cô lập ứng dụng của chúng ta từ các công cụ, công nghệ và cơ chế phân phối (delivery mechanisms) bên ngoài. Ứng dụng không có kiến thức về những người / những gì đang gửi đầu vào hoặc nhận đầu ra của nó. Điều này nhằm cung cấp sự bảo vệ chống lại sự tiến triển của công nghệ và yêu cầu người dùng, cái có thể làm cho sản phẩm lỗi thời ngay sau khi chúng được phát triển.

Trong bài đăng này, tôi sẽ đi sâu vào các chủ đề sau:

Vấn đề của cách làm truyền thống

Cách tiếp cận truyền thống có thể sẽ mang lại cho chúng ta những vấn đề ở cả phía front-end và back-end.

Ở front-end, chúng ta sẽ có rò rỉ logic nghiệp vụ vào giao diện người dùng (tức là khi chúng ta đưa use-case logic vào trong controller hoặc view, làm cho nó không thể tái sử dụng được trong các màn hình giao diện người dùng khác) hoặc thậm chí là rò rỉ UI vào logic nghiệp vụ (tức là khi chúng ta tạo ra các phương thức trong các entity của chúng ta vì một số logic chúng ta cần trong một template).

hexagonal-arch-5-traditional2

Ở front-end, chúng ta có thể rò rỉ các thư viện và công nghệ bên ngoài vào trong logic kinh doanh bởi vì chúng ta có thể tham chiếu (reference) trực tiếp đến chúng thông qua type hinting, kế thừa, hoặc khởi tạo các lớp thư viện trong business logic của chúng ta.

Tiến hóa từ Layered Architecture

Đến năm 2005, nhờ EBI và DDD, chúng ta biết rằng những gì thực sự có liên quan trong hệ thống là các layer bên trong. Những layer này là nơi chứa tất cả các logic nghiệp vụ, chúng là sự khác biệt thực sự đối với các đối thủ cạnh tranh của chúng ta. Đó là “application” thật sự.

Nhưng Alistair Cockburn nhận ra rằng các layer trên cùng và dưới cùng, chỉ đơn giản là điểm đầu vào/đầu ra đến/từ ứng dụng. Mặc dù chúng thực

sự khác nhau nhưng chúng vẫn có những

hexagonal-arch-1-outer-layers-similarity

mục tiêu rất giống nhau và có sự đối xứng trong thiết kế. Hơn nữa, nếu chúng ta muốn cô lập các lớp ứng dụng bên trong của chúng ta, chúng ta có thể làm điều đó bằng cách sử dụng những điểm vào/ra theo một cách tương tự.

Để thoát khỏi sơ đồ phân lớp điển hình, chúng ta sẽ mô tả hai bên của hệ thống là trái và phải, thay vì trên và dưới cùng.

hexagonal-arch-2-left-right6

Mặc dù chúng ta có thể xác định hai mặt đối xứng của ứng dụng, mỗi bên có thể có một số điểm xuất nhập. Ví dụ, một API và giao diện người dùng là hai điểm xuất nhập khác nhau ở phía bên trái của ứng dụng, trong khi ORM và công cụ tìm kiếm là hai điểm xuất nhập khác nhau ở phía bên phải ứng dụng của chúng ta. Để chứng minh rằng ứng dụng của chúng ta có một số điểm xuất nhập, chúng ta sẽ vẽ sơ đồ ứng dụng của chúng ta với một vài mặt. Sơ đồ có thể có bất kỳ đa giác nào với nhiều mặt, nhưng sự lựa chọn đó là một hình lục giác. Do đó tên “Kiến trúc lục giác“.

hexagonal-arch-3-hexagon2

Kiến trúc Ports & Adapters giải quyết các vấn đề đã được xác định trước đó bằng cách sử dụng một lớp trừu tượng, được thực hiện như một port và môtk adapter.

Port là gì?

Như tên gọi của nó, một Port là một cổng đầu vào, đầu ra trong ứng dụng của chúng ta. Trong nhiều ngôn ngữ, nó sẽ là một interface.

Ví dụ: có thể là một interface được sử dụng để thực hiện tìm kiếm trong công cụ tìm kiếm. Trong ứng dụng của chúng ta, chúng ta sẽ sử dụng giao diện này mà không cần có kiến thức về việc thực hiện cụ thể là gì.

Adapter là gì?

Cũng như tên gọi của nó, Adapter (Bộ chuyển đổi) là một lớp chuyển đổi (thích nghi) một interface thành một interface khác.

Ví dụ, một adapter implement một interface A và được đưa vào interface B. Khi adapter được khởi tạo, nó được đưa trong constructor của đối tượng implement interface B. Adapter này sau đó được đưa vào bất cứ nơi nào interface A là cần thiết và nhận được phương pháp yêu cầu rằng nó chuyển đổi và proxy đến đối tượng bên trong thực hiện interface B.WWW

Nếu vẫn chưa hiểu, đừng lo lắng, lát nữa chúng ta sẽ xem xét các ví dụ cụ thể

Hai loại adapter khác nhau

Những Adapter ở phía bên trái, đại diện cho giao diện người dùng, được gọi là Adapter sơ cấp (primary) vì chúng là nơi để bắt đầu một số action đối với ứng dụng, trong khi những Adapter ở bên phải, đại diện cho các connection với các công cụ phụ trợ, được gọi là Adapter thứ cấp (Secondary) bởi vì chúng chỉ được gọi khi primary Adapter thực hiện một hành động nào đó.

  • Phía bên trái, Adapter phụ thuộc vào Port và được đưa (inject) vào trong implementation cụ thể của port (hay còn gọi là nơi thực hiện use-case). Cả Port và implementation cụ thể của nó (use-case) thuộc về bên trong ứng dụng;
  • Ở phía bên phải, Adapter là implementation cụ thể của Port và được đưa (inject) vào business logic của chúng ta, ứng dụng của chúng ta chỉ làm việc trên interface đó. Ở phía này, Port nằm trong ứng dụng, nhưng implementation của nó thuộc về bên ngoài và wrap một số tool bên ngoài.

hexagonal-arch-4-ports-adapters2

Lợi ích

Sử dụng port/adapter design, với ứng dụng của chúng ta nằm ở trung tâm của hệ thống, cho phép chúng ta giữ ứng dụng được tách biệt với các chi tiết triển khai như công nghệ thường xuyên thay đổi, công cụ và cơ chế phân phối, làm cho giai đoạn chứng minh khả năng (Proof of Concept – POC) và thử nghiệm sản phẩm (Pilot Phase) trở nên dễ dàng thực hiện hơn.

Tách biệt các hiện thực (Implementation) cụ thể và công nghệ

Bài toán

Chúng ta có một ứng dụng sử dụng SOLR làm search engine và sử dụng một open source library để kết nối với nó và thực hiện việc tìm kiếm.

Hướng tiếp cận truyền thống

Với phương pháp tiếp cận truyền thống, chúng ta sẽ sử dụng các lớp thư viện trực tiếp trong codebase của chúng ta bằng cách khởi tạo trực tiếp lớp thư viện đó hoặc tạo ra lớp kế thừa từ lớp thư viện đó.

Tiếp cận theo hướng Ports & adapters

Chúng ta sẽ tạo ra một interface, chúng ta hãy gọi nó là UserSearchInterface, và sử dụng trong code của chúng ta. Chúng ta cũng sẽ tạo một adapter cho SOLR, sẽ implement UserSearchInterface với tên là UserSearchSolrAdapter. Đây là một wrapper cho thư viện SOLR, do đó nó sẽ được inject vào hệ thống của chúng ta, hệ thống không hề biết sự tồn tại cụ thể của UserSearchSolrAdapter, mà chỉ work trên UserSearchInterface.

Vấn đề

Tại một số thời điểm, chúng ta muốn chuyển từ SOLR sang Elasticsearch. Hơn nữa, đối với cùng một tìm kiếm, đôi khi chúng ta muốn sử dụng SOLR và những lần khác chúng ta muốn sử dụng Elasticsearch, tất cả phụ thuộc vào lúc run-time.

Nếu chúng ta sử dụng cách tiếp cận truyền thống, chúng ta sẽ phải tìm kiếm và thay thế việc sử dụng thư viện SOLR cho thư viện Elasticsearch. Tuy nhiên, đó không đơn giản chỉ là tìm và thay thế: các thư viện có những cách khác nhau được sử dụng, các phương pháp khác nhau với đầu vào và đầu ra khác nhau, do đó thay thế các thư viện sẽ không phải là một nhiệm vụ tầm thường. Và việc sử dụng một thư viện thay vì một thư viện khác lúc run-time, thậm chí sẽ không thể thực hiện được.

Tuy nhiên, nếu chúng ta sử dụng Ports & Adapters, chúng ta chỉ cần tạo một adapter mới, đặt tên nó là UserSearchElasticsearchAdapter và dùng nó thay vì adapter SOLR, có thể chỉ bằng cách thay đổi cấu hình trong DIC. Để thực hiện các thao tác khác nhau lúc run-time, chúng ta có thể sử dụng Factory để quyết định nên dùng adapter nào.

Tách biệt cơ chế phân phối đầu ra (Delivery mechanisms isolation)

Tương tự như ví dụ trước, giả sử chúng ta có một ứng dụng cần một GUI web, một CLI và một API web. Chúng ta cũng có một số chức năng mà chúng ta muốn cung cấp trong cả ba giao diện người dùng, hãy gọi chức năng UserProfileUpdate đó.

Sử dụng Ports & Adapters, chúng ta sẽ thực hiện chức năng này trong một application service method và nghĩ nó như là một use-case. Service này sẽ implement một interface xác định các phương pháp, đầu vào và đầu ra.

Mỗi phiên bản UI sẽ có một Controller (hoặc console command) mà có thể sử dụng giao diện đó để kích hoạt logic mong muốn. Ở đây, Adapter thực sự là controller (hoặc CLI Command).

Sau đó, chúng ta có thể thay đổi hoàn toàn UI mà không ảnh hưởng đến business logic.

Testing

Trong cả hai ví dụ trước, testing trở nên dễ dàng hơn với Kiến trúc Ports and Adapters. Trong những ví dụ đầu tiên, chúng ta có thể mô phỏng hoặc khai báo giao diện (Port) và test ứng dụng của chúng ta mà không cần sử dụng SOLR cũng như Elasticsearch.

Trong ví dụ thứ hai, chúng ta có thể test tất cả các UI trong sự cô lập từ ứng dụng, và ngược lại, các use-case (service) sẽ được test mà không cần UI, chỉ cần đưa vào các dạng input thô và verify output thô.

Kết luận

Theo cách tôi nhìn thấy, kiến trúc Ports & Adapters chỉ có một mục đích: cô lập business logic từ các cơ chế phân phối và các công cụ được hệ thống sử dụng. Bằng cách sử dụng interface trong lập trình.

Ở phía UI (các driving adapter), chúng ta tạo ra adapters sử dụng các application interfaces của chúng ta, ví dụ như Controller.

Về phía cơ sở hạ tầng (các driven adapter), chúng ta tạo ra các adapter implement các interface từ ứng dụng của chúng ta, ví dụ như Repository.

Đó là tất cả!

Tuy nhiên, lưu ý rằng ý tưởng này cũng đã được công bố 13 năm trước, mặc dù không rõ ràng nhấn mạnh mục tiêu cô lập các công cụ và cơ chế phân phối từ cốt lõi của ứng dụng.

fig_7_14_boundaries
Ivar Jacobson 1992, pp. 171

Bất kỳ interactor nào của hệ thống với một Actor đi qua một đối tượng Boundary. Như Jacobson mô tả, một Actor có thể là một người sử dụng giống như một khách hàng hoặc một quản trị viên (nhà điều hành), nhưng nó cũng có thể là một “người sử dụng” không phải là con người giống như một máy báo động hoặc một máy in, cái tương ứng với Driving Adapters và Driven Adapters của Ports & Adapters Architecture

Bài viết được tham khảo từ:

https://herbertograca.com/2017/09/14/ports-adapters-architecture/

Đọc thêm:

1992 – Ivar Jacobson – Object-Oriented Software Engineering: A use case driven approach

200? – Alistair Cockburn – Hexagonal Architecture

2005 – Alistair Cockburn – Ports and Adapters

2012 – Benjamin Eberlei – OOP Business Applications: Entity, Boundary, Interactor

2014 – Fideloper – Hexagonal Architecture

2014 – Philip Brown – What is Hexagonal Architecture?

2014 – Jan Stenberg – Exploring the Hexagonal Architecture

2017 – Grzegorz Ziemoński – Hexagonal Architecture Is Powerful

2017 – Shamik Mitra – Hello, Hexagonal Architecture

Tổng hợp bởi edwardthienhoang

Đo lường kết cấu kiến trúc phần mềm

Mục tiêu của thiết kế làm làm sao để hệ thống xây dựng lên nhanh chóng, dễ dàng thích nghi với các thay đổi về nghiệp vụ và công nghệ, dễ dàng bảo trì về sau. Để làm được điều đó, thiết kế cần đảm bảo tính low coupling, high cohesion và encapsulation. Tuy nhiên trong quá trình phát triển, làm sao để biết được là chúng ta đã đi đúng hướng, các class, component, … tạo ra đã đáp ứng được yêu cầu về mặt thiết kế hay chưa? Vì vậy chúng ta cần một số công cụ, phương thức nhằm đo lường (measure) các số liệu (metric) về thiết kế trong suốt quá trình thiết kế và phát triển để đảm bảo mọi thứ đang được đi đúng hướng.

Robert C. Martin, tác giả của SOLIDClean CodeAgile Software Development, Principles, Patterns, and Practices và gần đây nhất là cuốn Clean Architecture: A Craftsman’s Guide to Software Structure and Design.. Ông đã đưa ra một nhóm các metric tập trung vào việc phân tích mối quan hệ giữa các component.

Các metric bao gồm:

  • Efferent Coupling (Ce)
  • Afferent Coupling (Ca)
  • Instability (I)
  • Abstractness (A)
  • Normalized Distance from Main Sequence (D)

EFFERENT COUPLING (CE)

Metric này được sử dụng để đo mối tương quan giữa các component. Theo định nghĩa, nó là một số class trong một component phụ thuộc vào các class trong các component khác, trả lời cho câu hỏi mật độ một component phải thay đổi khi có sự thay đổi ở các component khác như thế nào.

Pic.-1-–-Outgoing-dependencies
Hình 1 – Outgoing dependencies

Trong Hình 1 cho thấy rằng component A có sự phụ thuộc đến (outgoing dependency) 3 component khác, đó là lý do tại sao chỉ số Ce cho component này là 3.

Khi chỉ số Ce > 20 cho thấy sự không ổn định của một component, thay đổi trong bất kỳ component bên ngoài có thể gây ra sự cần thiết phải thay đổi bên trong component này. Chỉ số Ce nên nằm trong khoảng từ 0 đến 20, giá trị cao hơn gây ra vấn đề với việc phát triển và bảo trì.

AFFERENT COUPLING (CA)

Metric này là phần bổ sung cho Metric Ce và được sử dụng để đo lường một loại phụ thuộc khác giữa các component. Nó cho phép chúng ta đo độ nhạy của các component còn lại với những thay đổi trong component phân tích, trả lời cho câu hỏi liệu những thay đổi trong component này sẽ ảnh hưởng đến những chỗ khác như thế nào.

Pic.-2-–-Incoming-dependencies
Hình 2 – Incoming dependencies

Trong Hình 2 có thể thấy rằng component A chỉ có 1 sự phụ thuộc vào (incoming dependency) component X, đó là lý do tại sao giá trị của chỉ số Ca bằng 1.

Giá trị của Ca càng cao nghĩa là độ ổn định (stability) của component sẽ cao. Điều này cho thấy có rất nhiều component đang depend trên component này. Vì vậy, nó không thể được sửa đổi đáng kể bởi vì xác suất lây lan những thay đổi như vậy sẽ tăng lên.
Giá trị khuyến nghị của Ca vào khoảng 0 đến 500.

INSTABILITY (I)

Metric này được sử dụng để đo độ nhạy tương đối của component với những thay đổi. Theo công thức dưới đây:

Instability

Trong đó: Ce – phụ thuộc đến, Ca – phụ thuộc vào

Pic.-3-Instability
Hình 3 – Instability

Trong Hình 3 có thể thấy rằng component A phụ thuộc đến 3 component và có 1 component phụ thuộc vào nó, do đó theo công thức ta có I = 0,75.

Trên cơ sở giá trị của chỉ số I chúng ta có thể phân biệt hai loại thành phần:
Những component có nhiều phụ thuộc đến và không nhiều phụ thuộc vào (giá trị I tiến gần tới 1), điều này không ổn định bởi vì các component này có khả năng bị thay đổi rất cao;

Những component có nhiều phụ thuộc vào và không nhiều phụ thuộc đến (giá trị I gần về 0), do đó chúng sẽ có khả năng ít bị thay đổi, tuy nhiên, sự thay đổi từ chúng sẽ có ảnh hưởng rất lớn.

Các giá trị ưu tiên cho chỉ số I phải nằm trong phạm vi từ 0 đến 0.3 hoặc 0.7 đến 1. Các component phải rất ổn định hoặc rất không ổn định, và nên tránh các component có độ ổn định trung bình.

ABSTRACTNESS (A)

Metric này được sử dụng để đo mức độ trừu tượng của component được tính theo công thức:

Abstractness1

Trong đó: Tabstract là số lớp trừu tượng, interface trong một component, T concrete là số lớp cụ thể trong một component

Các giá trị khuyến nghị cho chỉ số A phải có các giá trị cực trị gần 0 hoặc 1. Các component có độ ổn định (I gần bằng 0), nghĩa là chúng phụ thuộc ở mức rất thấp trên các component khác, cũng nên được trừu tượng (chỉ số A cũng gần bằng 1). Ngược lại, các component không ổn định (I gần với 1) nên bao gồm các lớp cụ thể (A cũng gần bằng 0).

Thêm vào đó, cần lưu ý rằng việc kết hợp tính trừu tượng và tính ổn định cho phép Martin hình thành luận văn về sự tồn tại của luồng chính (Main sequence) (Hình 4).

Pic.-4-–-Main-sequence
Hình 4 – Main sequence

Trong trường hợp tối ưu, tính không ổn định của lớp được bù đắp bởi tính trừu tượng của nó, có một phương trình I + A = 1. Các class được thiết kế tốt nên tập trung xung quanh điểm kết thúc của đồ thị này theo luồng chính.

Normalized-Distance-from-Main-Sequence

Trong đó: A- tính trừu tượng, I – tính bất ổn

Giá trị của chỉ số D có thể được giải thích theo cách sau: nếu chúng ta đặt một lớp cho một đồ thị của luồng chính (Hình 5) thì khoảng cách của nó từ luồng chính sẽ tỉ lệ thuận với giá trị của D.

Pic.-5-–-Normalized-distance-from-Main-Sequence
Hình 5 – Khoảng cách được chuẩn hóa từ luồng chính

Giá trị của D phải càng thấp càng tốt để các thành phần được đặt gần với luồng chính.

Ngoài ra, hai trường hợp cực kỳ bất lợi được xem xét:

  • A = 0 và I = 0, một component rất ổn định (stable) và cụ thể (concrete), tình huống không được mong muốn bởi vì chúng rất khó hoặc không thể mở rộng;
  • A = 1 và I = 1, hoàn toàn không thể vì một component hoàn toàn trừu tượng phải có một số kết nối với bên ngoài, do đó có thể tạo ra cá instance để implement các chức năng được định nghĩa trong các lớp trừu tượng trong component này.

SUMMARY

Sử dụng các số liệu của Martin, chúng ta có thể dễ dàng đánh giá các mối quan hệ giữa các component trong dự án và xác định liệu có cần phải thực hiện các thay đổi để tránh những vấn đề có thể xảy ra trong tương lai hay không. Tất cả các số liệu được thảo luận trong bài viết này có thể được đo cho các dự án trong .NET sử dụng công cụ NDepend hoặc JDepend đối với Java.

Đây là bài viết trong loạt bài viết về “Tổng quan về sự phát triển của kiến trúc phần mềm“. Đây là loạt bài viết chủ yếu giới thiệu về một số mô hình kiến trúc phần mềm hay nói đúng hơn là sự phát triển của chúng qua từng giai đoạn, qua đó giúp chúng ta có cái nhìn tổng quát, up-to-date và là roadmap để bắt đầu hành trình chinh phục (đào sâu) thế giới của những bản thiết kế với vai trò là những kỹ sư và kiến trúc sư phần mềm đam mê với nghề.

Bài viết được tham khảo từ:

https://www.future-processing.pl/blog/object-oriented-metrics-by-robert-martin/

Đọc thêm:

Click to access oodmetrc.pdf


Click to access Agile-Principles-Patterns-and-Practices-in-C.pdf


Click to access bb_msc_thesis.pdf


http://staff.unak.is/andy/staticanalysis0809/metrics/i.html
http://www.onjava.com/pub/a/onjava/2004/01/21/jdepend.html
https://www.future-processing.pl/blog/analysing-the-quality-of-code-with-ndepend/

Tổng hợp bởi edwardthienhoang

 

Domain-Driven Design

Tôi đã có một bài viết về Domain Drive Development (DDD) – First thought để “đặt vấn đề” cho DDD, các bạn có thể tham khảo ở đó trước. Trong phạm vi bài viết này, tôi sẽ tham khảo và diễn đạt lại từ bài viết Domain-Drive Design của tác giả herbertograca.

Domain-Drive Design do Eric Evans tạo ra trong cuốn sách nổi tiếng của ông về Domain-Driven Design: Tackling Complexity in the Heart of Software, xuất bản năm 2003. Cuốn sách của Eric Evans là chìa khóa chính thức hóa nhiều khái niệm phát triển phần mềm hiện nay.

Tôi không thể đưa ra một đánh giá toàn diện về DDD trong một bài viết trên blog. Có quá nhiều khái niệm quan trọng liên quan đến DDD. May mắn thay, đó cũng không phải là mục tiêu ở đây. Tuy nhiên, những gì tôi sẽ làm là liệt kê các khái niệm DDD mà tôi thấy có liên quan đến cách tôi muốn tổ chức mã và cách tôi nghĩ về Kiến trúc: các khái niệm hệ thống rộng tạo thành nền tảng cho phát triển tính năng.

Trong bài viết, tôi sẽ nói về:

  • Ubiquitous language (Ngôn ngữ chung)
  • Layers Bounded contexts
  • Anti-Corruption Layer
  • Shared Kernel
  • Generic Subdomain

Ubiquitous language

Một vấn đề thường xảy ra trong phát triển phần mềm, xoay quanh sự hiểu biết về mã nguồn, nó là gì, nó làm gì, nó như thế nào, tại sao nó lại … nó thậm chí còn phức tạp hơn để hiểu mã nguồn khi nó sử dụng một thuật ngữ khác với thuật ngữ mà các chuyên gia về nghiệp vụ (domain expert) sử dụng. Ví dụ, nghiệp vụ chuyển tiền trong ngân hàng được các chuyên gia gọi là remittance, tuy nhiên khi các developer nghe được từ “chuyển tiền”, họ lại để trong code là money transfer, điều này có thể gây ra nhiều nhầm lẫn khi thảo luận về ứng dụng. Tuy nhiên, hầu hết sự mơ hồ này có thể được giải quyết bằng cách đặt tên đúng các lớp và các phương thức, làm cho chúng thể hiện một đối tượng và phương pháp nào đó trong ngữ cảnh của nghiệp vụ.

Ý tưởng chính của việc sử dụng một Ngôn ngữ chung (Ubiquitous language) là để giúp cho việc không nhầm lẫn mơ hồ giữa các khái niệm về nghiệp vụ với cách đặt tên, ánh xạ các khái niệm đó ở trong mã nguồn. Code, class, method, properties và đặt tên mô-đun phải phù hợp với ngôn ngữ chung. Nếu cần thì ta có thể refactor (tái cấu trúc) lại mã nguồn để đạt được điều đó, với mục đích chung là thống nhất các thuật ngữ giữa code và nghiệp vụ.

Layers

Tôi đã nói về Layers trong một bài viết trước, nhưng tôi sẽ viết lại trong bài viết này từ góc nhìn của DDD:

User Interface

Chịu trách nhiệm render màn hình người dùng sử dụng để tương tác với ứng dụng và dịch các đầu vào của người dùng vào các lệnh ứng dụng. Điều quan trọng cần lưu ý là “người dùng” có thể là con người nhưng cũng có thể là các ứng dụng khác kết nối với API của chúng ta, tương ứng hoàn toàn với các đối tượng Boundary trong kiến trúc EBI;

Application Layer

Dàn xếp các đối tượng domain để thực hiện các tác vụ theo yêu cầu của người dùng hay còn gọi là các use-case. Nó không chứa business logic. Điều này giống với Interactors trong kiến trúc EBI, ngoại trừ Interactors là bất kỳ đối tượng nào không liên quan đến User Interfaces hoặc Entity, và trong trường hợp này, Application Layer chỉ chứa các đối tượng có liên quan đến một use-case. Lớp này là nơi mà Application Services thuộc về, vì chúng là nơi tổng hợp, sử dụng tất cả các loại object khác như repositories, Domain Services, Entities, Value Objects hoặc bất kỳ đối tượng Domain nào để đáp ứng yêu cầu của các use-case.

Domain Layer

Đây là lớp có chứa tất cả logic nghiệp vụ, Domain Services, Entities, Events và bất kỳ loại đối tượng khác có chứa Logic nghiệp vụ. Rõ ràng nó chính là Entity Object của EBI. Đây là trung tâm, là trái tim của hệ thống. Domain Services sẽ chứa các logic nghiệp vụ không phù hợp với một Entity, thường là kết hợp một số Entity trong việc thực hiện một số công việc liên quan đến nghiệp vụ;

Infrastructure

Các khả năng kỹ thuật hỗ trợ các Layers ở trên, ví dụ như persistence hoặc messaging.

ddd_layers

Eric Evans, 2003

Bounded contexts

Trong một ứng dụng doanh nghiệp, có thể có rất nhiều model, các khái niệm cũng như số lượng và kích thước của team làm việc trên codebase là rất lớn. Điều này mang lại cho chúng ta hai vấn đề:

  1. Các developer làm việc trên codebase càng lớn thì càng khó nhận thức và hiểu đúng về những gì những đoạn mã đang làm, và do đó có thể tạo ra bug, gây khó khăn trong việc debug, hiểu đúng và ra quyết định được làm theo thế nào.
  2. Các nhiều developer làm việc trên cùng một codebase, càng khó khăn hơn để hợp tác và có một tầm nhìn chung về kỹ thuật và nghiệp vụ của ứng dụng.

Nói cách khác, đó là 2 vấn đề lớn cần giải quyết khi làm việc với các ứng dụng tầm cỡ enterprise.

Giải pháp thông thường cho một vấn đề lớn là chia nhỏ nó thành những phần nhỏ hơn, hay còn gọi là “bounded contexts”. Nơi mỗi phần phục cho những đối tượng người dùng khác nhau. Nói cách khác, trong 1 hệ thống phần mềm doanh nghiệp, nơi mà hệ thống sẽ phục vụ cho rất nhiều đối tượng người sử dụng khác nhau, việc chúng ta nên làm là chia nhỏ hệ thống đó ra thành những hệ thống nhỏ hơn, đơn lẻ về nhiệm vụ, và đối tượng người dùng. Ví dụ hệ thống nhân sự, kế toán, tiền lương. Mỗi hệ thống có 1 ngữ cảnh riêng gọi là “bounded contexts”, nơi mà nó hệ thống con đó chỉ có hiểu biết về ngữ cảnh, nghiệp vụ nó đảm nhiệm.

Two subsystems commonly serve very different user communities.

Eric Evans 2014, Domain-Driven Design Reference

Các bounded contexts xác định một ngữ cảnh áp dụng một phần riêng biệt của mô hình nghiệp vụ. Việc cô lập này có thể đạt được bằng cách tách các logic kỹ thuật, tách biệt codebase, tách biệt giản đồ cơ sở dữ liệu (database schema) và về tổ chức team. Mức độ cô lập bounded context, như thường lệ, phụ thuộc vào tình hình thực tế: nhu cầu và khả năng chúng ta có.

Một điểm thú vị, đây không phải là một khái niệm hoàn toàn mới. Ivar Jacobson đã viết về các hệ thống con (subsystems) trong cuốn sách của mình (Object-Oriented Software Engineering: A Use Case Driven Approach), trở lại vào năm 1992, mười một năm trước Eric Evans!

fig_7_27_subsystems

Ivar Jacobson, 1992

Khi đó, ông đã có một vài ý tưởng rất cụ thể về chủ đề này:

  • Hệ thống do vậy bao gồm một số các hệ thống con có thể chứa các hệ thống con của chính nó. Ở dưới cùng của phân cấp như vậy là các đối tượng phân tích (analysis objects). Các hệ thống con là một cách để cấu trúc hệ thống cho việc phát triển và bảo trì.
  • Nhiệm vụ của các hệ thống con là đóng gói các đối tượng sao để làm giảm đi sự phức tạp.
    Tất cả các đối tượng đảm nhiệm các phần cụ thể của chức năng sẽ được đặt trong cùng một hệ thống con
  • Mục đích là để có một gắn kết chức năng mạnh mẽ trong một subsystem và một sự liên kết lỏng lẽo giữa các subsystem (ngày nay được gọi là low coupling and high cohesion)
  • [Một hệ thống con] nên tốt hơn nên được sử dụng bởi chỉ một actor, vì thay đổi thường được gây ra bởi một actor.
  • […] bắt đầu bằng cách đặt các đối tượng điều khiển trong một subsystem, và sau đó đặt các đối tượng thực thể liên kết chặt chẽ (strongly coupled) và các đối tượng giao diện (interface objects) trong cùng một subsystem.
  • Tất cả các đối tượng có gắn kết chức năng mạnh mẽ (strong mutual functional coupling) sẽ được đặt trong cùng một subsystem […]
    • Liệu những thay đổi trong một đối tượng dẫn đến những thay đổi trong đối tượng kia? (Điều này bây giờ được gọi là Nguyên tắc The Common Closure Principle – Classes được xuất bản bởi Robert C. Martin trong bài báo “Granularity” năm 1996, 4 năm sau cuốn sách Ivar Jacobson)
    • Liệu chúng có giao tiếp với cùng một actor?
    • Có phải cả hai đều phụ thuộc vào một đối tượng thứ ba, chẳng hạn như một interface object hay một entity? Liệu một đối tượng thực hiện một số hoạt động trên đối tượng kia? (Điều này được gọi là Nguyên tắc The Common Reuse Principle – Classes, được sử dụng cùng nhau được đóng gói cùng nhau của Robert C. Martin trong bài báo “Granularity” năm 1996, 4 năm sau cuốn sách Ivar Jacobson)
  • Một tiêu chí khác cho việc phân chia là phải có ít thông tin trao đổi giữa các hệ thống con khác nhau càng tốt (low coupling)
  • Đối với các dự án lớn, có thể có các tiêu chí khác cho phân hệ thống con, ví dụ:
    • Các nhóm phát triển khác nhau có năng lực hoặc nguồn lực khác nhau và có thể phân phối các công việc phát triển phù hợp (các nhóm cũng có thể được tách biệt về mặt địa lý)
    • Trong một môi trường phân tán, một hệ thống phụ có thể được yêu cầu ở mỗi logical node (SOA, web services và micro services) Nếu một sản phẩm hiện có có thể được sử dụng trong hệ thống này, điều này có thể được coi là một subsystem (các libraries mà hệ thống phụ thuộc vào, ví dụ như ORM).

Anti-Corruption Layer

Một Anti-Corruption Layer cơ bản là một middleware giữa hai hệ thống con. Nó được sử dụng để cô lập hai hệ thống con, làm cho chúng phụ thuộc vào layer này thay vì phụ thuộc trực tiếp vào nhau. Bằng cách này, nếu chúng ta tái cấu trúc hoặc thay thế hoàn toàn một trong các hệ thống con thì chúng ta sẽ chỉ phải cập nhật layer này để các hệ thống con khác không bị ảnh hưởng.

Điều này đặc biệt hữu ích khi có một hệ thống mới mà chúng ta cần phải tích hợp với một hệ thống có sẵn. Để không để những hệ thống cũ chịu sự ảnh hưởng từ việc thêm mới các hệ thống con mới, chúng ta tạo ra một Anti-Corruption Layer sẽ điều chỉnh API của hệ thống cũ cho các nhu cầu của hệ thống con mới.

Có 3 mối quan tâm chính:

  1. Điều chỉnh các API hệ thống con với những gì các client subsystems cần;
  2. Translate data và commands giữa các hệ thống con;
  3. Thiết lập trao đổi (communication) theo một hoặc nhiều hướng, nếu cần

fig_14_8_anticorruption_layer

Eric Evans, 2003

Đây là một kỹ thuật được sử dụng hợp lý khi chúng ta không kiểm soát một hoặc tất cả các hệ thống con, nhưng cũng có thể sử dụng nó khi chúng ta kiểm soát tất cả các hệ thống con liên quan, ngay cả khi chúng được thiết kế tốt nhưng đơn giản có các model rất khác nhau và chúng ta muốn ngăn chặn sự rò rỉ từ model này sang model khác (thay đổi một hệ thống con để phù hợp với nhu cầu của một hệ thống con khác).

Shared Kernel

Trong một số trường hợp, bất chấp mong muốn của chúng ta để có các thành phần tách biệt hoàn toàn và tách rời, vẫn có một số trường hợp buộc ta phải tách một số domain code ra để chia sẻ cho nhiều component khác sử dụng.

Điều này sẽ cho phép các component vẫn giữ được tính phân tách và độc lập với các component khác mặc dù sử dụng chung những mã chia sẻ cùng (shared code), ta gọi các mã chia sẻ này với cái tên “shared kernel”.

Trường hợp ví dụ, với các events được kích hoạt bởi một component và lắng nghe bởi một hoặc một số component khác. Và tương tự cho các service interfaces and thậm chí là các entities.

Tuy nhiên, nên giữ phần Shared Kernel này càng nhỏ càng tốt, và cẩn thận khi thay đổi nó vì chúng ta có thể một cách vô tình gây ảnh hưởng đến những chỗ khác đang sử dụng nó. Điều quan trọng là mã trong Shared Kernel sẽ không nên bị thay đổi nếu không có sự tham gia và hiểu biết của tất cả các nhóm phát triển khác sử dụng nó.

Generic Subdomain

Một Subdomain là một phần rất biệt lập của domain. Generic Subdomain là một Subdomain không liên quan đến ứng dụng của chúng ta mà có thể được sử dụng trong bất kỳ ứng dụng nào tương tự.

Vì vậy, nếu có một ứng dụng có một phần của nó là về finance, có lẽ chúng ta có thể sử dụng một thư viện finance hiện có trong ứng dụng. Nhưng dù sao đi nữa, ngay cả khi không thể sử dụng thư viện hiện có và cần xây dựng riêng của chúng ta, nếu nó là một Generic Subdomain thì đó không phải là hoạt động cốt lõi và nó cần phải được coi là cần thiết nhưng không quan trọng. Đây không phải là phần quan trọng nhất trong ứng dụng nên không phải là nơi các chuyên gia giỏi nhất nên tập trung và thậm chí phải rõ ràng bên ngoài mã nguồn chính, nó có thể được cài đặt với một công cụ quản lý sự phụ thuộc (dependency management tool).

Kết luận

Các khái niệm DDD tôi đã chọn để tiếp cận ở đây là, một lần nữa, chủ yếu về single responsibility, low coupling, high cohesion, isolating logic để ứng dụng của chúng ta trở nên nhất quán, dễ dàng và nhanh chóng hơn để thay đổi và thích ứng với nhu cầu của doanh nghiệp.

Sources

1992 – Ivar Jacobson – Object-Oriented Software Engineering: A use case driven approach

1996 – Robert C. Martin – Granularity

2003 – Eric Evans – Domain-Driven Design: Tackling Complexity in the Heart of Software

2014 – Eric Evans – Domain-Driven Design Reference

Đây là bài viết trong loạt bài viết về “Tổng quan về sự phát triển của kiến trúc phần mềm“. Đây là loạt bài viết chủ yếu giới thiệu về một số mô hình kiến trúc phần mềm hay nói đúng hơn là sự phát triển của chúng qua từng giai đoạn, qua đó giúp chúng ta có cái nhìn tổng quát, up-to-date và là roadmap để bắt đầu hành trình chinh phục (đào sâu) thế giới của những bản thiết kế với vai trò là những kỹ sư và kiến trúc sư phần mềm đam mê với nghề.

Bài viết được tham khảo từ:

https://herbertograca.com/2017/09/07/domain-driven-design/

Tổng hợp bởi edwardthienhoang

Software Architecture Timeline

Và điều đầu tiên đề cập đến trong bài viết này tôi gọi là “lược sử (timeline) về kiến trúc phần mềm” nơi tác giả liệt kê các mốc chính về các mô hình lập trình, các kiểu thiết kế nổi bật từ thuở sơ khai đến hiện tại, có thể kể đến như:

 

  • 1950s

    •   Non-structured Programming
    • ~1951 – Assembly
  • 1960s

    •   Structured Programming
    •   Layering: 1 tier with the UI, Business Logic and Data Storage
    • ~1958 – Algol
  • 1970s

    •   Procedural / Functional Programming
    • ~1970 – Pascal
    • ~1972 – C
    •   1979 – Model-View-Controller
  • 1980s

    •  Object Oriented Programming (first thoughts were in the late 1960s, though)
    •   Layering: 2 tier, the 1st tier with the UI, the 2nd tier with Business Logic and Data Storage
    • ~1980 – C++
    •   CORBA – Common Object Request Broker Architecture (though the first stable version was only out in 1991, the first usages were during the 1980s)
    • ~1986 – Erlang
    • ~1987 – Perl
    •   1987 – PAC aka Hierarchical Model-View-Controller
    •   1988 – LSP (~SOLID)
  • 1990s

    •   Layering: 3 tier, the 1st tier with the UI, the 2nd tier Business Logic (and the UI presentation logic in case of a browser as client), the 3rd tier with the Data Storage
    • ~1991 – Message Bus
    • ~1991 – Python
    •   1992 – Entity-Boundary-Interactor Architecture aka EBC aka EIC
    • ~1993 – Ruby
    • ~1995 – Delphi, Java, Javascript, PHP
    •   1996 – Model-View-Presenter
    •   1996 – OCP, ISP, DIP (~SOLID), REP, CRP, CCP, ADP
    •   1997 – SDP, SAP
    • ~1997 – Aspect Oriented Programming
    • ~1997 – Web Services
    • ~1997 – ESB – Enterprise Service Bus (although the book that coined the term was published in 2004, the concept was already used before)
  • 2000s

    • 2002 – SRP (~SOLID)
    • 2003 – Domain-Driven Design
    • 2005 – Model-View-ViewModel
    • 2005 – Ports & Adapters Architecture aka Hexagonal Architecture
    • 2006? – CQRS & ES (Command Query Responsibility Segregation & Event Sourcing)
    • 2008 – Onion Architecture
    • 2009 – Microservices (at Netflix)
  • 2010s

    • 2010 – Data-Context-Interaction Architecture 
    • 2012 – Clean Architecture
    • 2014 – C4 Model

Giống như ông bà ta thường nói, không biết lịch sử nước nhà thì cũng như là người mất gốc. Bằng cách nắm được timeline của một việc nào đó sẽ giúp chúng ta có cái nhìn toàn cảnh đầu tiên nhất về sự việc đó, bằng không sẽ rất khó khăn khi liên kết các mắt xích của vấn đề lại với nhau.

Trong bài tiếp theo, chúng ta sẽ tìm hiểu kỹ hơn về các khái niệm căn bản trong kiến trúc phần mềm. Sẽ thật là thiếu sót khi làm về một việc nào đó mà không thể đưa ra định nghĩa rõ ràng về nó phải không nào. See you then.

Đây là bài viết trong loạt bài viết về “Tổng quan về sự phát triển của kiến trúc phần mềm“. Đây là loạt bài viết chủ yếu giới thiệu về một số mô hình kiến trúc phần mềm hay nói đúng hơn là sự phát triển của chúng qua từng giai đoạn, qua đó giúp chúng ta có cái nhìn tổng quát, up-to-date và là roadmap để bắt đầu hành trình chinh phục (đào sâu) thế giới của những bản thiết kế với vai trò là những kỹ sư và kiến trúc sư phần mềm đam mê với nghề.

Bài viết được tham khảo từ:
https://herbertograca.com/2017/07/03/the-software-architecture-chronicles/

Tổng hợp bởi edwardthienhoang