Nhái Cốm Blog

I love you just the way you are

0R8A4000_

Nguyên tắc minh bạch

Leave a comment

Tác giả: Giovanni Scerra
Bài gốc: The Rule of Transparency

(lược dịch)

Giới thiệu

Nguyên tắc minh bạch là một đặc điểm quan trọng trong thiết kế phần mềm, xuất hiện nhiều thập kỉ trước từ cộng đồng Unix. Khái niệm này được giới thiệu cũng như mô tả bởi lập trình viên ủng hộ phần mềm mở Eric Raymond, người đầu tiên nghiên cứu cách hoạt động của các dự án mã nguồn mở cũng như khả năng đưa ra những phần mềm đáng tin cậy và dễ bảo trì bằng sự tham gia của đội ngũ lập trình viên trên toàn cầu.

Khái quát

Bài viết này nhằm mục đích giới thiệu về tính minh bạch trong thiết kế phần mềm và sẽ chỉ đề cập đến bề nổi những tác động của nó. Mục đích chính nhằm cung cấp cho người đọc định hướng nhất định về chủ đề cùng với những kĩ thuật và chiến lược thường sử dụng để tạo ra một thiết kế minh bạch.

Nguyên tắc minh bạch là gì?

Nguyên tắc minh bạch là sự kết hợp của hai phẩm chất có thể quan sát được của thiết kế, bao gồm:

“Một hệ thống phần mềm là minh bạch khi bạn có thể nhìn vào và hiểu ngay những gì hệ thống đang thực hiện và cách thực hiện của hệ thống. Điều này có thể được phát hiện khi hệ thống có thiết bị theo dõi và hiển thị trạng thái bên trong. Vì vậy, chương trình của bạn không chỉ thực thi tốt mà còn có thể nhìn thấy cách nó hoạt động tốt như thế nào.”

Thật không may, trong phần mềm thương mại, nguyên tắc minh bạch rất hiếm khi được xem xét trên toàn cục bởi lý do bảo mật hay những lo ngại liên quan đến pháp lý. Tuy nhiên, mục tiêu ban đầu của nó là để tạo ra môi trường thuận lợi giúp theo dõi, đảm bảo chất lượng phần mềm ngay cả trong những tình huống khó khăn.

Tại sao sự minh bạch lại quan trọng

Các vấn đề trong thiết kế và những giải pháp rối rắm thông thường ẩn đằng sau sự phức tạp. Mặc dù sự minh bạch bản thân nó không đảm bảo cho một thiết kế tốt, khả năng hình dung thiết kế rõ ràng ngay từ đầu là yếu tố cực kì quan trọng giúp tránh bỏ sót các vấn đề.

Mã nguồn minh bạch là bước đầu tiên hướng tới mục tiêu chất lượng; cũng là yêu cầu cho các phần mềm có khả năng tồn tại lâu hơn trước những thay đổi về chức năng, công nghệ và tổ chức.

Thiết kế phần mềm minh bạch

Tính minh bạch là thiết kế phần mềm sao cho có thể mô tả rõ ràng cách thức hoạt động của mã nguồn nhằm mục đích kiểm tra, hiểu, theo dõi và gỡ lỗi. Trước khi đi sâu vào chi tiết, có một vài điểm đáng được đề cập đến:

  1. Tính minh bạch không phải đi ngược lại tính đóng gói trong thiết kế hướng đối tượng. Trên thực tế, các chi tiết thực thi bên trong có thể được ẩn đi mà vẫn có khả năng kiểm tra. Tính minh bạch không những có thể tồn tại song song với tính đóng gói mà trong nhiều trường hợp, nó còn hỗ trợ giúp tách bạch các thành phần với nhau.
  2. Tính minh bạch là mục tiêu được xác định rõ ràng nhưng nhiều khía cạnh khác trong thiết kế phần mềm có thể giúp hoàn thành tiêu chí này. Chúng ta sẽ xem xét các khía cạnh này (tập trung chủ yếu vào các ngôn ngữ hướng đối tượng) và tìm hiểu xem làm thế nào chúng có thể trực tiếp đóng góp vào việc đạt được tính minh bạch.

