Tìm hiểu công nghệ Design By Contract và Xây dựng công cụ hỗ trợ cho C# require not_empty: not empty -- i.e. count 0 do Result := representation @ count end feature – Status report empty: BOOLEAN is -- Kiểm tra Stack rỗng? do Result := (count = 0) ensure empty_definition: Result = (count = 0) end full: BOOLEAN is -- Kiểm tra Stack đầy? do Result := (count = capacity) ensure full_definition: Result = (count = capacity) end feature – Element change put (x: G) is -- Thêm phần tử x vào Stack. require not_full: not full --i.e. count...
Trang 1require
not_empty: not empty i.e count > 0
do
Result := representation @ count
end feature – Status report
empty: BOOLEAN is
Kiểm tra Stack rỗng?
do
Result := (count = 0)
ensure
empty_definition: Result = (count = 0)
end
full: BOOLEAN is
Kiểm tra Stack đầy?
do
Result := (count = capacity)
ensure
full_definition: Result = (count = capacity)
end feature – Element change
put (x: G) is
Thêm phần tử x vào Stack
require
not_full: not full i.e count < capacity in
this representation
do
count := count + 1
representation put (count, x)
ensure
not_empty: not empty
added_to_top: item = x
Trang 2one_more_item: count = old count + 1
in_top_array_entry: representation @ count = x
end
remove is
Xóa phần tử trên cùng của Stack
require
not_empty: not empty i.e count > 0
do
count := count – 1
ensure
not_full: not full one_fewer: count = old count – 1
end feature {NONE} Implementation
representation: ARRAY [G]
Mảng dùng để chứa các phần tử của Stack
invariant
… Sẽ tìm hiểu trong phần sau
end class STACK2
Phần biểu diễn về lớp ở trên cho ta thấy sự đơn giản khi làm việc với những xác nhận Ngoại trừ mệnh đề invariant còn thiếu sẽ được bổ sung trong phần sau,
chúng ta hãy cùng nhau xem xét tỉ mỉ những thuộc tính khác nhau của nó
8.2 Mệnh lệnh và yêu cầu
Những xác nhận trong lớp STACK2 minh họa một khái niệm cơ bản mà ta đã
có cái nhìn lướt qua về sự chuyển tiếp từ những kiểu dữ liệu trừu tượng sang những lớp: sự khác nhau giữa những khung nhìn “imperative” và “applicative”
Những xác nhận trong empty và full có thể làm bạn băn khoăn Xét thủ tục
full trong lớp trên:
Trang 3full: BOOLEAN is
Stack có đầy không?
do
Result := (count = capacity)
ensure
full_definition: Result = (count = capacity)
end
Hậu điều kiện yêu cầu rằng thực thể Result có giá trị luận lý bằng với giá trị của biểu thức count = capacity Điều đó có nghĩa là Result có giá trị true nếu count bằng với capacity và nó có giá trị false nếu ngược lại
Result = (count = capacity) (1)
Bởi vì trong thân thủ tục đã gán
Result := (count = capacity) (2)
Vậy thì liệu hậu điều kiện ở đây có dư thừa không?
Có sự khác biệt rất lớn giữa (1) và (2), vì vậy không hề có sự dư thừa
(2) là một câu lệnh, thể hiện một hành động, gán giá trị true hay false của
biểu thức count = capacity cho biến Result Trong khi đó, (1) chỉ là một xác nhận, không làm gì hết Nó chỉ đặc tả thuộc tính của trạng thái cuối cùng được mong đợi
Một câu lệnh, tức (2), thì mang tính chất ra lệnh, còn một xác nhận, tức (1),
thì chỉ mang tính chất mô tả Câu lệnh mô tả cho câu hỏi “như thế nào”, còn xác nhận mô tả cho câu hỏi “cái gì” Một câu lệnh là một phần của cài đặt, còn một xác
nhận chỉ là một thành phần của đặc tả
Câu lệnh thì mang tính mệnh lệnh, bắt buộc, còn xác nhận thì chỉ mang tính
yêu cầu Hai thuật ngữ này nhấn mạnh sự khác nhau cơ bản giữa tin học và toán học
− Những thao tác của tin học có thể làm thay đổi trạng thái của máy tính Những chỉ thị của các ngôn ngữ lập trình thông thường là những câu lệnh tác động trực tiếp đến máy tính
Trang 4− Những lý luận toán học không thể thay đổi bất cứ gì Ví dụ như khi ta lấy căn bậc hai của 2 thì số 2 trước khi lấy căn và sau khi lấy căn vẫn như nhau
Tóm lại, xác nhận là mô tả một kết quả được mong đợi, còn chỉ thị (thân vòng lặp) là ra lệnh bằng cách nào đó đạt được kết quả Ta không được nhằm lẫn giữa hai khái niệm này cũng như giữa hai khái niệm “:=” và “=” Những người sử dụng một lớp nào đó để tạo ra một môđun của riêng mình sẽ quan tâm đến các xác nhận hơn là các chỉ thị
Nguyên nhân của sự gần giống nhau giữa dấu gán (:=) và dấu bằng (=) là việc gán trong nhiều trường hợp là một cách đơn giản để đạt đến sự ngang bằng Cài đặt Result := (count = capacity) thật sự là một ví dụ rõ ràng dễ nhầm lẫn Nhưng trong những ví dụ cao hơn thì sự khác nhau giữa đặc tả và cài đặt sẽ lớn hơn, ngay cả trong một ví dụ đơn giản là hàm tính căn của số thực x có hậu điều kiện là abs(Result^2-x)<=tolerance (với abs là trị tuyệt đối, còn tolerance là dung sai) Các chỉ thị trong thân hàm sẽ có tầm quan trọng thấp hơn
vì chúng là những cài đặt cho thuật toán chung của việc tính căn bậc hai
Thậm chí đối với thủ tục put trong STACK2, có cùng đặc tả nhưng có thể có
sự thực thi khác nhau, mặc dù sự khác biệt có thể là rất nhỏ Chẳng hạn nếu thân thủ tục như sau:
if count=capacity then Result:=True else Result:=False end
có thể được đơn giản (nhờ những quy tắc khởi tạo mặc định) thành:
if count = capacity then Result:=True end
Do đó, sự hiện diện của những thành tố giống nhau trong thủ tục và hậu điều kiện không phải là bằng chứng của sự dư thừa Nó là bằng chứng của tính bền vững giữa cài đặt và đặc tả - có thể nói là của tính đúng đắn
Qua đây, ta thấy một đặc tính của những xác nhận mà sẽ được phát triển cao hơn: sự thích hợp của chúng đối với tác giả của các lớp client, người mà chúng ta không nên thắc mắc khi đọc những cài đặt thủ tục, nhưng là người cần một mô tả trừu tượng hơn về vai trò của thủ tục Ý tưởng này đưa đến ý niệm về một dạng thức ngắn gọn (short form) sẽ được thảo luận sau
Trang 5Vì nguyên nhân thực tiễn, ta sẽ cho phép các xác nhận bao hàm một vài thành tố mệnh lệnh (các hàm)
Để tóm tắt cho chương này, ta có bảng sau được chia thành 2 cột để thấy sự tương phản giữa 2 hạng mục của các thành tố phần mềm:
Câu lệnh Biểu thức
8.3 Lưu ý về những cấu trúc rỗng
Tiền điều kiện của thủ tục khởi tạo make trong lớp STACK1 yêu cầu một lời chú giải Nó đưa ra n>=0, theo sau là một stack rỗng Nếu n bằng 0, make sẽ gọi thủ tục khởi tạo mảng, cũng có tên là make, với đối số 1 và 0 cho khoảng tiệm cận dưới và trên Đây không phải là một lỗi, mà là xuất phát từ quy ước trong thủ tục khởi tạo ARRAY: dùng đối số đầu tiên lớn hơn đối số thứ hai đưa ra một mảng rỗng
n có giá trị là 0, hoặc là đối số đầu tiên lớn hơn đối số thứ hai của mảng không hề sai mà đơn giản là stack hay mảng này rỗng Lỗi chỉ xảy ra khi có lời gọi muốn truy xuất đến một phần tử trong cấu trúc, chẳng hạn một lời gọi put cho stack hay item cho mảng, cả hai tiền điều kiện của 2 thủ tục này sẽ luôn luôn sai vì cấu trúc rỗng (“Khách hàng luôn luôn sai.”)
Khi định nghĩa một cấu trúc dữ liệu nói chung như stack hay mảng, bạn nên xác định trường hợp một cấu trúc rỗng là có nghĩa hay không Trong một số trường
hợp thì không: Ví dụ: hầu hết định nghĩa của khái niệm tree bắt đầu từ sự giả định
rằng có ít nhất một node (là node gốc) Nhưng trường hợp rỗng đưa ra không phải là không có khả năng hợp lý, cũng như mảng và stack, bạn nên lên kế hoạch thiết kế
Trang 6cấu trúc dữ liệu của bạn, thừa nhận rằng mỗi lần khách hàng tạo ra những thể hiện rỗng thì không chấp nhận nó Một hệ thống ứng dụng, ví dụ cần một stack có n phần tử, với n là tiệm cận trên của số phần tử được chứa trong stack, sẽ được tính toán bởi ứng dụng ngay trước khi nó tạo ra stack Trong một số lần chạy, con số có thể có giá trị 0 Đây không phải là lỗi mà nó là một trường hợp vô cùng
8.4 Thiết kế tiền điều kiện: tolerant hay demanding?
Ta chỉ có thể gắn những điều kiện cho một trong hai đối tác của hợp đồng: khách hàng (client) hay nhà cung cấp (supplier)
Có 2 khả năng:
− Nếu gán trách nhiệm cho khách hàng, điều kiện sẽ là một phần của
tiền điều kiện
− Nếu đặt ở nhà cung cấp, những điều kiện này sẽ xuất hiện trong cấu trúc if then của mã lệnh
Ta gọi trường hợp thứ nhất là demanding và thứ hai là tolerant Lớp stack ở trên là một minh hoạ cho kiểu demanding, phiên bản tolerant không có sự hiện diện
của tiền điều kiện:
remove is
Xóa phần tử trên cùng của Stack
do
if empty then
print ("Error: attempt to pop an empty stack")
else
count := count – 1
end end
Dùng kiểu nào thì tốt hơn?
Thoáng nhìn thì tolerant có vẻ tốt hơn (cả tính đáng tin cậy và tính tái sử dụng), nếu có nhiều khách hàng mà chỉ có một nhà cung cấp, tính tái sử dụng được
Trang 7thể hiện rõ Nhà cung cấp sẽ thực hiện việc kiểm tra chung chứ không cần mỗi
khách hàng phải có sự kiểm tra riêng
Nhưng nếu chúng ta xem xét vấn đề gần hơn, lập luận trên sẽ không còn đúng Điều kiện ở đây mô tả những gì cần thiết để thủ tục có thể làm công việc của
nó Trong ví dụ trên, nếu stack là rỗng, một thông báo lỗi sẽ được đưa ra, điều này không thích hợp Bởi vì chỉ có khách hàng – mođun dùng stack trong ứng dụng cụ thể mới quyết định được việc xoá phần tử của một chuỗi rỗng như trên có ý nghĩa
gì
8.5 Một môđun tolerant
Mặc dù ta đã thấy được rằng tolerant không phải là một sự tiếp cận đúng,
nhưng cũng nên nghiên cứu xem lớp đối tượng sẽ trông như thế nào nếu ta quyết định tiếp cận theo cách này
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; phiên bản tolerant, đưa những dòng lệnh kiểm tra vào thẳng mã nguồn."
class STACK3 [G] creation
make
feature Initialization
make (n: INTEGER) is
Cấp phát cho stack độ lớn n phần tử nếu
n>0;
Nếu không thì gán error = Negative_size
Không có tiền điều kiện!
do
if capacity >= 0 then
capacity := n
!! representation.make (capacity)
Trang 8else
error := Negative_size
end ensure
error_code_if_impossible:
(n<0)=(error =Negative_size) no_error_if_possible: (n >= 0) = (error = 0) capacity_set_if_no_error:(error = 0)
implies (capacity = n) allocated_if_no_error: (error=0)
implies (representation/= Void)
end
feature Access
item: G is
là phần tử trên cùng của stack (nếu stack không rỗng)
Nếu Stack rỗng thì gán error = Underflow
Không có tiền điều kiện!
do
if not empty then
check representation /= Void end
Result := representation.item error := 0
else
error := Underflow
Trong trường hợp này, Result có giá trị mặc định
end ensure
error_code_if_impossible: (old empty) =
(error = Underflow)
Trang 9no_error_if_ possible: (not (old empty)) =
(error = 0)
end
feature Status report
empty: BOOLEAN is
Số phần tử của Stack
do
representation.empty
end
error: INTEGER
Xác định lỗi
full: BOOLEAN is
Số phần tử của Stack
do
Result := (capacity = 0) or else
representation.full
end
Overflow, Underflow, Negative_size: INTEGER is
unique
Những lỗi có thể xảy ra
feature Element change
put (x: G) is
Thêm vào phần tử x; nếu không được thì gán
giá trị cho error
Không có tiền điều kiện!
Trang 10do
if full then
error := Overflow
else
check representation /= Void end
representation.put (x); error := 0
end ensure
error_code_if_impossible: (old full) = (error
= Overflow)
no_error_if_possible: (not old full) = (error
= 0)
not_empty_if_no_error: (error = 0) implies not
empty
added_to_top_if_no_error: (error = 0) implies
item = x
one_more_item_if_no_error: (error = 0) implies
count = old count + 1
end
remove is
Xóa phần tử trên cùng của Stack; nếu không
được thì gán giá trị cho error
Không có tiền điều kiện!
do
if empty then
error := Underflow
else
check representation /= Void end
representation.remove error := 0
end
Trang 11ensure
error_code_if_impossible: (old empty) = (error
= Underflow)
no_error_if_possible: (not old empty) = (error
= 0)
not_full_if_no_error: (error = 0) implies not
full one_fewer_item_if_no_error: (error = 0)
implies count = old count – 1
end
feature {NONE} – Cài đặt
representation: STACK2 [G]
Cài đặt của stack (không được bảo vệ)
capacity: INTEGER
Số phần tử tối đa của stack
end class STACK3
Ví dụ trên đã cho ta thấy sự nặng nề của một lớp dùng cách tiếp cận tolerant Đây là một minh chứng cho thấy tolerant sẽ dẫn đến một phần mềm phức tạp và không cần thiết Ngược lại, với demanding, theo tinh thần của Design By Contract,
sẽ giúp những client trong phát hiện lỗi trong tất cả các trường hợp theo cách tốt nhất
Chương 9: Những điều kiện bất biến của lớp
Tiền điều kiện và hậu điều kiện mô tả những thuộc tính của những thủ tục riêng biệt Điều này cũng cần thiết cho việc biểu diễn những thuộc tính toàn cục của những thể hiện (instance) của một lớp vì chúng phải được lưu giữ trong tất cả thủ tục Những thuộc tính như thế sẽ tạo nên điều kiện bất biến của lớp (class invariant)
Trang 12Chúng giữ những thuộc tính ngữ nghĩa sâu hơn và mô tả những ràng buộc toàn vẹn của một lớp
9.1 Định nghĩa và ví dụ
Xem lại phần cài đặt ngăn xếp (stack) bằng cách sử dụng mảng mà không có
sự bảo đảm (STACK2)
class STACK2 [G] creation
make
feature
… make, empty, full, item, put, remove …
capacity: INTEGER count: INTEGER
feature {NONE} –- Cài đặt
representation: ARRAY [G]
end
Những thuộc tính của lớp bao gồm: representation kiểu mảng, capacity và
count kiểu số nguyên tạo nên một stack tượng trưng Mặc dù những tiền điều kiện
và hậu điều kiện của thủ tục được đưa ra trước đây có thể biểu diễn một vài thuộc tính ngữ nghĩa của stack nhưng chúng thất bại trong việc biểu diễn tính nhất quán khi các thuộc tính liên kết với nhau Ví dụ, count luôn luôn có giá trị từ 0 đến
capacity:
0 <= count; count <= capacity
(điều này cũng hàm ý rằng capacity >= 0), và capacity là kích thước của mảng:
capacity = representation.capacity
Một điều kiện bất biến của lớp (class invariant) cũng như là một xác nhận, biểu diễn những ràng buộc nhất quán chung được dùng cho mọi thể hiện của lớp
Nó khác với tiền điều kiện và hậu điều kiện là những cái chỉ mô tả cho những thủ tục riêng biệt
Sự xác nhận ở trên chỉ liên quan đến những thuộc tính Những điều kiện bất biến cũng có thể biểu diễn mối quan hệ ngữ nghĩa giữa những hàm với nhau hay