Đồ án: Tìm hiểu Design by Contract và Xây dựng Công cụ Hỗ trợ cho C#

Đồ án về Design by Contract: Tìm hiểu công nghệ và xây dựng công cụ hỗ trợ. Phân tích ưu điểm, nhược điểm và ứng dụng thực tế của Design by Contract.

Chuyên ngành

Công nghệ Phần mềm

Người đăng

Ẩn danh

Thể loại

Khóa luận cử nhân

2001 – 2005

114
2
0

Phí lưu trữ

35 Point

Mục lục chi tiết

LỜI CẢM ƠN

MỤC LỤC

LỜI NÓI ĐẦU

TỔNG QUAN

1. CHƯƠNG 1: GIỚI THIỆU VỀ EIFFEL

1.1. Giới thiệu

1.2. Design By Contract trong Eiffel

1.3. Các thao tác căn bản trên EiffelStudio

2. CHƯƠNG 2: MỘT SỐ CƠ CHẾ MANG LẠI TÍNH ĐÁNG TIN CẬY CHO PHẦN MỀM

3. CHƯƠNG 3: TÍNH ĐÚNG ĐẮN CỦA PHẦN MỀM

4. CHƯƠNG 4: BIỂU DIỄN MỘT ĐẶC TẢ

4.1. Những công thức của tính đúng đắn

4.2. Những điều kiện yếu và mạnh

5. CHƯƠNG 5: GIỚI THIỆU VỀ SỰ XÁC NHẬN TRONG VĂN BẢN CỦA PHẦN MỀM

6. CHƯƠNG 6: TIỀN ĐIỀU KIỆN VÀ HẬU ĐIỀU KIỆN

6.1. Lớp ngăn xếp

6.2. Tiền điều kiện

6.3. Hậu điều kiện

7. CHƯƠNG 7: GIAO ƯỚC CHO TÍNH ĐÁNG TIN CẬY CỦA PHẦN MỀM

7.1. Quyền lợi và nghĩa vụ

7.2. Những quyền lợi

7.3. Những nghĩa vụ

7.4. Nghệ thuật của sự tin cậy phần mềm: kiểm tra ít hơn, bảo đảm nhiều hơn

7.5. Những xác nhận không phải là một cơ chế kiểm tra đầu vào

8. CHƯƠNG 8: LÀM VIỆC VỚI NHỮNG XÁC NHẬN

8.1. Mệnh lệnh và yêu cầu

8.2. Lưu ý về những cấu trúc rỗng

8.3. Thiết kế tiền điều kiện: tolerant hay demanding?

8.4. Một môđun tolerant

9. CHƯƠNG 9: NHỮNG ĐIỀU KIỆN BẤT BIẾN CỦA LỚP

9.1. Định nghĩa và ví dụ

9.2. Định dạng và các thuộc tính của điều kiện bất biến của lớp

9.3. Điều kiện bất biến thay đổi

9.4. Ai phải bảo quản điều kiện bất biến?

9.5. Vai trò của những điều kiện bất biến của lớp trong kỹ thuật xây dựng phần mềm

9.6. Những điều kiện bất biến và hợp đồng

10. CHƯƠNG 10: KHI NÀO MỘT LỚP LÀ ĐÚNG?

10.1. Tính đúng đắn của một lớp

10.2. Vai trò của những thủ tục khởi tạo

10.3. Xem lại về mảng

11. CHƯƠNG 11: KẾT NỐI VỚI KIỂU DỮ LIỆU TRỪU TƯỢNG

11.1. So sánh đặc tính của lớp với những hàm ADT

11.2. Biểu diễn những tiên đề

11.3. Hàm trừu tượng

11.4. Cài đặt những điều kiện bất biến

12. CHƯƠNG 12: MỘT CHỈ THỊ XÁC NHẬN

13. CHƯƠNG 13: VÒNG LẶP CÓ ĐIỀU KIỆN BẤT BIẾN VÀ ĐIỀU KIỆN BIẾN ĐỔI

13.1. Vấn đề vòng lặp

13.2. Những vòng lặp đúng

13.3. Những thành phần của một vòng lặp đúng

13.4. Cú pháp của vòng lặp

14. CHƯƠNG 14: SỬ DỤNG NHỮNG XÁC NHẬN

14.1. Những xác nhận như một công cụ để viết phần mềm chính xác

14.2. Sử dụng những xác nhận cho việc viết tài liệu: thể rút gọn của một lớp đối tượng

15. CHƯƠNG 15: GIỚI THIỆU CÔNG CỤ XC#

15.1. XC# hoạt động như thế nào

15.2. Tiền điều kiện

15.3. Hậu điều kiện

15.4. Một số thuộc tính mà XC# qui ước sẵn

15.5. Ví dụ lớp Stack

16. CHƯƠNG 16: KẾT QUẢ THỰC NGHIỆM: CÔNG CỤ DCS

16.1. Nguyên lý làm việc

16.2. Chi tiết các lớp đối tượng

16.2.1. Màn hình Configuration

16.2.2. Lớp Extra

KẾT LUẬN

HƯỚNG PHÁT TRIỂN

TÀI LIỆU THAM KHẢO

Ý KIẾN CỦA GIÁO VIÊN PHẢN BIỆN

BẢNG CÁC HÌNH VẼ

Tóm tắt

I. Design by Contract DbC là gì Tổng quan và Lợi ích

