Chương 7: Giao ước cho tính đáng tin cậy của phần mềm Định nghĩa tiền điều kiện và hậu điều kiện cho một thủ tục là một cách định nghĩa một hợp đồng contract giữa thủ tục và những lớp g
Trang 1n>0; x/=Void
Lưu ý cách dùng dấu chấm phẩy (“;”) Ý nghĩa của dấu chấm phẩy ở đây tương đương với phép and Dấu chấm phẩy có thể đặt giữa phần khai báo và chỉ thị
Khi những mệnh đề của xác nhận nằm trên những dòng khác nhau, ta không cần
dùng dấu chấm phẩy (xem như có một phép and mặc định giữa các dòng liên tiếp)
Những quy ước này giúp ta có thể nhận biết các thành phần riêng biệt của một xác
nhận Trong thực tế, ta thường dán nhãn (label) cho những thành phần này, ví dụ như:
Positive: n>0
Not_void: x/=Void
Các nhãn như trên có vai trò nhất định trong lúc thực thi xác nhận Tuy nhiên, việc sử dụng chúng ở đây là nhằm làm cho văn bản của ta rõ ràng và tường
minh hơn
Chương 6: Tiền điều kiện và hậu điều kiện
Ứng dụng đầu tiên của xác nhận là đặc tả ngữ nghĩa của thủ tục Một thủ tục không chỉ là một đoạn mã chương trình mà nó là cài đặt của một hàm nào đó từ đặc
tả của một kiểu dữ liệu trừu tượng, nó sẽ thực hiện một công việc hữu ích Việc biểu diễn công việc này một cách chính xác là vô cùng cần thiết
Ta có thể đặc tả công việc cần thực thi của một thủ tục bằng 2 xác nhận liên quan với nó là tiền điều kiện (preconditions) và hậu điều kiện (postconditions) Tiền điều kiện chỉ ra những thuộc tính cần được thoả mãn bất cứ khi nào thủ tục được gọi; còn hậu điều kiện chỉ ra những thuộc tính chắc chắn có sau khi thủ tục thực thi xong
6.1 Lớp ngăn xếp
Một ví dụ sẽ giúp ta làm quen với cách sử dụng các xác nhận:
Trang 2class STACK [G] feature
…Declaration of the features:
count, empty, full, put, remove, item
end
Trước khi xem phần cài đặt, cần chú ý rằng những thủ tục được đặc trưng bởi những thuộc tính ngữ nghĩa mạnh mẽ và độc lập với cách biểu diễn nó Ví dụ như:
− remove và item chỉ thực thi được khi có số phần tử lớn hơn 0
− put tăng số phần tử lên 1, remove giảm số phần tử đi 1
Những thuộc tính như thế là một phần trong đặc tả của kiểu dữ liệu trừu tượng, ngay cả những người chưa bao giờ sử dụng đến kiểu dữ liệu trừu tượng cũng phải hiểu như vậy Nhưng trong các cách tiếp cận thông thường của việc xây dựng phần mềm, những văn bản phần mềm thường không đề cập đến chúng Thông qua những tiền điều kiện và hậu điều kiện của thủ tục, ta có thể đưa chúng vào những thành phần tường minh của phần mềm
Ta sẽ biểu diễn những tiền điều kiện và hậu điều kiện như là mệnh đề được giới thiệu qua hai từ khóa require và ensure Lưu ý rằng những phần cài đặt của những thủ tục được chừa trống, ta sẽ tìm hiểu trong phần sau
indexing
description: "Stacks: Cấu trúc dữ liệu với quy tắc truy xuất LIFO”
class STACK1 [G] feature – Access
count: INTEGER
Số phần tử của Stack
item: G is
Phần tử trên cùng
require
not empty
Trang 3do
…
end feature – Status report
empty: BOOLEAN is
Kiểm tra Stack rỗng?
do end
full: BOOLEAN is
Kiểm tra Stack đầy?
do
…
end feature Element change
put (x: G) is
Thêm phần tử x vào Stack
require
not full
do
…
ensure
not empty item = x
count = old count + 1
end
remove is
Xóa phần tử trên cùng của Stack
require
not empty
do
…
ensure
not full
Trang 4count = old count – 1
end end
Cả 2 mệnh đề require và ensure đều là tùy chọn Nếu có thì require phải nằm trước mệnh đề local Những phần tiếp theo giải thích chi tiết hơn ý nghĩa của tiền điều kiện và hậu điều kiện
6.2 Tiền điều kiện
Tiền điều kiện biểu diễn những ràng buộc mà một thủ tục sẽ thực hiện một cách chính xác Ví dụ như:
− put sẽ không được gọi nếu ngăn xếp đã đầy
− remove và item sẽ không thực hiện trên một ngăn xếp rỗng
Tiền điều kiện vào có hiệu lực đến tất cả những lời gọi của thủ tục, cả trong lớp và từ những lớp liên quan Một hệ thống chính xác sẽ không bao giờ thực thi một lời gọi không thỏa mãn tiền điều kiện
6.3 Hậu điều kiện
Hậu điều kiện biểu diễn những thuộc tính của trạng thái kết quả có được sau khi thực thi thủ tục Ví dụ như:
− Sau thủ tục put, ngăn xếp sẽ không thể rỗng, phần tử trên cùng là phần
tử mới được thêm vào, và số lượng phần tử sẽ tăng lên 1
− Sau thủ tục remove, ngăn xếp không thể đầy, số phần tử giảm đi 1
Sự có mặt của mệnh đề hậu điều kiện trong thủ tục bảo đảm kết quả của thủ tục sẽ thoả mãn các thuộc tính (giả sử rằng thủ tục đã thỏa mãn tiền điều kiện để được gọi)
Một từ khóa đặc biệt, old, xuất hiện trong hậu điều kiện putvà remove dùng
từ khóa này để biểu diễn sự thay đổi của biến count Cú pháp: old e, trong đóe là
Trang 5một biểu thức, thường là một thuộc tính, biểu thị giá trị của e trong đầu vào của thủ tục Hậu điều kiện của putcó mệnh đề
count = old count + 1
để thể hiện rằng put, khi được gọi bởi bất kỳ đối tượng nào, sẽ tăng giá trị biến
count của đối tượng đó lên 1
Chương 7: Giao ước cho tính đáng tin cậy của phần mềm
Định nghĩa tiền điều kiện và hậu điều kiện cho một thủ tục là một cách định nghĩa một hợp đồng (contract) giữa thủ tục và những lớp gọi nó
7.1 Quyền lợi và nghĩa vụ
Bằng cách kết hợp những mệnh đề require pre và ensure post trong thủ tục
r, lớp đối tượng “tuyên bố” với những khách hàng của nó:
Nếu các bạn hứa gọi r thỏa mãn pre, tôi hứa sẽ trả về kết quả thỏa mãn post
Trong mối liên hệ giữa người và người hoặc giữa các công ty với nhau, hợp đồng là một văn bản làm cho những điều khoản của mối quan hệ trở nên trong sáng,
rõ ràng Thật đáng ngạc nhiên khi trong lĩnh vực phần mềm, nơi mà sự đúng đắn, rõ ràng có vai trò sống còn, ý tưởng hợp đồng này lại phải mất quá nhiều thời gian để thể hiện mình Tiền điều kiện và hậu điều kiện mô tả một hợp đồng giữa thủ tục (đóng vai trò nhà cung cấp) và những đối tượng gọi đến nó (vai trò khách hàng)
Đặc tính quan trọng nhất của hợp đồng trong công việc của con người là sự đòi hỏi về “nghĩa vụ” (obligation) và “quyền lợi” (right) cho cả 2 bên – thường là
nghĩa vụ của bên này sẽ trở thành quyền lợi của bên kia Điều này cũng đúng đối
với hợp đồng giữa các lớp đối tượng:
Trang 6− Tiền điều kiện ràng buộc khách hàng: nó định nghĩa những điều kiện
để một lời gọi đến thủ tục trở nên hợp pháp Đây là nghĩa vụ của khách hàng và là
quyền lợi của nhà cung cấp
− Hậu điều kiện ràng buộc lớp đối tượng: nó định nghĩa những điều
kiện cần phải được đảm bảo bởi thủ tục khi trả về Đây là quyền lợi của khách hàng
và là nghĩa vụ của nhà cung cấp
7.1.1 Những quyền lợi
− Đối với khách hàng, đó là sự đảm bảo những thuộc tính phải có được sau khi gọi thủ tục
− Đối với nhà cung cấp, đó là sự đảm bảo những tính chất phải được thỏa mãn ở bất cứ nơi nào thủ tục được gọi
7.1.2 Những nghĩa vụ
− Đối với khách hàng, đó là sự đáp ứng những yêu cầu được phát biểu trong tiền điều kiện
− Đối với nhà cung cấp, đó là những gì phải làm mà hậu điều kiện đã định ra
Đây là hợp đồng cho thủ tục putcủa ví dụ trên:
Khách hàng Đáp ứng tiền điều kiện: Chỉ gọi
put(x)khi ngăn xếp chưa đầy
Kết quả hậu điều kiện:
Thông tin ngăn xếp được cập nhật: không rỗng, xnằm trên cùng, counttăng 1
Nhà cung cấp
Đáp ứng hậu điều kiện: Cập nhật thông tin ngăn xếp: không rỗng, xnằm trên cùng, count
tăng 1
Kết quả tiền điều kiện:
Được bảo đảm là ngăn xếp chưa đầy khi putđược gọi
Trang 77.2 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
Mặc dù bạn có thể chưa để ý, một trong những nguyên tắc của hợp đồng có
xu hướng đi ngược lại những kiến thức tổng quát đã được công nhận trong ngành công nghiệp phần mềm; gặp nhiều sự phản ứng ngay từ đầu, nhưng Design by Contract đã là một trong số những đóng góp chính vào sự tin cậy của phần mềm và đạt được tầm quan trọng xứng đáng
Một quy tắc tương phản với sự quan sát đã nêu trên: tiền điều kiện là quyền lợi của nhà cung cấp và được biểu diễn ở góc dưới bên phải trong bảng trên: nếu phần khách hàng của hợp đồng không được thõa mãn, nói cách khác nếu lời gọi không đáp ứng tiền điều kiện, sau đó lớp đối tượng không được bao bọc bởi hậu điều kiện Trong trường hợp này thủ tục có thể làm bất cứ chuyện gì: trả ra giá trị bất kỳ, lặp vô hạn, không trả về giá trị, thậm chí làm hỏng sự thực thi bằng một cách nào đó Đây là trường hợp “khách hàng sai”
Lợi ích đầu tiên của thỏa thuận này là sự đơn giản hóa đáng kể phong cách lập trình Tiền điều kiện được xem như những ràng buộc mà một lời gọi đến một thủ tục phải tuân theo, còn nếu bạn là người phát triển lớp đối tượng, bạn sẽ giả sử rằng những ràng buộc này được thỏa mãn khi viết những thủ tục; bạn sẽ không cần phải kiểm tra những ràng buộc đó trong thủ tục Xét hàm căn bậc 2 như sau:
sqrt (x: REAL): REAL is
Căn bậc 2 của x
require
x >= 0
do … end
Bạn sẽ chỉ viết thuật toán tính căn bậc 2 mà không cần quan tâm đến trường hợp x < 0, điều này đã được kiểm tra bởi tiền điều kiện và trở thành trách nhiệm của những khách hàng – những lớp sẽ gọi tới hàm này
Thực tế phương pháp của Design by Contract còn đi xa hơn nữa
Viết đoạn chương trình này vào sau do
Trang 8if x < 0 then
“Handle the error, somehow”
else
“Proceed with normal square root computation”
end
không chỉ là không cần thiết mà là không thể chấp nhận được Điều này có thể biểu diễn như một nguyên tắc:
Nguyên tắc không dư thừa
Không được kiểm tra tiền điều kiện trong thân của thủ tục trong bất kỳ trường
hợp nào
Nguyên tắc này đã đi ngược với chủ trương của rất nhiều sách giáo khoa về công nghệ phần mềm hay phương pháp lập trình, thường được biết đến như là phương pháp lập trình phòng thủ - với ý tưởng: để thu được một phần mềm đáng tin cậy, bạn nên thiết kế mỗi thành phần của hệ thống sao cho nó có khả năng tự bảo vệ cao nhất Thà dư còn hơn thiếu, một người không bao giờ là quá cẩn thận khi tiếp xúc với người lạ Sự kiểm tra dư thừa có thể vô ích, nhưng ít nhất là nó không gây tác hại
Design By Contract đi theo một hướng ngược lại: những kiểm tra dư thừa có thể và thật sự gây ra tác hại Điều này mới nghe có vẻ lạ, mọi người sẽ nghĩ rằng sự kiểm tra dư thừa có thể là vô ích, nhưng không thể nào gây ra tác hại Tuy nhiên chỉ những người có sự hiểu biết chưa sâu về tính tin cậy của phần mềm và chỉ tập trung vào những phần riêng biệt của phần mềm mới có suy nghĩ như vậy Nếu hạn chế tầm nhìn trong một phạm vi hẹp của một thành phần cụ thể, ví dụ như thủ tục sqrt ở trên, ta sẽ thấy rằng thủ tục có vẻ chặt chẽ hơn nếu có phần kiểm tra thêm trong thân thủ tục Nhưng một hệ thống không chỉ giới hạn trong một thủ tục cụ thể mà nó bao gồm nhiều thủ tục trong nhiều lớp đối tượng khác nhau Để đạt được một hệ thống
Trang 9đáng tin cậy, ta phải từ bỏ cái nhìn hạn chế để có một tầm nhìn vĩ mô, bao hàm trên kiến trúc tổng thể
Nếu ta có được cái nhìn như thế thì tính đơn giản sẽ trở thành một tiêu chuẩn
quyết định Như ta đã nói từ đầu, sự phức tạp là kẻ thù chính của chất lượng Khi quan tâm đến điều này, ta sẽ thấy rằng những kiểm tra dư thừa không còn là vô hại nữa Thử hình dung một hệ thống thông thường, với hàng ngàn thủ tục, những kiểm tra dư thừa này sẽ trở thành một sự phức tạp không cần thiết khổng lồ Nếu chúng ta bắt đầu bằng con đường này, có thể chắc chắn một điều là: không bao giờ đạt được một hệ thống đáng tin cậy
Với Design By Contract, bạn sẽ nhận ra những điều kiện chắc chắn và cần thiết để thực hiện các chức năng của một liên hiệp khách hàng – nhà cung cấp (hay gọi là một hợp đồng); và để cho rõ ràng, đối với mỗi điền kiện, cần xác định rõ trách nhiệm là của ai: khách hàng hay nhà cung cấp Có thể có nhiều câu trả lời, theo một chừng mực nào đó, điều này là những phong cách thiết kế khác nhau Nhưng một khi bạn đã quyết định thì bạn cần phải bám vào nó Nếu bạn có yêu cầu
về sự chính xác của tiền điều kiện, xác định rõ rằng yêu cầu là một phần trách nhiệm của khách hàng (những lời gọi đến thủ tục) thì không được kiểm tra trong thân của thủ tục; còn nếu yêu cầu này không nằm trong tiền điều kiện thì thủ tục cần phải kiểm tra nó
Đối với một hệ thống phần mềm, dù lớn hay nhỏ, chất lượng riêng của từng thành phần là chưa đủ Cái giá trị nhất chính là sự bảo đảm rằng mỗi tương tác giữa hai thành phần bất kỳ đều phải có một sự phân công rõ ràng về quyền lợi và nghĩa
vụ, tức là cần phải có một bản hợp đồng
7.3 Những xác nhận không phải là một cơ chế kiểm tra đầu vào
Để tránh hiểu lầm, lưu ý rằng những hợp đồng ta đang bàn là giữa một thủ tục (nhà cung cấp) và những thủ tục khác (khách hàng – những thủ tục gọi đến thủ tục đó) Chúng ta đang quan tâm đến mối quan hệ phần mềm – phần mềm, không
Trang 10phải là phần mềm – con người hay phần mềm – ngoại cảnh Vì vậy, tiền điều kiện
sẽ không quan tâm đến sự đúng sai của dữ liệu người dùng nhập vào, ví dụ xét thủ
điều kiện:
require
input > 0
không phải là một kỹ thuật đáng tin cậy Trong trường hợp này không thể có sự thay thế cho một điều kiện if … then thông thường
Sự xác nhận không phải là một giải pháp kiểm tra sự đúng đắn của dữ liệu nhập Tiêu chuẩn của Modular Protection (bảo vệ tính môđun cho phần mềm) khuyến khích rằng cần xác nhận tính hợp lệ của dữ liệu nhập từ các đối tượng ngoài
hệ thống sao cho càng gần với mã nguồn càng tốt, có thể sử dụng “bộ lọc” nếu cần thiết:
Để nhận dữ liệu từ ngoài hệ thống (đường màu xanh nhạt), bạn không thể dựa vào tiền điều kiện Nhưng với những môđun màu vàng, dữ liệu phải thỏa mãn
Trang 11tiền điều kiện mới có thể đi vào những môđun xử lý (màu xanh đậm) Vậy trong trường hợp này, ta chỉ có thể sử dụng xác nhận cho đường màu xanh đậm, thể hiện
sự tương tác phần mềm – phần mềm Và hậu điều kiện mà những môđun nhập nhận được sẽ phải phù hợp với tiền điều kiện của những thủ tục xử lý
Chương 8: Làm việc với những xác nhận
Trong phần này chúng sẽ tìm hiểu chi tiết hơn về cách dùng tiền điều kiện và hậu điều kiện thông qua những ví dụ cơ bản Những vấn đề đơn giản và phức tạp
của xác nhận sẽ được minh họa rõ ràng qua những ví dụ dưới đây
8.1 Lớp stack
có thể nghĩ ra một phiên bản đầy đủ cùng với một cài đặt được giải thích rõ ràng
Để một lớp có thể sử dụng trực tiếp được, ta phải chọn một cài đặt Hãy xem một cài đặt của ngăn xếp bằng cách dùng mảng
Hình 8-1: Stack được cài đặt bằng mảng
Mảng được gọi là representation, có phạm vi từ 1 đến capacity Thuộc tính count có kiểu số nguyên (integer) cho biết số phần tử trong ngăn xếp
Trang 12Gọi a là một đối tượng của lớp này, phương thức a.put(x,i) gán giá trị x cho phần tử thứ của stack; để lấy giá trị tại phần tử thứ i ta dùng phương thức a.item(i)
hoặc a @ i Lưu ý rằng phạm vi của mảng là từ 1 đến capacity, vì vậy i phải nằm giữa 1 và capacity
indexing
description: “Stacks: Cấu trúc dữ liệu với quy tắc truy xuất LIFO, và có độ lớn cố định.”
class STACK2 [G] creation
make
feature Initialization
make (n: INTEGER) is
Cấp phát cho Stack độ lớn n phần tử
require
positive_capacity: n >= 0
do
capacity := n
!! representation make (1, capacity)
ensure
capacity_set: capacity = n array_allocated: representation /= Void stack_empty: empty
end feature Access
capacity: INTEGER
count: INTEGER
Số phần tử của Stack
item: G is
Phần tử trên cùng