Sự đơn giản

Mã lệnh đơn giản góp phần tạo ra tính minh bạch bởi nó dễ thực hiện và chúng ta có lý do để thực hiện điều này. Gợi ý nhỏ cho sự đơn giản là thiết kế các thành phần nhỏ, tập trung và độc lập rồi kết nối chúng lại nhờ các lớp mã lệnh gắn kết. Đây là một lời khuyên tốt cho dù còn nhiều vấn đề khác sẽ được đề cập đến trong bài viết. Trong một lần nói chuyện, Rich Hickey (người sáng tạo ra ngôn ngữ Clojure) đã nhấn mạnh một số điểm nổi bật của sự đơn giản:

  1. Đơn giản không có nghĩa là dễ dàng hay quen thuộc. Không dễ để đạt đến sự đơn giản vì nó cần đến những kĩ năng phân tích tuyệt vời. Dù vậy, nó sẽ giúp chúng ta trong những thay đổi và xác minh tính đúng đắn. Sự đơn giản không liên quan đến việc giúp lập trình viên cảm thấy thoải mái hay khiến công việc của họ trở nên dễ dàng hơn.
  2. Tính đơn giản không liên quan đến việc chúng ta thông minh hay ngu ngốc nếu đem so với sự phức tạp chúng ta có thể tạo ra.
  3. Đơn giản không liên quan quá nhiều đến số lượng của các thành phần của một hệ thống mà liên quan nhiều hơn đến cách thức các thành phần tương tác với nhau. Các thành phần càng gắn kết chặt chẽ với nhau, chúng càng phải ghi nhớ rằng đây là điểm mấu chốt hạn chế khả năng hiểu điều gì đang diễn ra với mã lệnh, chưa tính đến việc phát sinh thêm nhiều thay đổi.

 
Vậy những nguyên tắc và kỹ thuật cụ thể nào có thể giúp chúng ta trong việc thiết kế mã lệnh đơn giản?

Dưới đây là một số thủ thuật:

Thủ thuật trong kĩ thuật Lợi ích
Nguyên tắc trách nhiệm duy nhất
(Single Responsibility Principle)
Tính mạch lạc và nhất quán giúp suy nghĩ tách bạch về các thành phần
Khái quát – Abstraction (các giao diện) Giảm sự phụ thuộc giữa các thành phần
Bộ các quy tắc (rule engine), tính đa hình (polymorphism) và các thuật toán tổng quát Có thể thay thế những đặc tả, điều kiện logic phức tạp
Ưu tiên sự phức tạp ở dữ liệu thay vì mã lệnh hay các khai báo xử lý dữ liệu (LINQ, lambda expressions, …) Xử lý và nghĩ về dữ liệu sẽ dễ dàng hơn khi phải làm với mã lệnh
Hàng đợi thông báo (Message queue), mẫu đăng ký (subscriber pattern), mẫu xuất bản (publisher pattern) Nhằm loại bỏ các tương tác phức tạp giữa các thành phần và đưa ra kênh giao tiếp đơn giản, ít phụ thuộc hơn
Mô hình phẳng và tường minh thay vì phân cấp thừa kế Chuỗi thừa kế nhiều tầng làm mã lệnh trở nên khó theo dõi và gỡ lỗi. Trong thiết kế, bạn nên đặt câu hỏi cho mình khi dùng tính kế thừa chỉ với mục đích sử dụng lại mã lệnh.
Chi tách / tách bạch toàn bộ các yếu tố cấu thành (tập tin, cơ sở dữ liệu, bộ nhớ chia sẻ, ngày và giờ, …) trong logic thực thi Việc trộn lẫn các yếu tố cấu thành cũng như logic có thể gây ra những tác dụng phụ, từ đó làm tăng tính phức tạp
Tạo sự thống nhất bằng cách tuân thủ các quy định, quy ước và mẫu (pattern) trong toàn bộ hệ thống Tính nhất quán làm giảm số lượng các vấn đề chúng ta phải xem xét