Trong bối cảnh phát triển phần mềm ngày càng phức tạp, việc đảm bảo tính đúng đắn và tin cậy của phần mềm trở nên vô cùng quan trọng. Design by Contract (DbC), hay Lập trình theo hợp đồng, là một phương pháp tiếp cận giúp giải quyết vấn đề này bằng cách thiết lập các hợp đồng rõ ràng giữa các thành phần phần mềm. Theo Meyer (tác giả của Eiffel, ngôn ngữ lập trình tích hợp DbC), "phần mềm phải đáng tin cậy". DbC không chỉ là một kỹ thuật lập trình mà còn là một triết lý thiết kế, thúc đẩy việc xác định rõ ràng trách nhiệm và quyền lợi của từng thành phần. Về cơ bản, mỗi thành phần (lớp, phương thức) sẽ có một hợp đồng quy định những điều kiện cần đáp ứng trước khi thực hiện (Precondition), những điều kiện phải đảm bảo sau khi thực hiện (Postcondition), và những điều kiện luôn đúng trong suốt quá trình hoạt động (Invariant). Việc áp dụng Design by Contract mang lại nhiều lợi ích. Thứ nhất, nó giúp phát hiện lỗi sớm hơn trong quá trình phát triển, giảm thiểu chi phí sửa lỗi sau này. Thứ hai, nó cải thiện khả năng bảo trì và tái sử dụng mã nguồn, vì các hợp đồng đóng vai trò như tài liệu hóa chính thức về hành vi của các thành phần. Thứ ba, nó nâng cao tính tin cậy của phần mềm, vì các Assertion được kiểm tra một cách tự động, đảm bảo rằng các thành phần hoạt động đúng như mong đợi. Theo báo cáo từ các dự án sử dụng DbC, số lượng lỗi trong quá trình kiểm thử giảm đáng kể, đồng thời thời gian phát triển cũng được rút ngắn.

1.1. Định nghĩa cốt lõi của Design by Contract DbC

Khái niệm cốt lõi của Design by Contract xoay quanh việc xây dựng các hợp đồng giữa các đối tượng phần mềm. Một hợp đồng bao gồm ba thành phần chính: Precondition, Postcondition, và Invariant. Precondition xác định những điều kiện mà một phương thức phải được gọi với. Ví dụ, một phương thức tính căn bậc hai có thể yêu cầu Precondition là số đầu vào phải lớn hơn hoặc bằng 0. Postcondition xác định những điều kiện mà một phương thức phải đảm bảo sau khi thực thi. Ví dụ, phương thức tính căn bậc hai có thể đảm bảo Postcondition là kết quả trả về, khi bình phương, phải gần bằng số đầu vào. Invariant xác định những điều kiện luôn đúng cho một đối tượng trong suốt vòng đời của nó. Ví dụ, một lớp BankAccount có thể có Invariant là số dư tài khoản không bao giờ được âm. Việc vi phạm bất kỳ điều khoản nào trong hợp đồng sẽ dẫn đến một ngoại lệ, báo hiệu rằng có lỗi xảy ra trong quá trình thực thi. Nguyên tắc Defensive Programming được bổ trợ và phát triển mạnh mẽ hơn khi có DbC, giúp code đáng tin cậy hơn.

1.2. Lợi ích vượt trội của Design by Contract trong phát triển phần mềm

Áp dụng Design by Contract mang lại nhiều lợi ích quan trọng cho quá trình phát triển phần mềm. DbC giúp phát hiện lỗi sớm hơn trong vòng đời phát triển, giảm chi phí sửa lỗi. Khi một Precondition không được đáp ứng, lỗi sẽ được phát hiện ngay lập tức, thay vì lan truyền và gây ra những hậu quả khó lường. DbC cải thiện khả năng bảo trì và tái sử dụng mã nguồn. Các hợp đồng đóng vai trò như tài liệu hóa chính thức về hành vi của các thành phần, giúp các nhà phát triển dễ dàng hiểu và chỉnh sửa mã nguồn. DbC nâng cao tính tin cậy của phần mềm. Các Assertion được kiểm tra một cách tự động, đảm bảo các thành phần hoạt động như mong đợi, giảm thiểu rủi ro xảy ra lỗi trong quá trình vận hành. DbC cũng giúp cải thiện quá trình giao tiếp giữa các thành viên trong nhóm phát triển. Các hợp đồng tạo ra một ngôn ngữ chung để thảo luận về thiết kế và hành vi của phần mềm.

1.3. Design by Contract so với Lập trình phòng thủ Defensive Programming

Design by Contract (DbC) và Defensive Programming (Lập trình phòng thủ) là hai phương pháp tiếp cận khác nhau để xây dựng phần mềm tin cậy. Defensive Programming tập trung vào việc bảo vệ một thành phần phần mềm khỏi những đầu vào không hợp lệ và những tình huống bất thường. Các thành phần phần mềm thường xuyên kiểm tra đầu vào và thực hiện các biện pháp phòng ngừa để tránh những lỗi có thể xảy ra. Ngược lại, DbC tập trung vào việc thiết lập các hợp đồng rõ ràng giữa các thành phần phần mềm. Mỗi thành phần phần mềm chỉ chịu trách nhiệm thực hiện đúng hợp đồng của mình, và không cần phải kiểm tra tất cả các trường hợp có thể xảy ra. DbC khuyến khích việc phân chia trách nhiệm rõ ràng giữa các thành phần phần mềm, trong khi Defensive Programming có xu hướng tập trung trách nhiệm vào một thành phần. Defensive Programming có thể dẫn đến việc viết mã thừa và khó bảo trì, trong khi DbC có thể giúp giảm thiểu lượng mã cần viết và cải thiện khả năng bảo trì.

II. Precondition Postcondition và Invariant Cách sử dụng hiệu quả

