Tải bản đầy đủ (.pdf) (12 trang)

TÌM HIỂU CÔNG NGHỆ DESIGN BY CONTRACT VÀ XÂY DỰNG CÔNG CỤ HỖ TRỢ CHO C# - 6 pps

Bạn đang xem bản rút gọn của tài liệu. Xem và tải ngay bản đầy đủ của tài liệu tại đây (564.32 KB, 12 trang )









Tìm hiểu công nghệ Design By Contract và Xây dựng công cụ hỗ trợ cho C#
61
trong giới hạn cho phép. Điều kiện bất biến biểu diễn mối quan hệ giữa count,
lower và upper; nó cho phép count được cài đặt như một hàm chứ không phải
một thuộc tính.
indexing
description: "Mảng giá trị cùng kiểu, truy xuất các phần
tử thông qua các chỉ số mảng"
class ARRAY [G] creation
make
feature Khởi tạo
make (minindex, maxindex: INTEGER) is
Xác định 2 biên của mảng với minidex và
maxindex
Mảng rỗng nếu minindex > maxindex.
require
meaningful_bounds: maxindex >=
minindex - 1
do

ensure
exact_bounds_if_non_empty: (maxindex >=
minindex) implies


((lower = minindex) and (upper =
maxindex))
conventions_if_empty: (maxindex < minindex)
implies ((lower = 1) and (upper = 0))
end
feature – Truy cập
lower, upper, count: INTEGER
Chỉ số cao nhất vào thấp nhất hợp lệ
; kích thước
mảng.
infix "@", item (i: INTEGER): G is
Giá trị mảng tại chỉ số i








Tìm hiểu công nghệ Design By Contract và Xây dựng công cụ hỗ trợ cho C#
62
require
index_not_too_small: lower <= i
index_not_too_large: i <= upper
do … end
feature -– Thay đổi thành phần
put (v: G; i: INTEGER) is
Gán giá trị v cho phần tử có chỉ số
require

index_not_too_small: lower <= i
index_not_too_large: i <= upper
do

ensure
element_replaced: item (i) = v
end
invariant
consistent_count: count = upper – lower + 1
non_negative_count: count >= 0
end class ARRAY

Phần trống duy nhất còn lại là phần cài đặt của thân những thường trình
item và put.


Chương 11: Kết nối với kiểu dữ liệu trừu tượng
Trong phần này, ta sẽ củng cố thêm khái niệm “xác nhận” bằng việc tìm hiểu
về sự kết nối giữa các xác nhận và những thành phần của một đặc tả của một kiểu
dữ liệu trừu tượng (ADT – Abstract Data Type).
Một ADT được tạo bởi 4 thành phần:
− Tên kiểu, có thể cùng với những tham số chung (phần TYPES)









Tìm hiểu công nghệ Design By Contract và Xây dựng công cụ hỗ trợ cho C#
63
− Danh sách các hàm với những ký hiệu của chúng (phần
FUNCTIONS)
− Tiên đề (AXIOMS) mô tả những thuộc tính của kết quả
− Những quy định để sử dụng được hàm (phần PRECONDITIONS)
Ví dụ: ADT của lớp STACK
TYPES
• STACK [G]

FUNCTIONS
• put: STACK [G] × G → STACK [G]
• remove: STACK [G] →STACK [G]
• item: STACK [G] → G
• empty: STACK [G] → BOOLEAN
• new: STACK [G]

AXIOMS
For any x: G, s: STACK [G]
A1 • item (put (s,
x)) = x
A2 • remove (put (s, x)) = s
A3 • empty (new)
A4 • not empty (put (s, x))
PRECONDITIONS
• remove (s: STACK [G]) require not empty (s)
• item (s: STACK [G]) require not empty (s)

11.1. So sánh đặc tính của lớp với những hàm ADT
Để hiểu được mối liên hệ giữa những xác nhận và ADT, trước tiên ta cần tìm