Cuối cùng, Domain Driven Design (DDD) dạy chúng ta một bài học vô cùng quan trọng: sự phức tạp không phải luôn luôn gây ra bởi các yếu tố kỹ thuật. Đặc biệt, có hai khái niệm cơ bản của DDD giúp mã lệnh trở nên rõ ràng và dễ hiểu hơn:

  1. Áp dụng chung một ngôn ngữ phổ biến, một thuật ngữ rõ ràng giữa những người làm kỹ thuật (sử dụng cả trong mã lệnh) và những người làm kinh doanh.
  2. Đầu tư thời gian vào việc phân tích cũng như thăm dò vấn đề để xây dựng các mô hình phần mềm trong sáng, có khả năng phản ánh hiệu quả các khía cạnh kinh doanh cần quản lý.

Có một ý kiến thú vị từ lời khuyên của Eric Raymond mà tôi thấy rất hữu ích rằng: đừng quá thông minh.

Khi thiết kế phần mềm, hãy bỏ đi cái tôi của mình. Thế giới có thể sống tốt mà không cần đến một núi những vấn đề phức tạp không cần thiết được tạo ra bởi những lập trình viên và kiến trúc sư nhằm minh họa những thủ thuật hay ho của họ.

Khả năng dự đoán

Chúng ta không thể ngay lập tức nhìn ra mã lệnh làm gì mà không dựa trên thực tế rằng hành vi của mã lệnh có thể dự đoán được. Đôi khi chúng ta bị hấp dẫn với việc “hoàn hành công việc” để rồi bỏ qua những yêu cầu hợp lý từ các lập trình viên khác (đôi khi là từ chính chúng ta) trong việc đọc và sử dụng mã lệnh.

Làm thế nào để người khác có thể dễ dàng dự đoán mã lệnh của chúng ta? Dưới đây là một vài lời khuyên:

Tiến trình có khả năng dự đoán

Một điểm quan trọng của khả năng dự đoán có liên quan đến thứ tự của các hoạt động, đặc biệt khi thiết kế API (Application Programming Interface) và giao diện. Để làm cho các tiến trình có khả năng dự đoán ​​được, có thể sử dụng hai quy tắc sau đây:

  1. Nếu một thành phần đưa ra nhiều hoạt động nhưng không có gì bắt chúng phải được gọi theo một thứ tự xác định, bạn có thể gọi các phương thức theo thứ tự bất kỳ.
  2. Nếu một thành phần đưa ra nhiều hoạt động cần được gọi theo một thứ tự nhất định, thứ tự đó cần yêu cầu đầu vào của một hoạt động là đầu ra của hoạt động đứng trước nó. Hoặc thứ tự đó cần sử dụng thành phần hiện tại dưới dạng mới trong quá trình thực hiện các lời gọi.

Đôi khi chúng ta có thể đoán rằng những quy tắc này đã bị vi phạm khi hậu tố hoặc tiền tố của tên phương

thức ngầm định cho thứ tự gọi (ví dụ như Init, Cleanup, Load, Unload, Begin, End, Open, Close, …). Dấu hiệu khác của một tiến trình không thể dự đoán là khi một class đưa ra quá nhiều phương thức nhưng không có dữ liệu trả về ngoài việc trạng thái bên trong nó bị thay đổi.

Logic có khả năng dự đoán