Các thành phần cốt lõi của Design by Contract (DbC) bao gồm Precondition, Postcondition, và Invariant. Hiểu rõ và sử dụng hiệu quả các thành phần này là chìa khóa để tận dụng tối đa lợi ích của DbC. Precondition xác định những yêu cầu mà người gọi phương thức (client) phải đảm bảo trước khi gọi phương thức đó (supplier). Postcondition xác định những gì mà phương thức phải đảm bảo sau khi thực thi xong, giả sử Precondition đã được đáp ứng. Invariant xác định những điều kiện luôn đúng cho một đối tượng trong suốt vòng đời của nó, ngoại trừ trong quá trình thực thi một phương thức. Việc xác định chính xác Precondition, Postcondition, và Invariant đòi hỏi sự hiểu biết sâu sắc về yêu cầu của phần mềm và hành vi của các thành phần. Quá trình này có thể giúp phát hiện những mâu thuẫn và thiếu sót trong thiết kế, từ đó cải thiện chất lượng của phần mềm. Việc sử dụng các công cụ hỗ trợ DbC, chẳng hạn như các thư viện C# Contract, có thể giúp tự động kiểm tra các Assertion và phát hiện lỗi trong quá trình phát triển. Tài liệu gốc nhấn mạnh việc 'những client này phải bảo đảm một số điều kiện nhất định khi gọi một phương thức của một supplier gọi là tiền điều kiện (precondition); đáp lại, sau khi thực thi thủ tục, supplier phải đáp ứng một số điều kiện tương ứng gọi là hậu điều kiện (postcondition)'.

2.1. Xác định và áp dụng Precondition cho phương thức C

Precondition là một điều kiện tiên quyết phải đúng trước khi một phương thức được thực thi. Nó đảm bảo rằng phương thức nhận được các tham số hợp lệ và có thể thực hiện công việc của mình một cách chính xác. Trong C#, Precondition thường được biểu diễn bằng các câu lệnh Contract.Requires. Ví dụ, một phương thức chia hai số có thể có Precondition là số chia phải khác 0: Contract.Requires(denominator != 0);. Khi một Precondition không được đáp ứng, một ngoại lệ ContractException sẽ được ném ra, báo hiệu rằng người gọi phương thức đã vi phạm hợp đồng. Sử dụng Precondition giúp ngăn chặn các lỗi xảy ra do đầu vào không hợp lệ và cải thiện tính tin cậy của phương thức. Cần lưu ý rằng Precondition không nên được sử dụng để kiểm tra các lỗi do người dùng nhập liệu, mà chỉ nên được sử dụng để kiểm tra các lỗi do các thành phần phần mềm khác gây ra.

2.2. Thiết lập Postcondition để đảm bảo kết quả đúng đắn trong C

Postcondition là một điều kiện phải đúng sau khi một phương thức đã thực thi xong. Nó đảm bảo rằng phương thức đã thực hiện công việc của mình một cách chính xác và trả về kết quả hợp lệ. Trong C#, Postcondition thường được biểu diễn bằng các câu lệnh Contract.Ensures. Ví dụ, một phương thức tính tổng hai số có thể có Postcondition là kết quả trả về phải lớn hơn hoặc bằng cả hai số đầu vào: Contract.Ensures(Contract.Result<int>() >= a && Contract.Result<int>() >= b);. Contract.Result<T>() được sử dụng để truy cập giá trị trả về của phương thức. Sử dụng Postcondition giúp đảm bảo rằng phương thức hoạt động đúng như mong đợi và trả về kết quả chính xác. Việc vi phạm Postcondition cho thấy có lỗi trong quá trình thực thi của phương thức.

2.3. Invariant và vai trò quan trọng trong lớp đối tượng C

Invariant là một điều kiện phải luôn đúng cho một đối tượng trong suốt vòng đời của nó. Nó đảm bảo rằng đối tượng luôn ở trạng thái hợp lệ và nhất quán. Trong C#, Invariant thường được biểu diễn bằng các câu lệnh Contract.Invariant. Invariant được kiểm tra trước và sau khi thực thi mỗi phương thức công khai (public method) của lớp. Ví dụ, một lớp BankAccount có thể có Invariant là số dư tài khoản không bao giờ được âm: Contract.Invariant(Balance >= 0);. Sử dụng Invariant giúp đảm bảo rằng đối tượng luôn ở trạng thái hợp lệ và ngăn chặn các lỗi xảy ra do trạng thái không nhất quán. Việc vi phạm Invariant cho thấy có lỗi trong quá trình thực thi của một hoặc nhiều phương thức của lớp.

III. C Contract Thư viện hỗ trợ Design by Contract trong

Để áp dụng Design by Contract trong C#, ta có thể sử dụng thư viện C# Contract, còn được gọi là Code Contracts, một phần của .NET Framework. Thư viện này cung cấp các công cụ để định nghĩa và kiểm tra các Precondition, Postcondition, và Invariant của các thành phần phần mềm. Sử dụng C# Contract giúp tự động hóa quá trình kiểm tra Assertion, giảm thiểu lỗi và cải thiện tính tin cậy của phần mềm. C# Contract hỗ trợ cả Static Checking (kiểm tra tĩnh) và Runtime Checking (kiểm tra thời gian chạy). Static Checking cho phép phát hiện lỗi trong quá trình biên dịch, trước khi chương trình được thực thi. Runtime Checking cho phép phát hiện lỗi trong quá trình thực thi, khi chương trình đang chạy. Sự kết hợp giữa Static CheckingRuntime Checking giúp đảm bảo rằng các hợp đồng được tuân thủ một cách nghiêm ngặt. Theo Microsoft, việc sử dụng Code Contracts giúp giảm thiểu số lượng lỗi trong quá trình phát triển và cải thiện chất lượng của phần mềm.

3.1. Tìm hiểu về namespace System.Diagnostics.Contracts trong C

Namespace System.Diagnostics.Contracts trong .NET Framework chứa các lớp và phương thức cần thiết để định nghĩa và sử dụng Code Contracts trong C#. Lớp Contract là lớp trung tâm của namespace này, cung cấp các phương thức như Requires, Ensures, và Invariant để định nghĩa các Precondition, Postcondition, và Invariant. Namespace này cũng cung cấp các thuộc tính (attributes) như ContractClassAttributeContractClassForAttribute để định nghĩa các hợp đồng cho các interface và abstract class. Việc import namespace System.Diagnostics.Contracts cho phép sử dụng các tính năng của Code Contracts trong mã nguồn C#.