hiểu mối liên hệ giữa những đặc tính của lớp và phần tương ứng của ADT – những
hàm của ADT. Những hàm này gồm 3 loại: creator, query và command.








Tìm hiểu công nghệ Design By Contract và Xây dựng công cụ hỗ trợ cho C#
64
Sự phân loại của một hàm f: A × B × …
Æ
X phụ thuộc vào vị trí xuất hiện
của ADT (A, B, …, X) so với mũi tên , gọi là T .
Nếu T chỉ xuất hiện ở bên phải của mũi tên thì f là hàm creator. Trong lớp
đối tượng, nó là phương thức khởi tạo. Ví dụ: hàm new
Nếu T chỉ có mặt ở bên trái của mũi tên thì f là hàm query. Trong lớp đối
tượng, nó là những phương thức truy xuất đến nhữ
ng thuộc tính của các thể hiện
của một lớp. Ví dụ: hàm item và empty.
Nếu T xuất hiện ở cả 2 bên mũi tên thì f là hàm command. Những hàm này
tạo ra những đối tượng mới từ những đối tượng đã có. Trong lớp đối tượng, những
hàm này thường được biểu diễn như một phương thức để làm thay đổi một đối
tượng hơn là tạo ra một đối tượng mới. Ví dụ hàm put và remove.

11.2. Biểu diễn những tiên đề
Từ sự tương ứng giữa những hàm ADT và những đặc tính của lớp, ta có thể
suy ra sự tương ứng giữa ngữ nghĩa thuộc tính ADT và những xác nhận.

Tiền điều kiện của ADT xuất hiện lại như những mệnh đề tiền điều kiện
(precondition clauses) của các thủ tục tương ứng.
Một tiên đề, bao gồm một hàm command và một hay nhiều hàm query, xuất
hiện lại như những mệnh đề hậu điều kiện (postcondition clauses) trong những thủ
tục tương ứng.
Những tiên đề chỉ bao gồm những hàm query xuất hiện lại như những hậu
điều kiện của những hàm tương ứng hoặc những mệnh đề của điều kiện bất biến.
Tiên đề bao gồm những hàm khởi tạo xuấ
t hiện lại trong hậu điều kiện của
những thủ tục khởi tạo tương ứng.









Tìm hiểu công nghệ Design By Contract và Xây dựng công cụ hỗ trợ cho C#
65
11.3. Hàm trừu tượng


Hình
11-1: Sự biến đổi giữa những đối tượng trừu tượng và cụ thể

A là một ADT và C là một lớp được cài đặt từ A. Lúc này tương ứng với hàm
trừu tượng af của A sẽ có hàm cụ thể cf trong lớp C.
Mũi tên a mô tả cho sự trừu tượng hóa hàm, với mỗi đối tượng cụ thể (thể

hiện của lớp), sự trừu tượng hoá này sẽ sinh ta một đối tượng trừu tượng (thể hiện
của ADT). Những hàm trừu tượng này thường là từng phần.


Sự tương ứng giữa lớp và ADT
(cf ; a) = (a ; af)


Trong đó dấu “;” là toán tử kết hợp giữa các hàm. Nói cách khác, nếu ta có
hai hàm f và g thì f;g là hàm h sao cho h(x) = g(f(x)) với mọi x (f;g còn được viết
dưới dạng g o f )








Tìm hiểu công nghệ Design By Contract và Xây dựng công cụ hỗ trợ cho C#
66
Hai đường đứt khúc cùng chỉ đến đối tượng trừu tượng ABST_2. Kết quả
vẫn như nhau ngay cả khi bạn:
- Áp dụng sự biến đối cụ thể cf, sau đó trừu tượng hóa kết quả, sinh ra
a(cf(CONC_1)).
- Trừu tượng hóa trước, sau đó áp dụng sự biến đổi trừu tượng af, sinh ra
af(a(CONC_1)).

11.4. Cài đặt những điều kiện bất biến
Một số xác nhận xuất hiện trong những điều kiện bất biến nhưng chúng lại

