Architecture

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

2 thoughts on “Thực hành implement Clean Architecture”

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.