3.2. Cách sử dụng Contract Class Contract Method trong C

Contract ClassContract Method là hai khái niệm quan trọng trong C# Contract. Contract Class được sử dụng để định nghĩa các hợp đồng cho các interface và abstract class. Nó cho phép định nghĩa các Precondition, Postcondition, và Invariant mà tất cả các lớp kế thừa từ interface hoặc abstract class phải tuân thủ. Contract Method được sử dụng để định nghĩa các hợp đồng cho các phương thức cụ thể. Nó cho phép định nghĩa các PreconditionPostcondition mà phương thức phải tuân thủ. Sử dụng Contract ClassContract Method giúp đảm bảo tính nhất quán và tuân thủ hợp đồng trong toàn bộ hệ thống.

3.3. Phân biệt Static Checking và Runtime Checking với C Contract

C# Contract hỗ trợ cả Static CheckingRuntime Checking. Static Checking được thực hiện trong quá trình biên dịch bằng công cụ ccrewrite. Nó phân tích mã nguồn và kiểm tra xem các hợp đồng có thể bị vi phạm hay không. Nếu một hợp đồng có thể bị vi phạm, trình biên dịch sẽ đưa ra cảnh báo. Runtime Checking được thực hiện trong quá trình thực thi chương trình. Các câu lệnh Contract.Requires, Contract.Ensures, và Contract.Invariant sẽ được kiểm tra tại thời điểm chạy. Nếu một hợp đồng bị vi phạm, một ngoại lệ ContractException sẽ được ném ra. Static Checking giúp phát hiện lỗi sớm hơn, trong khi Runtime Checking giúp đảm bảo rằng các hợp đồng được tuân thủ trong quá trình vận hành. Sự kết hợp giữa hai loại kiểm tra này giúp tăng cường tính tin cậy của phần mềm.

IV. Static Checking Runtime Checking Cơ chế kiểm tra hợp đồng DbC

Design by Contract (DbC) đạt hiệu quả cao nhất khi kết hợp cả Static CheckingRuntime Checking. Static Checking, sử dụng các công cụ như trình biên dịch hoặc các công cụ phân tích mã nguồn, giúp phát hiện các vi phạm hợp đồng tiềm ẩn ngay trong quá trình phát triển. Runtime Checking đảm bảo rằng các hợp đồng được tuân thủ trong quá trình thực thi chương trình, bằng cách kiểm tra các Assertion tại thời điểm chạy. Việc sử dụng cả hai cơ chế này giúp phát hiện lỗi sớm hơn và đảm bảo tính tin cậy của phần mềm trong mọi giai đoạn. Một số công cụ, như Spec#, cung cấp khả năng Formal Verification, cho phép chứng minh một cách toán học rằng mã nguồn tuân thủ các hợp đồng đã được định nghĩa. Tuy nhiên, Formal Verification thường đòi hỏi kiến thức và kỹ năng chuyên sâu, và có thể không phù hợp với tất cả các dự án. Theo nghiên cứu của Viện Công nghệ Massachusetts (MIT), việc kết hợp Static CheckingRuntime Checking giúp giảm thiểu số lượng lỗi trong quá trình phát triển lên đến 50%.

4.1. Ưu điểm và hạn chế của Static Checking trong DbC

Static Checking trong DbC có ưu điểm là phát hiện lỗi sớm trong quá trình phát triển, trước khi chương trình được thực thi. Nó giúp giảm chi phí sửa lỗi và cải thiện tính tin cậy của phần mềm. Tuy nhiên, Static Checking cũng có những hạn chế. Nó có thể không phát hiện được tất cả các lỗi, đặc biệt là các lỗi liên quan đến các điều kiện phức tạp hoặc các tương tác giữa các thành phần. Static Checking cũng có thể đưa ra các cảnh báo sai (false positives), khiến nhà phát triển mất thời gian để kiểm tra và loại bỏ. Một số công cụ Static Checking yêu cầu mã nguồn phải được viết theo một phong cách nhất định, điều này có thể làm giảm tính linh hoạt trong quá trình phát triển.

4.2. Lợi ích và nhược điểm của Runtime Checking trong DbC

Runtime Checking trong DbC có ưu điểm là kiểm tra các Assertion tại thời điểm chạy, đảm bảo rằng các hợp đồng được tuân thủ trong quá trình vận hành. Nó giúp phát hiện các lỗi mà Static Checking có thể bỏ sót. Tuy nhiên, Runtime Checking cũng có những nhược điểm. Nó làm chậm quá trình thực thi chương trình, vì các Assertion phải được kiểm tra mỗi khi một phương thức được gọi. Runtime Checking có thể không phát hiện được tất cả các lỗi, đặc biệt là các lỗi liên quan đến các điều kiện hiếm gặp hoặc các tương tác phức tạp. Ngoài ra, Runtime Checking có thể gây ra các ngoại lệ không mong muốn trong quá trình vận hành.

4.3. Cách lựa chọn giữa Static Checking và Runtime Checking cho dự án C

Việc lựa chọn giữa Static CheckingRuntime Checking phụ thuộc vào yêu cầu cụ thể của dự án. Nếu dự án đòi hỏi tính tin cậy cao và có đủ thời gian và nguồn lực, nên sử dụng cả hai cơ chế. Static Checking giúp phát hiện lỗi sớm và giảm chi phí sửa lỗi, trong khi Runtime Checking đảm bảo rằng các hợp đồng được tuân thủ trong quá trình vận hành. Nếu dự án có hạn chế về thời gian hoặc nguồn lực, có thể ưu tiên Static Checking để phát hiện các lỗi phổ biến và quan trọng. Ngoài ra, có thể tùy chỉnh mức độ kiểm tra của Runtime Checking để giảm thiểu ảnh hưởng đến hiệu năng. Trong giai đoạn phát triển và kiểm thử, nên bật Runtime Checking ở mức cao nhất để phát hiện càng nhiều lỗi càng tốt. Trong giai đoạn triển khai, có thể giảm mức độ kiểm tra hoặc tắt hoàn toàn Runtime Checking để cải thiện hiệu năng. Tuy nhiên, cần cân nhắc kỹ lưỡng trước khi tắt Runtime Checking, vì điều này có thể làm giảm tính tin cậy của phần mềm.