không là bản sao trực tiếp trong đặc tả ADT. Những xác nhận này bao hàm những
thuộc tính, một số là thuộc tính ẩn mà lúc định nghĩa thì chúng sẽ không có ý nghĩa
trong ADT. Một ví dụ đơn giản là những thuộc tính dưới đây xuất hiện trong điều
kiện bất biến của lớp STACK4
count_non_negative: 0<=count
count_bounded: count<=capacity
Những xác nhận như thế này là những điều kiện bất biến của lớp, gọi là cài
đặt của điều kiện bất biến (implementation invariant). Chúng đáp ứng cho việc biểu
diễn tính vững chắc của những đại diện được chọn trong lớp (ở đây là
count,
capacity và representation) có quan hệ với những ADT tương ứng.
Hình 10-1 đã giúp ta hiểu rõ về cài đặt của điều kiện bất biến. Nó minh họa
cho những thuộc tính đặc trưng của hàm trừu tượng
a (biểu diễn bằng những mũi
tên dọc). Gọi hàm
a sẽ nối mỗi thành phần nguồn với nhiều nhất một thành phần
đích. Nếu ta đi theo chiều ngược lại của mũi tên để xét nghịch đảo của
a (có thể gọi
là quan hệ biểu diễn) , ta sẽ thấy rằng đó không phải là một hàm, bởi vì có thể có
nhiều biểu diễn của một đối tượng trừu tượng.
Xét mảng thể hiện cho STACK thông qua cặp
<representation,
count>, một STACK trừu tượng có nhiều biểu diễn khác nhau, tất cả đều có giá trị
count và các phần tử từ 1 đến count của mảng representation giống nhau,
nhưng giá trị
capacity thể hiện kích thước mảng có thể là bất kỳ giá trị nào lớn









Tìm hiểu công nghệ Design By Contract và Xây dựng công cụ hỗ trợ cho C#
67
hơn hoặc bằng count, và những phần tử có chỉ số vượt quá count có thể có giá trị
tùy ý.
Quan hệ cài đặt thường không phải là một hàm, nhưng sự nghịch đảo của nó
(mũi tên hướng lên) là một hàm thật sự vì mỗi đối tượng cụ thể biểu diễn cho nhiều
nhất một đối tượng trừu tượng. Trong ví dụ về lớp STACK của chúng ta, mỗi cặp
<representation,count> hợp lệ chỉ biểu diễn cho một STACK trừu tượng.


Hình 11-2: Hai cài đặt của cùng một đối tượng trừu tượng

Cả hai stack cụ thể trên đây đều là cài đặt (implementation) của stack trừu
tượng bao gồm 3 phần tử 342, -133 và 5. Việc a là một hàm là một yêu cầu chung:
nếu một đối tượng cụ thể là cài đặt của nhiều đối tượng trừu tượng, đại diện được
chọn sẽ trở nên mơ hồ và không thích hợp. Vì vậy mũi tên a nên vẽ theo chiều như
trên để
mô tả cho sự kết nối giữa kiểu cụ thể và trừu tượng.









Tìm hiểu công nghệ Design By Contract và Xây dựng công cụ hỗ trợ cho C#
68
Cài đặt những điều kiện bất biến là một phần của những xác nhận không có
bản đối chiếu trong đặc tả ADT. Nó không liên hệ với ADT mà là với những biểu
diễn của nó. Nó định rõ khi một đối tượng cụ thể thật sự là cài đặt của một (và chỉ
một) đối tượng trừu tượng.


