Cùng điểm qua 1 số nguyên tắc đóng gói (packaging principle) của Robert C. Martin:
Nếu tôi hỏi bạn: “tại sao phải phân loại các class vào các package” bạn có thể sẽ trả lời: “để dễ quản lý”. Tất nhiên câu trả lời này là đúng, nhưng chưa giúp ích được gì nhiều. Và câu chuyện của chúng ta sẽ còn kéo dài hơn thế.
Phân loại là công việc mà chúng ta rất hay làm trong đời sống. Ví dụ, một người giữ xe thường sắp xe ga, xe số riêng, ngoài mục đích dễ tìm xe còn mục đích kinh tế: xe ga đắt tiền nên sẽ cần phải để mắt hơn.
Công việc phát triển phần mềm cũng tương tự như vậy. Nhiệm vụ của package không phải là để dễ tìm class. Tất nhiên với package thì việc tìm class có thể tiện lợi hơn đôi chút nhưng nó không quá quan trọng, nhất là các IDE ngày nay đều hỗ trợ tìm class trong nháy mắt. Một phần mềm hạng trung khoảng trăm class đã có thể rất phức tạp. Sở dĩ chúng phức tạp vì chúng có những mối quan hệ phụ thuộc lẫn nhau. Kiểm soát tốt những sự phụ thuộc này là mục tiêu sống còn của phần mềm. Công việc này quả thực là rất khó khăn với một số lượng lớn class. Do đó, chúng ta cần gom class vào các package. Số lượng package hiển nhiên sẽ ít hơn rất nhiều so với số lượng class. Việc kiểm soát sự phụ thuộc giữa một số lượng ít các package rõ ràng là thuận tiện hơn. Hơn nữa nó cũng giúp chúng ta có một cái nhìn khái quát về tổng thể phần mềm hơn là đi sâu vào class, vì class là quá nhỏ, quá chi tiết.
Nhưng chia class vào package như thế nào cho hiệu quả?
Granularity. Mỗi package cần phải đủ nhỏ vì nếu nó quá to thì sẽ rất tội cho những package khác chỉ phụ thuộc vào một phần của nó.
Stability. Mỗi khi có sự thay đổi xảy ra thì số lượng package bị tác động càng ít càng tốt.
Uncle Bob đưa ra 6 nguyên lý để đảm bảo tính hiệu quả của việc chia class.
The granule of reuse is the granule of release.
Một phần mềm tốt thì cần phải dễ reuse. Một người nào đó muốn reuse các class của chúng ta, nhưng vì các class này lại được đóng vào các package, cho nên người ấy sẽ phải phụ thuộc vào nguyên các package ấy. Số lượng các class dư thừa (đối với người ấy) cần phải ít nhất có thể. Lý tưởng là zero (equivalence). Đây chính là nội dung của nguyên lý Reuse/Release Equivalence.
The classes in a component are reused together. If you reuse one of the classes in a component, you reuse them all.
Cái người mà đang muốn reuse các class của chúng ta, anh ấy muốn số lượng package phải phụ thuộc là ít nhất có thể. Dễ thấy, muốn đáp ứng điều này, chúng ta nên nhét những class muốn được reuse cùng nhau vào chung một package, càng nhiều càng tốt. Lý tưởng là package đầy ắp các class muốn reuse cùng nhau.
Chúng ta có thể thấy nguyên lý Reuse/Release Equivalence và Common Reuse liên quan với nhau rất chặt chẽ. Thỏa mãn được nguyên lý này sẽ thường kéo theo thỏa mãn nguyên lý kia.
Bạn có thể hỏi: “làm sao biết trước cái người đó là ai và anh ta muốn reuse các class nào?” Good question! Và câu trả lời là kinh nghiệm! Chỉ có kinh nghiệm mới giúp chúng ta lường trước được những kiểu cách reuse mà thôi.
The classes in a component should be closed together against the same kinds of changes. A change that affects a component affects all the classes in that component and no other components.
Bắt đầu từ nguyên lý này, chúng ta sẽ không nói về reuse nữa, mà sẽ nói về sự thay đổi. Một lần nữa, kinh nghiệm có vai trò tuyệt đối ở đây bởi chúng ta cần phải đoán trước những kiểu thay đổi có thể xảy ra. Đoán được chúng rồi, ta sẽ thiết kế package để khi có thay đổi xảy ra, thì số lượng package bị thay đổi là ít nhất có thể. Lý tưởng là một package cho mỗi một sự thay đổi.
Muốn làm được như thế, thì package cần phải Single Responsibility, như class vậy.
Allow no cycles in the component dependency graph.
Nguyên lý này là hiển nhiên và tương tự như với class. Để xuất hiện một chu trình của những sự phụ thuộc là điều tối kị, vì một thay đổi ở một package sẽ kéo theo tất cả các package khác thuộc chu trình bị thay đổi theo.
Depend in the direction of stability.
Một package hay biến động thì nên hạn chế để package khác phụ thuộc vào nó. Nói cách khác, các package nên phụ thuộc theo chiều hướng từ biến động tới ổn định.
A component should be as abstract as it is stable.
Nguyên lý này thực chất là bổ trợ thêm cho nguyên lý 5. Một package ổn định thì nên có tính trừu tượng cao (chứa nhiều abstract class). Ngược lại, một package hay biến động thì không nên trừu tượng để có thể dễ thay đổi.
Tôi đã nghe và đã có rất nhiều cuộc thảo luận về “package by layer” và “package by feature” trong vài tuần qua. Cả hai đều có những lợi ích của chúng nhưng có một cách tiếp cận lai mà bây giờ tôi sẽ nói đến gọi là “package by component”.
Giả sử rằng chúng ta đang xây dựng một ứng dụng web dựa trên Web-MVC pattern. Đóng gói codebase theo layer thường là cách tiếp cận mặc định bởi vì sau khi tất cả những gì trong sách, tutorial và framework mẫu cho chúng ta biết. Ở đây chúng tôi đang tổ chức mã bằng cách nhóm những thứ cùng loại với nhau.
Có 3 layer được đóng gói theo package đó là Controller ở trên cùng, dưới nó là Service layer, và cuối cùng là Data layer. Layer là cách gom nhóm codebase theo chức năng kỹ thuật. Các thuật ngữ như “tách biệt các mối quan tâm (separation of concerns)” được đưa ra xung quanh để biện minh cho cách tiếp cận này và kiến trúc phân lớp thường được coi là “good thing”. Nếu cần phải thay đổi cơ chế truy cập dữ liệu? Không có vấn đề, mọi thứ đều ở cùng một nơi. Mỗi layer có thể được kiểm thử riêng biệt với những thành phần khác khác xung quanh nó, sử dụng các kỹ thuật mocking thích hợp, v.v. Vấn đề với các kiến trúc phân lớp là chúng thường biến thành một đống bùn lớn (big ball of mud), bởi vì trong Java, bạn tạo các public class cho phép truy cập công khai từ bên ngoài.
Thay vì tổ chức mã theo chiều ngang (horizontal slicing), package by feature có cách làm ngược lại bằng cách tổ chức mã theo chiều thẳng đứng (vertical slicing).
Bây giờ mọi thứ liên quan đến một tính năng duy nhất (hoặc bộ tính năng) nằm ở một nơi. Bạn vẫn có thể có một kiến trúc phân lớp, nhưng các layer nằm bên trong các gói tính năng. Nói cách khác, phân lớp là cơ chế tổ chức thứ cấp. Lợi ích thường được trích dẫn là “dễ dàng điều hướng codebase khi bạn muốn thực hiện thay đổi đối với một tính năng”, nhưng đây là điều nhỏ nhoi nhờ sức mạnh của các IDE hiện đại.
Bây giờ chúng ta có thể ẩn đi các class cụ thể bên trong và giữ cho chúng ra khỏi tầm nhìn từ phần còn lại của codebase. Câu hỏi là điều gì sẽ xảy ra khi tính năng mới C cần truy cập dữ liệu từ các tính năng A và B? Một lần nữa, trong Java, bạn sẽ cần bắt đầu tạo các public class cho phép truy cập công khai từ bên ngoài các gói và quả bong bóng lớn sẽ lại xuất hiện.
Đây là một hướng tiếp cận lai với mục tiêu chính là làm tăng tính module hóa và phong cách mã hóa rõ nét về mặt kiến trúc.
Các tiền đề cơ bản ở đây là tôi muốn codebase của tôi được tạo thành từ một số coarse-grained (phức hợp) components, với một số presentation layer (giao diện web, giao diện người dùng trên máy tính để bàn, API, ứng dụng độc lập, v.v …) được xây dựng phía trên cùng của hệ thống. Một “component” theo nghĩa này là sự kết hợp của business logic và data access liên quan đến một điều cụ thể (ví dụ domain concept, bounded context, v.v …). Như đã mô tả trước đây, ứng với mỗi component, tôi tạo ra các public interface ra bên ngoài và che giấu các thể hiện detail bên trong package, ví dụ như là data access code. Nếu tập tính năng mới C cần truy cập dữ liệu liên quan đến A và B, nó sẽ bị buộc phải sử dụng các public interface của các component A và B. Không được phép truy cập trực tiếp vào lớp detail bên trong và bạn có thể thực hiện điều này bằng cách sử dụng access modifiers của Java đúng cách. Một lần nữa, “kiến trúc phân lớp” là một cơ chế tổ chức thứ cấp. Để làm việc này, bạn phải dừng sử dụng từ khoá public một cách mặc định. Cấu trúc này đưa ra một số câu hỏi thú vị về kiểm tra, không chỉ về cách chúng tôi mock các đoạn mã data access để tạo ra các “unit test” nhanh.
Câu trả lời ngắn gọn là đừng bận tâm, trừ khi bạn thực sự cần. Tôi đã nói về và viết về điều này trước đây, nhưng architecture và testing có liên quan với nhau. Thay vì là các mô hình kiểm thử điển hình (unit test thì rất nhiều, integration test thì ít hơn và thậm chí các UI test cũng ít), hãy xem xét điều dưới đây.
Tôi đang tránh để không sử dụng thuật ngữ “Unit testing” bởi vì mỗi người đều có cái nhìn khác nhau về độ lớn của một unit. Thay vào đó, tôi đã thông qua một chiến lược mà một số class có thể và nên được kiểm thử một cách riêng biệt. Điều này bao gồm những thứ như lớp domain classes, utility classes, web controllers (sử dụng mocked components), v.v … Sau đó, có thể dễ dàng để test các component thông qua các public interface. Nếu tôi có một component lưu trữ dữ liệu trong một cơ sở dữ liệu MySQL, tôi muốn kiểm thử tất cả mọi thứ từ public interface ngay trở vô cơ sở dữ liệu MySQL. Đây thường được gọi là “integration test”, nhưng một lần nữa, thuật ngữ này có nghĩa khác nhau đối với những người khác nhau. Tất nhiên, coi các component như là một hộp đen là dễ dàng hơn nếu tôi có quyền kiểm soát mọi thứ bên trong nó. Nếu bạn có một component đang gửi tin nhắn không đồng bộ hoặc sử dụng dịch vụ bên thứ ba, có thể bạn sẽ cần phải thêm các điểm dependency injection (ví dụ: ports and adapters) để kiểm tra component này một cách đầy đủ, nhưng hãy coi đây là ngoại lệ. Tất cả điều này vẫn còn áp dụng nếu bạn đang xây dựng hệ thống theo hướng microservice. Bạn có lẽ sẽ có một số bài kiểm tra cấp thấp, hy vọng một loạt các bài kiểm thử dịch vụ nơi bạn đang thử nghiệm các dịch vụ microservices thông public interface của chúng, và một số thử nghiệm hệ thống chạy các kịch bản từ đầu đến cuối. Oh, và bạn vẫn có thể viết tất cả điều này theo phong cách test-frst, TDD nếu đó là cách bạn làm việc.
Tôi đang sử dụng chiến lược này cho một số hệ thống mà tôi đang xây dựng và dường như nó hoạt động tốt. Tôi có một codebase tương đối đơn giản, sạch sẽ và trung thực với những dependency dễ hiểu, dễ maintain. Chiến lược này cũng làm cầu nối khoảng cách giữa mô hình, trong đó codebase thực sự phản ánh ý định của kiến trúc. Nói cách khác, chúng ta thường vẽ các components trên bảng khi thảo luận về kiến trúc, nhưng những component này khó tìm thấy trong codebase. Packaging code by layer là một lý do chính tại sao không phù hợp giữa thiết kế và codebase. Những người quen thuộc với mô hình C4 của tôi có lẽ sẽ nhận thấy việc sử dụng thuật ngữ “class” và “component”. Đây không phải là sự trùng hợp ngẫu nhiên. Kiến trúc và kiểm có liên quan nhiều hơn chúng ta đã thừa nhận trong quá khứ.
Đâ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://edwardthienhoang.wordpress.com/2018/01/12/nguyen-ly-thiet-ke-package/
https://katatunix.wordpress.com/2015/02/01/nguyen-ly-thiet-ke-package/
Đọc thêm:
2008 – Johannes Brodwall – Package by feature
2012 -Johannes Brodwall – How Changing Java Package Names Transformed my System Architecture
2012 – sivaprasadreddy.k – Is package by feature approach good?
2013 – Lahlali Issam – Lessons to Learn from the Hibernate Core Implementation
2013 – Manu Pk – Package your classes by Feature and not by Layers
2015 – Simon Brown – Package by component and architecturally-aligned testing
2015 – César Ferreira – Package by features, not layers
2017* – javapractices.com – Package by feature, not layer
1996 – Robert C. Martin – Granularity
1997 – Robert C. Martin – Stability
2009 – 500internalservererror – What do low coupling and high cohesion mean? What does the principle of encapsulation mean?
2011 – Robert C. Martin – Screaming Architecture
You need to login in order to like this post: click here
YOU MIGHT ALSO LIKE