V. Ứng dụng Design by Contract Ví dụ thực tế và Code mẫu C

Để hiểu rõ hơn về cách áp dụng Design by Contract, ta có thể xem xét một số ví dụ thực tế và code mẫu C#. Ví dụ, một lớp Stack có thể có Invariant là số lượng phần tử trong stack phải lớn hơn hoặc bằng 0 và nhỏ hơn hoặc bằng dung lượng của stack. Phương thức Push có thể có Precondition là stack phải chưa đầy, và Postcondition là số lượng phần tử trong stack phải tăng lên 1. Phương thức Pop có thể có Precondition là stack phải không rỗng, và Postcondition là số lượng phần tử trong stack phải giảm đi 1. Việc áp dụng DbC giúp đảm bảo rằng lớp Stack hoạt động đúng như mong đợi và ngăn chặn các lỗi xảy ra do các thao tác không hợp lệ. Một ví dụ khác là lớp BankAccount, đã được đề cập ở trên, với Invariant là số dư tài khoản không bao giờ âm. Phương thức Deposit có thể có Precondition là số tiền gửi phải lớn hơn 0, và Postcondition là số dư tài khoản phải tăng lên một lượng tương ứng. Phương thức Withdraw có thể có Precondition là số tiền rút phải lớn hơn 0 và nhỏ hơn hoặc bằng số dư tài khoản, và Postcondition là số dư tài khoản phải giảm đi một lượng tương ứng. Các ví dụ này cho thấy rằng DbC có thể được áp dụng cho nhiều loại lớp và phương thức khác nhau, giúp cải thiện tính tin cậy và dễ bảo trì của phần mềm.

5.1. Xây dựng lớp Stack C với Precondition Postcondition và Invariant

Dưới đây là một ví dụ về lớp Stack C# với Precondition, Postcondition, và Invariant được định nghĩa bằng thư viện C# Contract:

using System;
using System.Diagnostics.Contracts;

public class Stack<T>
{
    private T[] _items;
    private int _count;

    public Stack(int capacity)
    {
        Contract.Requires(capacity >= 0, "Capacity must be non-negative");

        _items = new T[capacity];
        _count = 0;
        Contract.Invariant(_count >= 0 && _count <= _items.Length);
    }

    public void Push(T item)
    {
        Contract.Requires(_count < _items.Length, "Stack is full");

        _items[_count] = item;
        _count++;
        Contract.Ensures(_count == Contract.OldValue(_count) + 1);
    }

    public T Pop()
    {
        Contract.Requires(_count > 0, "Stack is empty");

        T item = _items[_count - 1];
        _count--;
        Contract.Ensures(_count == Contract.OldValue(_count) - 1);
        return item;
    }

    public int Count { get { return _count; } }
}

Trong ví dụ này, Precondition của constructor đảm bảo rằng dung lượng của stack phải không âm. Precondition của phương thức Push đảm bảo rằng stack chưa đầy. Precondition của phương thức Pop đảm bảo rằng stack không rỗng. Postcondition của phương thức PushPop đảm bảo rằng số lượng phần tử trong stack thay đổi một cách chính xác. Invariant của lớp đảm bảo rằng số lượng phần tử trong stack luôn hợp lệ.

5.2. Triển khai lớp BankAccount C sử dụng Design by Contract

using System;
using System.Diagnostics.Contracts;

public class BankAccount
{
    private decimal _balance;

    public BankAccount(decimal initialBalance)
    {
        Contract.Requires(initialBalance >= 0, "Initial balance must be non-negative");

        _balance = initialBalance;
        Contract.Invariant(_balance >= 0);
    }

    public void Deposit(decimal amount)
    {
        Contract.Requires(amount > 0, "Deposit amount must be positive");

        _balance += amount;
        Contract.Ensures(_balance == Contract.OldValue(_balance) + amount);
    }

    public void Withdraw(decimal amount)
    {
        Contract.Requires(amount > 0, "Withdrawal amount must be positive");
        Contract.Requires(amount <= _balance, "Insufficient funds");

        _balance -= amount;
        Contract.Ensures(_balance == Contract.OldValue(_balance) - amount);
    }

    public decimal Balance { get { return _balance; } }
}

Trong ví dụ này, Precondition của constructor đảm bảo rằng số dư ban đầu phải không âm. Precondition của phương thức Deposit đảm bảo rằng số tiền gửi phải dương. Precondition của phương thức Withdraw đảm bảo rằng số tiền rút phải dương và không vượt quá số dư tài khoản. Postcondition của phương thức DepositWithdraw đảm bảo rằng số dư tài khoản thay đổi một cách chính xác. Invariant của lớp đảm bảo rằng số dư tài khoản luôn không âm.

5.3. Unit Testing và Design by Contract Bộ đôi hoàn hảo cho C