Thông thường, chúng tôi dành phần lớn thời gian cho những thay đổi ở phần logic lõi trong ứng dụng, cũng là nơi khó nhận ra trục trặc nhất. Khả năng dự đoán có giá trị nhất ở đây và nói chung cũng không quá khó để đạt được khả năng này nếu nó được xem xét từ giai đoạn thiết kế.

  1. Một trong những nguyên tắc nổi bật nhất cho khả năng dự đoán về mặt logic là tính tham chiếu minh bạch. Một hàm có thể tham chiếu một cách minh bạch khi mà bất kì thời điểm nào, nếu bạn truyền vào dữ liệu giống nhau, nó sẽ trả về cũng một kết quả. Việc không phụ thuộc vào thời gian này tạo sự tin tưởng đối với bất kỳ lập trình viên nào cần hiểu và thay đổi mã lệnh bằng cách loại bỏ những phát sinh không mong muốn cũng như loại bỏ những chỗ phức tạp ngoài tầm kiểm soát; những thứ luôn không đi kèm với tính ổn định.
  2. Một kĩ thuật lập trình hướng đối tượng (Object Oriented Programming – OOP) hiệu quả mang lại khả năng dự đoán logic là sử dụng những đối tượng bất biến (không thay đổi). Một đối tượng bất biến sẽ lưu trong nó trạng thái chỉ đọc được xác định tại thời điểm nó được tạo ra. Giá trị của trạng thái này không bị thay đổi trong toàn bộ thời gian đối tượng đó được sử dụng. Hệ quả là những đối tượng bất biến không cung cấp khả năng thay đổi dữ liệu và không đưa ra bất kỳ phương thức nào có thể thay đổi trạng thái bên trong nó. Nếu một trạng thái mới cần phải tạo ra, đối tượng bất biến cũ phải bị hủy bỏ để tạo ra một đối tượng bất biến khác với trạng thái mới. Việc kiểm tra một đối tượng bất biên thường rất đơn giản do nó chỉ có thể thực hiện trong quá trình tạo đối tượng. Các đối tượng bất biến mặc nhiên là thread-safe (có thể được sử dụng bởi nhiều luồng chương trình – thread – khác nhau mà không gây sai lệch về dữ liệu) và việc không có khả năng thay đổi trạng thái làm giảm đáng kể nguy cơ phát sinh các lỗi logic.
Tương tác có khả năng dự đoán

Tương tác giữa các thành phần có thể không dự đoán trước được vì nhiều lý do, hầu hết trong số đó liên quan đến những lựa chọn thiết kế tồi:

  1. Mã lệnh chung được đặt vào những thành phần cụ thể hoặc ngược lại. Do đó, các thành phần có thể được gọi từ những nơi khó ngờ đến nhất: mã lệnh chung cần phải được tái cấu trúc để trở nên tách biệt và có thể được sử dụng lại dễ dàng bởi các thành phần nhằm hỗ trợ những chức năng cụ thể thay vì thực hiện chúng.
  2. Có quá nhiều cách để đạt cùng một mục đích, có quá nhiều cách để thay đổi cùng một biến, có quá nhiều phương thức overload (overload method): những thành phần cấp thấp có API phức tạp, được thiết kế để cung cấp khả năng sử dụng linh hoạt, không nên được sử dụng trực tiếp nhằm ràng buộc việc sử dụng và truy cập một thành phần chỉ có thể thực hiện trong từng tình huống cụ thể khi cần thiết.
  3. Thiếu các lớp và các dịch vụ có kiến trúc đóng được thiết kế tốt. Một đặc điểm cơ bản của một lớp có kiến trúc đóng được thiết kế tốt là nó có thể giao tiếp với các lớp bên trên cũng như phía dưới sử dụng các API cấp cao mà chỉ trao đổi những cấu trúc dữ liệu ít ràng buộc thông qua bất kỳ định dạng nào tiện lợi nhất (XML, JSON, POJO / POCOS, …).

Cộng tác minh bạch

Nhìn vào một thành phần trong ứng dụng của mình, bạn mất bao lâu để tìm ra những thành phần mà nó phụ thuộc? Và bạn sẽ phải đọc bao nhiêu mã lệnh trước khi tìm ra câu trả lời?

Khi quan hệ cộng tác bị chôn vùi bởi những chi tiết thực thi, một phần thông tin quan trọng bị che lấp, bắt buộc chúng ta phải đào sâu vào đống mã lệnh để hiểu tương tác của một thành phần với phần còn lại của hệ thống.

  1. Dependency injection là mẫu thiết kế hiệu quả nhất cho việc phát hiện quan hệ cộng tác của các lớp (class) bằng cách làm rõ những lớp này qua tham số của hàm khởi tạo (constructor) hay phương thức. Kết quả là chỉ cần nhìn vào mô tả hàm và các API, chúng ta có thể hiểu nhiều hơn cách một lớp làm việc cũng như điều kiện cần để sử dụng, tái sử dụng hay kiểm tra lớp đó.
  2. Một nguyên tắc cơ bản khác đảm bảo tính cộng tác minh bạch là luật Demeter (Law of Demeter), còn gọi là luật “không nói chuyện với người lạ – don’t talk to strangers”. Nguyên tắc này ngăn cấm quan hệ cộng tác không trung thực như không sử dụng trực tiếp mà sử dụng làm trung gian cho các thành phần khác; qua đó làm lu mờ đi mối quan hệ và tương tác giữa các thành phần.