Chương 12: Một chỉ thị xác nhận
Những tiền điều kiện, hậu điều kiện và điều kiện bất biến của lớp là những
thành phần trung tâm của phương pháp sử dụng chỉ thị xác nhận (assertion
instruction). Chúng tạo nên sự kết nối giữa việc xây dựng phần mềm hướng đối
tượng và những lý thuyết bên dưới về kiểu dữ liệu trừu tượng (Abstract Data Type).
Có những xác nhận mặc dù ít đặc thù hơn nhưng cũng có vai trò quan trọng
trong quy trình phát triển phần mềm. Trong đó có chỉ thị check và những vòng lặp
có bất biến và điều kiện biến đổi (loop invariant và variant)
Chỉ thị check được sử dụng để thể hiện sự tin chắc của người viết phần mềm
rằng một thuộc tính nào đó sẽ thỏa mãn những tình huống nào đó trong việc tính
toán. Cú pháp:
check
assertion_clause
1

assertion_clause
2


assertion_clause
n


end
Khi chương trình thực thi thì những xác nhận assertion_clause
X
sẽ
được bảo đảm.
Đây là cách để chắc chắn một lần nữa rằng những thuộc tính nhất định được
thỏa mãn, và quan trọng hơn, nó giúp những người đọc phần mềm hiểu được những
giả thuyết mà ta dựa vào. Việc viết phần mềm đòi hỏi sự xác nhận thường xuyên
những thuộc tính của đối tượng. Xét ví dụ về hàm sqrt(x), bấ
t cứ hàm nào gọi








Tìm hiểu công nghệ Design By Contract và Xây dựng công cụ hỗ trợ cho C#
69
đến sqrt(x) đều dựa vào xác nhận rằng x không âm. Xác nhận này có thể hiển
hiện trong ngữ cảnh, ví dụ như một câu lệnh if then
if x >= 0 then y := sqrt (x) end
nhưng nó cũng có thể không trực tiếp như trên, ví dụ như x đã được gán
trước đó:
x := a^2 + b^2
Chỉ thị check sẽ giúp biểu diễn xác nhận đó nếu nó không hiển hiện trong
câu lệnh

x := a ^2 + b^2

… Other instructions …
check
x >= 0
Because x was computed above as a sum of
squares
end
y := sqrt (x)

Không cần điều kiện if then cho lời gọi hàm sqrt trong trường
hợp này, check đã xác nhận rằng lời gọi hàm là đúng. Lưu ý rằng chỉ thị check này
không làm ảnh hưởng đến thuật toán của thủ tục.
Ví dụ trên đã cho ta thấy sự hữu ích của check, nó đóng vai trò một tiền điều
kiện của một lời gọi hàm.
Một trường hợp khác cần lưu ý, khi ta viết một lời gọi hàm có d
ạng x.f, ta
đã chắc chắn rằng
x không rỗng, cho nên ta sẽ không viết
if x != Void then
nhưng sự không rỗng của
x lại không hiển hiện trong ngữ cảnh này. Ta đã gặp
trường hợp này trong hàm
put và remove lớp STACK3.
Thân hàm
put gọi đến hàm tương ứng trong lớp STACK2:









Tìm hiểu công nghệ Design By Contract và Xây dựng công cụ hỗ trợ cho C#
70
if full then
error := Overflow
else
check representation /= Void end
representation.put (x); error := 0
end
Ở đây người đọc sẽ nghĩ rằng không an toàn khi gọi
representation.put(x); như vậy, bởi vì không có một sự kiểm tra nào trước
đó về sự không rỗng của
representation. Nhưng nếu để ý kỹ, bạn sẽ thấy rằng
nếu
full sai thì capacity phải là số dương. Vì vậy, representation không
thể là rỗng. Đây là một điều rất quan trọng và sẽ là một phần trong cài đặt các ràng
buộc của lớp. Trong thực tế, với cài đặt đầy đủ của một ràng buộc được định trước,
ta nên viết chỉ thị check như sau:
check
representation_exists: representation /= Void
Because of clause
representation_exists_if_not_full of the
cài đặt điều kiện bất biến.
end
Trong những sự tiếp cận thông thường của việc xây dựng phần mềm, mặc dù
những lời gọi hàm và những phương thức khác thường tin tưởng vào sự đúng đắn
của chúng nhờ những xác nhận khác nhau, nhưng chúng vẫn là những xác nhận
không tường minh. Lập trình viên sẽ tự thuyết phục mình rằng một thuộc tính luôn
luôn được giữ ở một thời điểm nào đó, và đưa phân tích này vào trong mã ngu