Unit TestingDesign by Contract là hai phương pháp bổ sung cho nhau để đảm bảo tính tin cậy của phần mềm C#. Unit Testing tập trung vào việc kiểm tra từng thành phần phần mềm (unit) một cách độc lập. Design by Contract tập trung vào việc định nghĩa các hợp đồng rõ ràng giữa các thành phần phần mềm. Sử dụng cả hai phương pháp này giúp phát hiện lỗi sớm hơn và đảm bảo rằng phần mềm hoạt động đúng như mong đợi. Unit Tests có thể được sử dụng để kiểm tra xem các Precondition, Postcondition, và Invariant có được tuân thủ hay không. Khi một hợp đồng bị vi phạm, Unit Test sẽ thất bại, báo hiệu rằng có lỗi xảy ra. Ngược lại, DbC có thể giúp đơn giản hóa quá trình viết Unit Tests, vì các hợp đồng đóng vai trò như đặc tả chính thức về hành vi của các thành phần. Kết hợp Unit TestingDbC giúp tăng cường tính tin cậy và dễ bảo trì của phần mềm.

VI. Kết luận Design by Contract cho C Tương lai và Phát triển

Design by Contract (DbC) là một phương pháp tiếp cận mạnh mẽ để xây dựng phần mềm tin cậy và dễ bảo trì. Việc áp dụng DbC trong C# giúp phát hiện lỗi sớm hơn, cải thiện khả năng bảo trì, và nâng cao tính tin cậy của phần mềm. Mặc dù có một số thách thức, chẳng hạn như đòi hỏi sự thay đổi trong tư duy thiết kế và có thể làm chậm quá trình thực thi, nhưng lợi ích mà DbC mang lại là rất lớn. Trong tương lai, có thể mong đợi sự phát triển của các công cụ và kỹ thuật DbC tiên tiến hơn, giúp tự động hóa quá trình định nghĩa và kiểm tra hợp đồng, và tích hợp DbC một cách chặt chẽ hơn vào các quy trình phát triển phần mềm hiện đại. Nghiên cứu và phát triển các công cụ hỗ trợ Formal Verification cũng có thể giúp chứng minh một cách toán học rằng mã nguồn tuân thủ các hợp đồng đã được định nghĩa. DbC hứa hẹn sẽ đóng vai trò ngày càng quan trọng trong việc xây dựng phần mềm chất lượng cao và đáng tin cậy.

6.1. Tổng kết lợi ích và thách thức của Design by Contract trong C

Design by Contract mang lại nhiều lợi ích như phát hiện lỗi sớm, cải thiện bảo trì, tăng độ tin cậy, và hỗ trợ giao tiếp tốt hơn giữa các thành viên trong nhóm phát triển. Tuy nhiên, cũng có những thách thức cần xem xét. DbC đòi hỏi sự thay đổi trong tư duy thiết kế và có thể làm chậm quá trình thực thi. Việc định nghĩa các hợp đồng một cách chính xác có thể khó khăn và tốn thời gian. Ngoài ra, các công cụ hỗ trợ DbC chưa hoàn thiện và có thể gây ra các vấn đề về hiệu năng.

6.2. Hướng phát triển của Design by Contract và các công cụ hỗ trợ C

Hướng phát triển của Design by Contract tập trung vào việc tự động hóa quá trình định nghĩa và kiểm tra hợp đồng, cải thiện hiệu năng, và tích hợp DbC một cách chặt chẽ hơn vào các quy trình phát triển phần mềm hiện đại. Có thể mong đợi sự phát triển của các công cụ phân tích mã nguồn thông minh hơn, có khả năng tự động phát hiện các hợp đồng tiềm ẩn và đề xuất các cải tiến cho các hợp đồng hiện có. Các công cụ hỗ trợ Formal Verification cũng có thể trở nên dễ sử dụng hơn và phù hợp với nhiều loại dự án khác nhau. Ngoài ra, có thể có sự phát triển của các thư viện C# mới, cung cấp các tính năng DbC mạnh mẽ hơn và dễ sử dụng hơn.

6.3. Code Contracts và tương lai của lập trình đáng tin cậy với C

Code Contracts vẫn là một phần quan trọng của hệ sinh thái C# và đóng vai trò quan trọng trong việc xây dựng phần mềm đáng tin cậy. Mặc dù có thể không được quảng bá rộng rãi như các tính năng mới khác của C#, Code Contracts vẫn được sử dụng rộng rãi trong các dự án đòi hỏi tính tin cậy cao. Trong tương lai, có thể có sự tái tập trung vào Code Contracts và các kỹ thuật Formal Verification khác, do nhu cầu ngày càng tăng về phần mềm an toàn và đáng tin cậy. Việc kết hợp Code Contracts với các kỹ thuật Unit TestingIntegration Testing giúp tạo ra một quy trình phát triển phần mềm toàn diện, đảm bảo rằng phần mềm hoạt động đúng như mong đợi và đáp ứng các yêu cầu của người dùng.

22/09/2025

Trích đoạn nội dung tài liệu

Chương 1: Giới thiệu về Eiffel 1. Giới thiệu Đầu tiên, chúng ta sẽ làm quen với phần mềm Eiffel trước khi tìm hiểu về công nghệ Design By Contract. Vì sao lại như vậy? Vì tất cả ví dụ dùng trong luận văn đều sử dụng cấu trúc của ngôn ngữ Eiffel. Còn những khái niệm nào mới được đề cập trong chương này sẽ được giải thích kỹ hơn trong các phần sau khi giới thiệu về Design By Contract Qua hơn 10 năm tồn tại, Eiffel hiện nay được coi là một trong những môi trường phát triển phần mềm tốt nhất.

Trước sức mạnh to lớn của Eiffel trong lĩnh vực phần mềm thì dù muốn dù không, bạn cũng nên biết qua về nó. Vậy thực chất Eiffel là gì? Eiffel là khung làm việc trợ giúp cho việc suy nghĩ, thiết kế và thực thi phần mềm hướng đối tượng. Eiffel là một phương pháp, một ngôn ngữ hỗ trợ mô tả một cách hiệu quả và phát triển những hệ thống có chất lượng. Eiffel là ngôn ngữ thiết kế Vai trò của Eiffel còn hơn một ngôn ngữ lập trình.