Khai thác thông tin bên trong

Quy tắc minh bạch cũng nhấn mạnh chất lượng của khả năng khai phá thông tin, được coi như khả năng khai thác phần mềm nhằm cung cấp thông tin hữu ích về trạng thái bên trong khi phần mềm đang hoạt động; khả năng này giúp các lập trình viên có thể theo dõi và gỡ lỗi. Việc đầu tư thời gian và công sức vào khả năng khai thác thông tin bên trong có thể tiết kiệm rất nhiều thời gian khi cần xử lý các sự cố.

Dưới đây là một số gợi ý chủ đạo về chủ đề này:

  1. Thông báo lỗi chính xác: Một trong các mục tiêu đối với việc xử lý lỗi là cung cấp các thông báo lỗi có ý nghĩa, mang lại nhiều thông tin hữu ích có thể giúp chúng ta xác định nguyên nhân của vấn đề. Có rất nhiều thông báo lỗi thật sự không hữu ích (như thông báo lỗi con trỏ không tồn tại – null pointer exception). Do đó, nên xử lý các lỗi ngoại lệ (exception) càng sớm càng tốt để sau đó đưa ra một lỗi ngoại lệ mới với nhiều thông tin hữu ích, phù hợp với ngữ cảnh hiện tại hơn.
  2. Ghi lại chi tiết dữ liệu: Yêu cầu ứng dụng lưu lại các bước thực hiện quan trọng mà nó đang xử lý. Mô-đun cho phép ghi lại dữ liệu tốt sẽ cho phép xác định các mức độ ghi lại dữ liệu khác nhau (đặc biệt ở mức độ gỡ lỗi, mọi dữ liệu sẽ được ghi lại) và cho phép dễ dàng cấu hình thiết bị dùng để lưu dữ liệu được ghi lại. Thông thường, các tệp tin ghi lại dữ liệu này là cách duy nhất để phát hiện những trục trặc gây ra bởi vấn đề với dữ liệu và do đó, rất khó để tái hiện lại ở môi trường khác.
  3. Văn bản hóa: Hãy tạo ra thể hiện cho trạng thái và luồng chương trình ở hình thức mà con người có thể đọc được, từ đó có thể dễ dàng ghi vào các tệp tin hay màn hình văn bản. Ví dụ, tạo ra hàm override cho phương thức toString() của đối tượng để đưa ra trạng thái bên trong ở dạng tóm tắt có thể rất hữu ích khi gỡ lỗi với màn hình console, tránh tiêu tốn thời gian vào việc kiểm tra các đối tượng có cấu trúc phân cấp.
  4. Lựa chọn gỡ lỗi: Thêm chế độ gỡ lỗi vào ứng dụng sẽ cho phép bạn xâm nhập vào chương trình để cung cấp thêm thông tin cũng như khả năng xử lý sự cố. Ví dụ, chế độ gỡ lỗi có thể cho phép mô phỏng việc ký (sign) một ứng dụng web với các mức độ bảo mật khác nhau nhằm dễ dàng tái hiện lại các ngữ cảnh xác thực khác nhau. Ngoài ra, bạn có thể hiển thị thông tin gỡ lỗi trực tiếp trên giao diện đồ họa, cho phép thử nghiệm giao diện điều khiển (console)…

Tài liệu