ồn;
nhưng sau một thời gian, chỉ còn lại mã nguồn, không còn những phân tích đó nữa.
Vài tháng sau, một người nào đó (có thể là chính tác giả), muốn tìm hiểu lại phần
mềm, sẽ không thể biết được những thừa nhận không tường minh đó, và sẽ phải mất
nhiều công sức để hình dung lại được nó. Chỉ thị check sẽ giúp ta tránh khỏi trường
hợp đó.









Tìm hiểu công nghệ Design By Contract và Xây dựng công cụ hỗ trợ cho C#
71
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
Khả năng lặp lại một tính toán với một số lần tùy ý một cách dễ dàng là một
khả năng vượt xa con người của máy tính. Vì thế, vòng lặp có vai trò rất quan trọng
trong ngôn ngữ lập trình.
Nhưng vòng lặp cũng chứa đựng nhiều rủi ro vì rất khó khăn để có một vòng
lặp chính xác. Những vấn đề hay gặp là:
− Lỗi “off-by-one” (biểu diễn sự lặp lại quá nhiều hay quá ít).
− Kiểm soát không tốt ở biên, ví dụ như một vòng lặp làm việc tốt trên
một mảng nhiều phần tử nhưng lại bị lỗi khi mảng rỗng hay chỉ có một phần tử).
− Vòng lặp vô tận.


13.2. Những vòng lặp đúng
Việc sử dụng khôn ngoan xác nhận sẽ giúp ta giải quyết những vấn đề này.
Một vòng lặp có thể có một xác nhận liên kết, gọi là vòng lặp có điều kiện bất biến
(loop invariant). Cũng có khái niệm “loop variant (vòng lặp có điều kiện biến đổi)”.
Đây không phải là một xác nhận mà là một biểu thức nguyên. Hai khái niệm này sẽ
giúp ta bảo đảm sự chính xác của vòng lặp.
Xét ví dụ tính giá trị
lớn nhất của một mảng số nguyên:
maxarray (t: ARRAY [INTEGER]): INTEGER is
Giá trị lớn nhất của mảng t
require
t.capacity >= 1
local
i: INTEGER
do
from
i := t.lower








Tìm hiểu công nghệ Design By Contract và Xây dựng công cụ hỗ trợ cho C#
72
Result := t @ lower
until i = t.upper loop
i := i + 1

Result := Result.max (t @ i)
end
end
Ta thấy rằng ở đây ta có thể gán i := t.lower và Result := t@lower
mà không cần bận tâm đến việc mảng t tại vị trí
lower có phần tử nào không, có
được điều này là nhờ vào việc sử dụng xác nhận, chính là tiền điều kiện:
require
t.capacity >= 1
Đặc điểm của điều kiện bất biến này là mỗi lần lặp, Result luôn là giá trị lớn
nhất của phần mảng số nguyên đã được xét.

13.3. Những thành phần của một vòng lặp đúng
Ví dụ trên minh họa cho một lược đồ tổng quát của việc tính toán bằng vòng
lặp. Bạn đã xác định được rằng lời giải cho vấn đề là một phần tử của một mặt
phẳng n chiều POST, vì vậy việc giải quyết vấn đề chính là việc tìm một phần tử
của POST. Trong một số trường hợp, POST chỉ có một phần tử
(chính là lời giải),
nhưng tổng quát thường có nhiều lời giải thích hợp. Những vòng lặp trở nên hữu ích
khi bạn không có cách nào làm việc trực tiếp với POST nhưng lại thấy được một
con đường gián tiếp: đầu tiên là nhắm vào một mặt phẳng m chiều INV chứa POST
(m > n); sau đó tiếp cận POST, lặp đi lặp lại nhưng vẫn bám vào INV. Xem hình
minh họa dưới đ
ây:

×