Những gì nó đem lại không chỉ giới hạn trong ngữ cảnh lập trình mà trải rộng khắp công việc phát triển phần mềm: phân tích, lên mô hình, viết đặc tả, thiết kế kiến trúc, thực hiện, bảo trì, làm tài liệu. Eiffel là một phương pháp. Eiffel dẫn đường các nhà phân tích và những nhà phát triển xuyên suốt tiến trình xây dựng một phần mềm. Phương pháp Eiffel tập trung cả về yếu tố sản phẩm và chất lượng, với những điểm nhấn: tính đáng tin cậy, tính tái sử dụng, tính mở rộng, tính khả dụng, tính bền vững.

9 LUAN VAN CHAT LUONG download : add luanvanchat@agmail.com Tìm hiểu công nghệ Design By Contract và Xây dựng công cụ hỗ trợ cho C# 1. Design By Contract trong Eiffel Eiffel hỗ trợ rất nhiều tính năng: tiếp cận hướng đối tượng hoàn thiện, khả năng giao tiếp bên ngoài (có thể giao tiếp với các ngôn ngữ C, C++, Java,…), hỗ trợ vòng đời phần mềm bao gồm việc phân tích, thiết kế, thực thi và bảo trì, hỗ trợ Design By Contract, viết xác nhận, quản lý ngoại lệ… Design By Contract hầu như là vấn đề luôn được nhắc đến khi đề cập về Eiffel. Trong Eiffel, mỗi thành phần của hệ thống đều có thể được thực hiện theo một đặc tả tiên quyết về các thuộc tính trừu tượng của nó, liên quan đến những thao tác nội tại và những giao tác của nó với các thành phần khác. Eiffel thực thi một cách trực tiếp ý tưởng Design By Contract, một phương pháp làm nâng cao tính đáng tin cậy của phần mềm, cung cấp một nền tảng cho việc đặc tả, làm tài liệu và kiểm nghiệm phần mềm, cũng như việc quản lý các ngoại lệ và cách sử dụng kế thừa thích hợp.

EiffelStudio EiffelStudio là trình biên dịch của Eiffel. Ngoài ra, nó còn là một IDE rất mạnh với những tính năng độc nhất như: công cụ công nghệ đảo tích hợp, bộ máy phân tích mã nguồn định lượng. Tùy vào nhu cầu của mình, bạn có thể sử dụng EiffelStudio như một môi trường lập trình hoặc chỉ như một công cụ giúp mô hình hóa, xây dựng các mô tả hệ thống bao gồm các lớp trừu tượng mà không thực thi bằng công cụ Diagram hoặc kết hợp cả 2 khả năng để đạt đến hiệu quả cao nhất. 10 LUAN VAN CHAT LUONG download : add luanvanchat@agmail.com Tìm hiểu công nghệ Design By Contract và Xây dựng công cụ hỗ trợ cho C# 1.

Giao diện Hình 1-1: Giao diện EiffelStudio Giao diện làm việc của EiffelStudio có 4 khung chính: Features, Class, Clusters, Context. Để thuận tiện cho việc lập trình, các bạn có thể đóng bớt các khung cửa sổ đi. Tất cả các khung cửa sổ này đều có thể đóng lại ngọai trừ Class. Các thao tác căn bản trên EiffelStudio Khởi động chương trình: Programs --> EiffelStudio Version --> EiffelStudio Chọn "Create a new project" > OK.

Class view là khung làm việc chính của bạn. Sau khi lập trình xong, bạn có thể biên dịch và cho chạy chương trình bằng công cụ Compile (F7). Debug chương trình: F10, F11. Lưu project: File > Save.

11 LUAN VAN CHAT LUONG download : add luanvanchat@agmail.com Tìm hiểu công nghệ Design By Contract và Xây dựng công cụ hỗ trợ cho C# Biểu diễn Design By Contract trong Eiffel: Precondition: require boolean expressions Postcondition: ensure boolean expressions Class invariant: invariant boolean expressions Chỉ thị Check: check assertion_clause1 assertion_clause2 … assertion_clausen end Loop invariant, loop variant: from initialization until exit invariant inv variant 12 LUAN VAN CHAT LUONG download : add luanvanchat@agmail.com Tìm hiểu công nghệ Design By Contract và Xây dựng công cụ hỗ trợ cho C# var loop body end Demo: Project stack STACK_CLASS: lớp stack chính, chứa các định nghĩa các thao tác trên stack. make: Hàm khởi tạo của stack. item: hàm lấy phần tử trên cùng stack. get(t): hàm lấy phần tử thứ t empty: kiểm tra stack có rỗng.

full: kiểm tra stack có đầy put(x): thêm phần tử x vào stack remove: bỏ phần tử trên cùng stack TEST_CLASS: lớp chính(main), lớp gọi các hàm của lớp STACK_CLASS. Ta sẽ thử vài trường hợp cho thấy khả năng bắt lỗi của Eiffel. Lưu ý: Sau mỗi trường hợp hãy sửa lại code như ban đầu rồi mới thử tiếp trường hợp khác. Mở tập tin test_class.

Chạy thử chương trình (F5). Chương trình khởi tạo stack gồm 8 phần tử từ 0 đến 7 và xuất stack. Stack được xuất ra màn hình. TH1: Lỗi xảy ra ở tiền điều kiện Sửa n:=8 thành n:=-8.