Tài liệu là một nhân tố quan trọng của tính minh bạch; nhưng nếu không được thực hiện đúng, nó cũng có thể là nguyên nhân tiêu tốn một lượng thời gian khổng lồ, thậm chí có thể gây hiểu lầm và phản tác dụng. Tài liệu không phải chỉ cần hữu ích; nó còn phải dễ dàng cập nhập và bảo trì theo thời gian.

  1. Một số lập trình viết những dòng chú thích dài lê thê chỉ để tuân thủ những quy định của công ty. Những người khác thậm chí căm ghét và từ chối viết những dòng chú thích. Họ từ bào chữa rằng mã lệnh đã là đủ tốt và tự nó có thể giải thích việc nó thực hiện. Cả hai thái cực này đều không đúng. Chú thích là rất hữu ích để bổ sung cho những gì bản thân mã lệnh không thể diễn đạt rõ ràng và sự thật là không ai có thể hoàn toàn viết mã lệnh có thể tự giải thích ý nghĩa. Chú thích có giá trị khi phải cung cấp bản tóm tắt về mục đich của các lớp (class), giải thích cách giải quyết hay lịch sử của một vấn đề nhằm giúp hiểu rõ hơn về giải pháp. Chú thích cũng nói lên những phần mở rộng trong thiết kế, giả định rõ ràng về đầu vào và đầu ra các tham số hay đề xuất để cải thiện ứng dụng.
  2. Tài liệu bên ngoài cũng có thể hữu ích khi được viết theo văn phong thân thiện (như các bài hướng dẫn), không chính quy nhằm giúp các lập trình viên làm quen với việc sử dụng mã lệnh trước khi mở tệp tin.
  3. Kiểm tra tự động cũng là một dạng tài liệu tốt, giúp xác định các đặc tả (specification) chính thức và yêu cầu mã lệnh phải tuân theo những chi tiết trong đặc tả. Ngoài ra, nó cũng đồng thời cung cấp những ví dụ sử dụng rất hữu ích.

Văn hóa

Niềm đam mê, thái độ tích cực và một nền văn hóa cởi mở trong việc chia sẻ kiến thức là những yếu tố thiết yếu làm nên những sản phẩm phần mềm chất lượng cao. Việc tạo ra văn hóa thích hợp có thể rất khó khăn trong một số đội ngũ hoặc một số công ty: Các thay đổi cần được thực hiện từ từ và cẩn thận để đảm bảo rằng những giá trị cốt lõi được mọi người hiểu và chấp nhận. Nói rõ hơn, tính minh bạch yêu cầu lập trình viên có khả năng đưa ra và lắng nghe những lời chỉ trích mang tính xây dựng, đồng thời luôn sẵn sàng học hỏi lẫn nhau.

Trong bối cảnh văn hóa phù hợp, việc kiểm tra lại mã lệnh trở thành công cụ đào tạo chính theo hướng minh bạch: Chia sẻ mã lệnh để đọc và hiểu là bước kiểm tra cuối cùng để xác định việc đặt tên không tốt, những thiết kế cứng nhắc và các đặc điểm khác đã được đề cập ở trên; những vấn đề này thường khó nhìn ra thông qua mã lệnh.

Kết lạị, thái độ đúng có lẽ được mô tả tốt hơn qua vài dòng trong cuốn sách “Nghệ thuật lập trình Unix – The Art of Unix Programming”:

“[…] Bạn phải tin rằng thiết kế phần mềm là một công việc thủ công đáng giá bằng toàn bộ sự thông minh, sáng tạo và đam mê bạn có thể tụ hợp được. Nếu không, bạn sẽ không nhìn lại quá khứ theo hướng đơn giản, rập khuôn trong việc tiếp cận thiết kế và triển khai thực hiện; bạn sẽ vội vàng viết mã lệnh khi đáng ra phải suy nghĩ. Bạn cũng sẽ vô tình làm phức tạp hóa vấn đề khi nhẽ ra phải không ngừng đơn giản hóa. Và sau cùng, bạn sẽ tự hỏi tại sao mã lệnh của mình lại phình ra và việc gỡ lỗi lại khó khăn đến vậy. […]”

Tham khảo

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 )

Twitter picture

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

Facebook photo

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

Google+ photo

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

Connecting to %s