Tại dòng if (n >= 0) then nhấn tổ hợp phím Ctrl-K. Tại dòng end --end if , nhấn tổ hợp phím Ctrl-K. 13 LUAN VAN CHAT LUONG download : add luanvanchat@agmail.com Tìm hiểu công nghệ Design By Contract và Xây dựng công cụ hỗ trợ cho C# Xuất hiện thông báo ngoại lệ sau: Hình 1-2: Thông báo khi lỗi xảy ra ở tiền điều kiện và con trỏ dừng lại ở câu lệnh Hình 1-3: Code gây ra lỗi ở tiền điều kiện Nguyên nhân: Khi bạn gọi thủ tục a.make(n), do trước đó khởi tạo n là một số âm (=-8), client không đảm bảo contract, nên trong thủ tục make của lớp STACK_CLASS, thủ tục make kiểm tra không thỏa tiền điều kiện positive_capacity: n>=0, nó dừng lại và thông báo cho người lập trình biết. TH2: Lỗi xảy ra ở hậu điều kiện Trong lớp TEST_CLASS, tại thủ tục make, sửa như sau: 14 LUAN VAN CHAT LUONG download : add luanvanchat@agmail.com Tìm hiểu công nghệ Design By Contract và Xây dựng công cụ hỗ trợ cho C# Capacity := n capacity := n-1 Recompile (Shift-F7) và cho chạy lại chương trình (F5).

Xuất hiện thông báo ngoại lệ sau: Hình 1-4: Thông báo khi lỗi xảy ra ở hậu điều kiện và con trỏ dừng lại ở câu lệnh Hình 1-5: Code gây ra lỗi ở hậu điều kiện Nguyên nhân: Trước đó, ta gán capacity := n-1, hậu điều kiện lại yêu cầu capacity = n. TH3: Lỗi xảy ra ở điều kiện bất biến. Trong lớp TEST_CLASS, tại thủ tục make, thêm vào dòng sau: count:=-1 15 LUAN VAN CHAT LUONG download : add luanvanchat@agmail.com Tìm hiểu công nghệ Design By Contract và Xây dựng công cụ hỗ trợ cho C# Chọn menu Project > Project Setting… Bỏ dấu check ở ensure. Đánh dấu check ở invariant.

Hành động này nhằm bỏ qua chế độ kiểm lỗi ở hậu điều kiện. Ở đây chỉ muốn minh họa cho việc phát hiện lỗi ở điều kiện bất biến. Xuất hiện thông báo ngoại lệ sau: Hình 1-6: Thông báo khi lỗi xảy ra ở điều kiện bất biến và con trỏ dừng lại ở câu lệnh Hình 1-7: Code gây ra lỗi ở điều kiện bất biến Nguyên nhân: Trước đó, ta gán count := -1, điều kiện bất biến yêu cầu count>=0. 16 LUAN VAN CHAT LUONG download : add luanvanchat@agmail.com Tìm hiểu công nghệ Design By Contract và Xây dựng công cụ hỗ trợ cho C# Chương 2: Một số cơ chế mang lại tính đáng tin cậy cho phần mềm Trước hết, phải nói rằng kỹ thuật định nghĩa thuộc tính của một đối tượng gần như là có liên quan với cấu trúc của những hệ thống phần mềm.

Những kiến trúc đơn giản, riêng biệt và có khả năng mở rộng sẽ giúp chúng ta đảm bảo tính đáng tin cậy của phần mềm dễ dàng hơn so với những cấu trúc vặn vẹo. Đặc biệt, cố gắng giới hạn sự liên quan giữa các môđun với nhau đến mức tối thiểu nhất sẽ là tiêu điểm cho việc thảo luận về tính riêng biệt. Điều này giúp ngăn chặn những rủi ro thông thường của tính đáng tin cậy, ví dụ như những biến toàn cục và việc định nghĩa những cơ chế liên lạc bị giới hạn, client và những mối quan hệ kế thừa. Nói đến chất lượng phần mềm thì không thể bỏ qua tính đáng tin cậy.

Chúng ta cố gắng giữ cho những cấu trúc càng đơn giản càng tốt. Tuy rằng điều này vẫn chưa đủ đảm bảo cho tính đáng tin cậy của phần mềm, nhưng dù sao, nó cũng là một điều kiện cần thiết. Một điều kiện khác cũng cần thiết nữa là làm cho phần mềm của chúng ta tối ưu và dễ đọc. Văn bản phần mềm không những được viết một lần mà nó còn phải được đọc đi đọc lại và viết đi viết lại nhiều lần.

Sự trong sáng và tính đơn giản của các câu chú thích là những yêu cầu cơ bản để nâng cao tính đáng tin cậy của phần mềm. Một vũ khí khác cũng rất cần thiết là việc quản lý bộ nhớ một cách tự động, đặc biệt là bộ thu gom rác (garbage collection). Bất kỳ hệ thống nào có khởi tạo và thao tác với cấu trúc dữ liệu động mà lại thực hiện thu hồi bộ nhớ bằng tay (tức là do người lập trình điều khiển) hoặc bộ nhớ không hề được thu hồi thì thật là nguy hiểm. Bộ thu gom rác không hề là một sự xa xỉ mà nó là thành phần thiết yếu để mở rộng tính đáng tin cậy cho bất kỳ một môi trường hướng đối tượng nào.

Một kỹ thuật khác nữa mà cũng có thể là thiết yếu mà có liên quan đến genericity là static typing. Nếu không có những luật như thế thì chúng ta sẽ không kiểm soát được những lỗi xảy ra lúc run-time do quá trình gõ code gây nên. 17 LUAN VAN CHAT LUONG download : add luanvanchat@agmail.com Tìm hiểu công nghệ Design By Contract và Xây dựng công cụ hỗ trợ cho C# Tóm lại, tất cả những kỹ thuật này cung cấp một nền tảng cần thiết để ta có cái nhìn gần hơn về một hệ thống phần mềm đúng đắn và bền vững. Chương 3: Tính đúng đắn của phần mềm Giả sử có một người đưa cho bạn một chương trình C với 300 000 dòng lệnh và hỏi rằng nó có đúng không.

Tôi nghĩ rằng rất có khả năng bạn thấy khó và thậm chí là không thể trả lời được.

Nội dung được bảo vệ bản quyền — Tải xuống đầy đủ