PDF:

Kiến trúc tiến hóa và thiết kế nổi dần: Ngôn ngữ, tính biểu
cảm và thiết kế, Phần 2
Tiếp tục khám phá tính biểu cảm trong mã lệnh của bạn tạo khả năng
cho thiết kế nổi dần như thế nào
Neal Ford ([email protected])
Kiến trúc phần mềm
ThoughtWorks
08 01 2010
Khả năng xem và thu lượm các mẫu (pattern) diễn đạt đặc trưng là rất quan trọng đối với thiết
kế nổi dần. Và điều quan trọng sống còn đối với thiết kế là tính biểu cảm của mã lệnh. Trong
loạt bài viết gồm hai phần, Neal Ford sẽ bàn về chỗ giao nhau giữa tính biểu cảm và mẫu diễn
đạt đặc trưng, giải thích các khái niệm này bằng cả mẫu diễn đạt đặc trưng lẫn mẫu thiết kế hình
thức hóa. Ông viết lại một số mẫu cổ điển của Gang of Four trong các ngôn ngữ động cho JVM
để cho bạn thấy rằng các ngôn ngữ biểu cảm hơn cho phép bạn thấy các phần tử thiết kế bị
che khuất bởi các ngôn ngữ mờ tối hơn như thế nào. (N.D: Gang of Four hay GoF - Nhóm bốn
người - là cuốn sách của bốn tác giả : Erich Gamma, Richard Helm, Ralph Johnson và John
Vlissides, được coi là nền tảng của các mẫu thiết kế khác, được phân loại làm 3 nhóm: tạo lập
(Creation), cấu trúc (Structure) và hành vi (Behavior)).
Xem thêm bài trong loạt bài này
Về loạt bài viết này
Loạt bài viết này nhằm cung cấp một phối cảnh tươi mới về các khái niệm thường được thảo
luận nhưng khó nắm bắt về kiến trúc và thiết kế phần mềm. Thông qua các ví dụ cụ thể, Neal
Ford mang đến cho bạn một nền tảng vững chắc cho cách làm thực tế lanh lẹn của kiến trúc
tiến hóa và thiết kế nổi dần. Bằng cách trì hoãn các quyết định quan trọng về thiết kế và kiến
trúc cho đến thời điểm quyết định cuối cùng, bạn có thể ngăn ngừa được những phức tạp
không cần thiết không để chúng ngầm phá hoại các dự án phần mềm của bạn.
Đây là phần thứ hai của loạt bài viết gồm hai phần bài minh họa về tính biểu cảm của ngôn ngữ máy
tính giúp cho thiết kế nổi lên bằng cách cho phép bạn tập trung nhiều hơn vào bản chất hơn là vào
nghi lễ như thế nào. Sự cách biệt lớn giữa ý định và kết quả là đặc trưng của nhiều ngôn ngữ đã có
từ hàng chục năm nay (bao gồm cả ngôn ngữ Java™), khi nó thêm những nghi lễ không cần thiết
cho việc giải quyết vấn đề. Các ngôn ngữ biểu cảm hơn làm cho việc tìm các mẫu diễn đạt đặc trưng
trở nên dễ dàng hơn, vì mã chứa ít tạp nhiễu hơn. Tính biểu cảm này là dấu hiệu của các ngôn ngữ
hiện đại như Groovy và Scala; của ngôn ngữ cũ hơn nhưng có tính biểu cảm hơn như ngôn ngữ
© Copyright IBM Corporation 2010
Kiến trúc tiến hóa và thiết kế nổi dần: Ngôn ngữ, tính biểu cảm và
thiết kế, Phần 2
Nhẫn hiệu đăng ký
Trang 1 của 13
developerWorks®
ibm.com/developerWorks/vn/
Ruby, mà JRuby là một biến thể JVM của ngôn ngữ đó; hoặc của những ngôn ngữ cũ hơn nhưng đã
được tân trang lại (reimagined) như là ngôn ngữ Clojure, là ngôn ngữ Lisp hiện đại trên JVM (xem
mục Tài nguyên). Trong bài viết này tôi tiếp tục phần giải thích mà tôi đã bắt đầu trong Phần 1—
triển khai thực hiện các mẫu truyền thống của Gang of Four từ cuốn Mẫu thiết kế bằng các ngôn
ngữ có tính diễn cảm hơn.
Mẫu Decorator
Cuốn Gang of Four định nghĩa mẫu Decorator (cái trang trí) như sau:
Mẫu Decorator gắn thêm các trách nhiệm bổ sung cho đối tượng theo phương thức
động. Các mẫu Decorators cung cấp một lựa chọn linh hoạt để tạo lớp con nhằm mở
rộng chức năng.
Nếu bạn đã từng sử dụng các gói java.io.* thì bạn ý thức được một cách sâu sắc về mẫu
Decorator. Rõ ràng là các nhà thiết kế các thư viện I/O đã đọc phần Decorator của cuốn Gang of
Four và thực sự đã yêu thích nó! Đầu tiên, tôi sẽ cho bạn xem việc thực hiện theo cách truyền thống
cho một mẫu Decorator bằng ngôn ngữ Groovy, sau đó làm cho nó trở nên động hơn trong các ví
dụ tiếp theo.
Cái trang trí truyền thống
Liệt kê 1 cho thấy một lớp Logger cùng với hai cái trang trí dành cho nó ( TimeStampingLogger và
UpperLogger), cả hai cái trang trí này được thực hiện bằng ngôn ngữ Groovy:
Liệt kê 1. Lớp Logger và hai cái trang trí
class Logger {
def log(String message) {
println message
}
}
class TimeStampingLogger extends Logger {
private Logger logger
TimeStampingLogger(logger) {
this.logger = logger
}
def log(String message) {
def now = Calendar.instance
logger.log("$now.time: $message")
}
}
class UpperLogger extends Logger {
private Logger logger
UpperLogger(logger) {
this.logger = logger
}
def log(String message) {
logger.log(message.toUpperCase())
}
}
Kiến trúc tiến hóa và thiết kế nổi dần: Ngôn ngữ, tính biểu cảm và
thiết kế, Phần 2
Trang 2 của 13
ibm.com/developerWorks/vn/
developerWorks®
Lớp Logger là một trình ghi nhật ký đơn giản, nó viết thông điệp ghi nhật ký ra màn hình. Lớp
TimeStampingLogger thêm dấu ấn thời gian thông qua việc trang trí, và lớp UpperLogger chuyển
thông điệp ghi nhật ký sang dạng chữ hoa. Để sử dụng một trong các cái trang trí này, bạn bao bọc
một cá thể Logger bằng một cái trang trí thích hợp, như trong liệt kê 2:
Liệt kê 2. Sử dụng các cái trang trí để bọc một trình ghi nhật ký
def logger = new UpperLogger(
new TimeStampingLogger(
new Logger()))
logger.log("Groovy Rocks")
Kết quả đầu ra từ Liệt kê 2 cho bạn thấy một thông điệp ghi nhật ký với dấu ấn thời gian đã chuyển
sang dạng chữ hoa:
Tue May 22 07:13:50 EST 2007: GROOVY ROCKS
Cho đến đây, điều khác thường duy nhất về cái trang trí này là việc thực hiện nó bằng Groovy.
Nhưng tôi có thể thực hiện một cái trang trí mà không cần thêm cấu trúc phụ như trong cách tiếp
cận dựa trên lớp.
Trang trí tại chỗ
Các mẫu thiết kế truyền thống trong cuốn Gang of Four giả định rằng giải pháp cho mọi bài toán
đều yêu cầu xây dựng thêm các lớp. Tuy nhiên, các ngôn ngữ hiện đại trên JVM có những phương
tiện khác, chẳng hạn như các lớp mở, cho phép bạn mở lại các lớp hiện có và thêm các phương
thức mới cho chúng mà không đòi hỏi tạo lớp con. Điều này đặc biệt tiện dụng khi bạn cần thay đổi
hành vi của một lớp được sử dụng một phần bởi cơ sở hạ tầng (ví dụ: Các sưu tập API), đòi hỏi một
lớp nhất định. Bạn có thể sửa đổi một lớp hiện có, chuyển nó như một tham số và tận dụng các API
mà không đòi hỏi API cơ sở phải khai báo một lớp trừu tượng hay một giao diện. Các lớp mở cũng
cho phép bạn thực hiện sửa đổi “tại chỗ” mà không cần phải tạo lớp con.
Tuy nhiên, việc thay đổi định nghĩa cho toàn bộ lớp nghe có vẻ đáng sợ: bạn có thể không muốn
thay đổi ở tất cả mọi nơi. May mắn thay, cả hai ngôn ngữ Groovy và Ruby cho phép bạn thêm các
phương thức mới vào các cá thể đơn lẻ của lớp. Nói cách khác, bạn có thể thêm một phương thức
mới chỉ vào một cá thể của lớp Logger mà không làm ảnh hưởng đến tất cả các cá thể khác của nó.
Liệt kê 3 cho thấy việc sử dụng lớp ExpandoMetaClass trong Groovy ghi đè lên phương thức log()
trên một cá thể đơn lẻ của lớp Logger:
Liệt kê 3. Ghi đè lên phương thức log() của một cá thể của lớp Logger
def logger = new Logger()
logger.metaClass.log = { String m ->
println m.toUpperCase()
}
logger.log "this log message brought to you in upper case"
Một khi bạn hiểu cơ chế hoạt động như thế nào, thì việc đọc mã này trở nên đơn giản hơn nhiều so
với việc đọc mã tương ứng khi sử dụng các lớp bổ sung. Tất cả các mã trang trí liên quan sẽ xuất
Kiến trúc tiến hóa và thiết kế nổi dần: Ngôn ngữ, tính biểu cảm và
thiết kế, Phần 2
Trang 3 của 13
developerWorks®
ibm.com/developerWorks/vn/
hiện ở một nơi thay vì bị phân tán rải rác trong vài tệp tin (vì trong ngôn ngữ Java, mỗi lớp công
cộng (public) phải nằm trong một tệp tin riêng của mình).
Khả năng này cũng tồn tại trong Ruby bằng cách sử dụng một tính năng của Ruby được biết đến
như là phương thức đơn độc singleton method (là một cái tên hay gây nhầm lẫn vì chữ (singleton)
được sử dụng với quá nhiều nghĩa) hoặc như là lớp riêng (eigenclass) tùy từng chỗ. Cùng mã đó
được thực hiện trong JRuby có trong liệt kê 4:
Liệt kê 4. Trang trí tại chỗ bằng cách sử dụng eigenclass của Ruby
class Logger
def log(msg)
puts msg
end
end
l = Logger.new
def l.log m
puts m.upcase
end
l.log "this log message brought to you in upper case"
Phiên bản Ruby không sử dụng phương tiện thêm ngoài chẳng hạn như ExpandoMeta Class. Trong
Ruby, bạn có thể định nghĩa một phương thức nội tuyến cho một cá thể cụ thể bằng cách đặt tên
biến ở phần đầu của khai báo phương thức. Ruby có sự linh hoạt tuyệt vời về cú pháp, áp đặt ít quy
tắc hơn về khi nào và ở đâu bạn có thể định nghĩa phương thức.
Tính năng này cũng áp dụng được với các lớp Java được xây dựng sẵn. Ví dụ: Lớp ArrayList đáng
lẽ phải có định nghĩa phương thức first() và last(), nhưng than ôi, nó đã không được làm như
vậy. Tuy nhiên, thật dễ dàng để thêm các phương thức đó trong Groovy, như được thể hiện trong
liệt kê 5:
Liệt kê 5. Việc thêm phương thức first() và last () của Groovy cho lớp ArrayList
ArrayList.metaClass.getFirst {
delegate.size > 0 ? get(0) : null
}
ArrayList.metaClass.getLast {
delegate.size > 0 ? get(delegate.size - 1) : null
}
ArrayList l = new ArrayList()
l << 1 << 2 << 3
println l.first
println l.last
ArrayList emptyList = new ArrayList()
println emptyList.first
println emptyList.last
Phương tiện ExpandoMetaClass cho phép bạn định nghĩa các thuộc tính mới của lớp (bằng cách sử
dụng mẫu đặt tên get/set quen thuộc của Java). Một khi bạn đã định nghĩa các thuộc tính mới cho
lớp, thì bạn có thể gọi chúng ra như bạn có thể làm với các thuộc tính bình thường.
Kiến trúc tiến hóa và thiết kế nổi dần: Ngôn ngữ, tính biểu cảm và
thiết kế, Phần 2
Trang 4 của 13
ibm.com/developerWorks/vn/
developerWorks®
Và bạn có thể làm tương tự như vậy trong JRuby, như trong liệt kê 6, bằng cách sử dụng các lớp
JDK hiện có:
Liệt kê 6. Thêm các phương thức vào lớp ArrayList bằng cách sử dụng Jruby
require 'java'
include_class 'java.util.ArrayList'
class ArrayList
def first
size != 0 ? get(0) : nil
end
def last
size != 0 ? get(size - 1) : nil
end
end
list
l <<
puts
puts
= ArrayList.new
1 << 2 << 3
list.first
list.last
empty_list = ArrayList.new
puts empty_list.first
puts empty_list.last
Bạn đừng rơi vào cái bẫy khi nghĩ rằng giải pháp cho mọi vấn đề là cần phải có thêm các lớp. Siêu
lập trình thường cung cấp các giải pháp “sạch” hơn cho các vấn đề.
Trang trí bằng móc nối lời gọi
Đôi khi bạn cần trang trí phủ lên không chỉ một vài lớp. Ví dụ, bạn có thể muốn trang trí tất cả các
hoạt động của cơ sở dữ liệu của bạn bằng các kiểm soát giao dịch. Việc tạo một cái trang trí đơn
giản, truyền thống cho từng trường hợp như vậy là quá cồng kềnh, và nó sẽ thêm rất nhiều cú pháp
vào mã của bạn đến nỗi sẽ rất khó để xác định đơn vị công việc mà bạn đang nhắm đến.
Ta hãy xem cái trang trí được hiển thị trong liệt kê 7, được thực hiện bằng ngôn ngữ Groovy:
Liệt kê 7. Lớp GenericLowerDecorator trong ngôn ngữ Groovy
class GenericLowerDecorator {
private delegate
GenericLowerDecorator(delegate) {
this.delegate = delegate
}
def invokeMethod(String name, args) {
def newargs = args.collect{ arg ->
if (arg instanceof String) return arg.toLowerCase()
else return arg
}
delegate.invokeMethod(name, newargs)
}
}
Lớp GenericLowerDecorator hoạt động như một cái trang trí phổ quát để buộc tất cả các tham số
dạng chuỗi ký tự thành chữ thường. Lớp GenericLowerDecorator thực hiện việc này bằng cách sử
Kiến trúc tiến hóa và thiết kế nổi dần: Ngôn ngữ, tính biểu cảm và
thiết kế, Phần 2
Trang 5 của 13
developerWorks®
ibm.com/developerWorks/vn/
dụng một phương thức móc nối. Khi bạn gọi ra cái trang trí này, bạn bao bọc nó xung quanh bất kỳ
cá thể nào. Phương thức invokeMethod() đón bắt tất cả các cuộc gọi phương thức đến lớp này, cho
phép bạn thực hiện bất cứ hành động gì mà bạn thích. Trong trường hợp này, tôi chặn từng cuộc
gọi phương thức và duyệt qua tất cả các tham số của phương thức. Nếu bất kỳ tham số nào có kiểu
chuểi ký tể, thì tôi sẽ thêm phiên bản chữ thường của nó vào một danh sách các đối số mới, và
để nguyên các đối số khác. Tại phần cuối của phương thức móc nối, tôi gọi phương thức ban đầu
trên đối tượng đã được trang trí và sử dụng danh sách mới của tôi. Cái trang trí này chuyển tất cả
các tham số chuỗi ký tự thành chữ thường, bất kể phương thức hoặc tham số của chúng. Liệt kê 8 là
một ví dụ về việc sử dụng mẫu đó, gói một trong những logger từ Liệt kê 1:
Liệt kê 8. Sử dụng mẫu GenericLowerDecorator trên một Logger
logger = new GenericLowerDecorator(
new TimeStampingLogger(
new Logger()))
logger.log('IMPORTANT Message')
Mọi phương thức được gọi bằng cái trang trí này chỉ sử dụng chuỗi ký tự chữ thường:
Tue May 22 07:27:18 EST 2007: important message
Bạn lưu ý rằng dấu ấn thời gian không được đặt ở dạng chữ thường nhưng tham số String thì
ở dạng chữ thường. Có thể thực hiện điều này trong ngôn ngữ Java, nhưng rất khó. Thực vậy,
sử dụng các khía cạnh (aspects) (thông qua AspectJ chẳng hạn), là cách duy nhất để đạt được
hiệu ứng này trong ngôn ngữ Java (xem phần Tài nguyên). Để nhận được kiểu trang trí này, bạn
phải chuyển sang một ngôn ngữ khác với trình biên dịch riêng của nó và thiết lập việc hậu xử lý
(postprocessing) mã Java của bạn. Dù không phải là không thể thực hiện được, nhưng quy trình
này sẽ rất rườm rà đến nỗi bạn sẽ không bao giờ muốn bận tâm.
Mẫu Adaptor
Cuốn Gang of Four mô tả các mẫu Adaptor như sau:
Mẫu Adaptor chuyển đổi giao diện của một lớp thành một giao diện khác mà các trình
khách mong đợi. Trình tiếp hợp (adapter) cho phép các lớp làm việc cùng nhau mà
nếu không thì không thể được vì các giao diện không tương thích.
Nếu bạn đã từng sử dụng bộ xử lý sự kiện trong Swing, thì bạn đã có những kiến thức sâu sắc về
mẫu Adaptor. Nó được sử dụng để tạo ra các lớp tiếp hợp với các giao diện xử lý sự kiện có chứa
nhiều phương thức sao cho bạn không cần phải tạo ra lớp riêng của mình, thực hiện các giao diện,
và bao gồm rất nhiều phương thức rỗng. Các lớp tiếp hợp của Swing cho phép bạn tạo lớp con và
chỉ cần ghi đè lên các phương thức mà bạn cần để xử lý sự kiện.
Tíếp hợp trong Groovy
Tuy nhiên, cuối cùng thì mẫu Adaptor cố gắng trả lời câu hỏi: “Tôi có thể làm cho cái chốt gỗ hình
vuông (square peg) này vừa với cái lỗ tròn (round hole) này không? (Ý nói làm chúng tiếp hợp
Kiến trúc tiến hóa và thiết kế nổi dần: Ngôn ngữ, tính biểu cảm và
thiết kế, Phần 2
Trang 6 của 13
ibm.com/developerWorks/vn/
developerWorks®
được với nhau).” Đó là vấn đề mà tôi sẽ giải quyết, với hai cách thực hiện khác nhau, mỗi cách làm
nổi bật tính biểu cảm trong ngôn ngữ. Cách thực hiện đầu tiên sử dụng Groovy; trong liệt kê 9 có ba
lớp và một giao diện liên quan đến:
Liệt kê 9. Các chốt gỗ hình vuông và các lỗ tròn
interface RoundThing {
def getRadius()
}
class SquarePeg {
def width
}
class RoundPeg {
def radius
}
class RoundHole {
def radius
def pegFits(peg) {
peg.radius <= radius
}
String toString() { "RoundHole with radius $radius" }
}
Việc thực hiện tiếp hợp truyền thống sẽ tạo ra một lớp SquarePegAdaptor gói bọc lấy cái chốt gỗ
hình vuông và thực hiện phương thức getRadius() mà phương thức pegFits() của RoundHole chờ
đợi. Tuy nhiên, Groovy cho phép tôi bỏ qua việc cấu trúc thêm một lớp bổ xung, định nghĩa cái tiếp
hợp của tôi một cách trực tiếp nội tuyến như trong liệt kê 10:
Liệt kê 10. Kiểm thử mẫu adaptor nội tuyến
@Test void pegs_and_holes() {
def adapter = { p ->
[getRadius:{Math.sqrt(
((p.width/2) ** 2)*2)}] as RoundThing
}
def hole = new RoundHole(radius:4.0)
(4..7).each { w ->
def peg = new SquarePeg(width:w)
if (w < 6)
assertTrue hole.pegFits(adapter(peg))
else
assertFalse hole.pegFits(adapter(peg))
}
}
Định nghĩa của trình tiếp hợp có vẻ hơi lạ, nhưng nó đóng gói rất nhiều chức năng. Tôi định nghĩa
cái tiếp hợp dưới dạng một khối mã (được phân cách bởi dấu "{" trong ngôn ngữ Groovy). Bên
trong khối mã, tôi tạo ra một bảng băm, ở đây khoá là tên của thuộc tính (getRadius()) và giá trị
là một khối mã thực hiện chức năng tôi cần cho trình tiếp hợp của tôi. Toán tử as trong Groovy
thực hiện nốt điều kỳ diệu. Khi tôi sử dụng toán tử as trên một khối mã, thì Groovy tạo ra một lớp
mới, lớp này thực hiện các giao diện RoundThing; các lời gọi phương thức của lớp này thực hiện
tìm kiếm trong bảng băm, so khớp tên của phương thức với giá trị của khoá và thi hành khối mã
Kiến trúc tiến hóa và thiết kế nổi dần: Ngôn ngữ, tính biểu cảm và
thiết kế, Phần 2
Trang 7 của 13
developerWorks®
ibm.com/developerWorks/vn/
tương ứng. Kết quả cuối cùng là một lớp tiếp hợp rất gọn nhẹ thực hiện các chức năng mà giao
diện RoundThing yêu cầu.
Mặc dù việc thực hiện cuối cùng ở mức lớp cũng giống như cách tiếp cận truyền thống, nhưng mã
(một khi bạn đã biết ngôn ngữ Groovy) trở nên dễ đọc và dễ hiểu hơn nhiều. Groovy cho phép bạn
tạo các lớp bao bọc (wrapper) nhẹ xung quanh các giao diện dành cho chính tình huống này.
Mẫu Adaptor trong JRuby
Điều gì sẽ xảy ra nếu bạn không muốn tạo ra một lớp bổ sung thêm cho trình tiếp hợp của bạn một
chút nào? Cả hai ngôn ngữ Groovy và Ruby đều hỗ trợ các lớp mở, cho phép bạn bổ sung phương
thức cần thiết trực tiếp vào lớp đang xét. Liệt kê 11 là triển khai thực hiện chốt vuông và lỗ tròn
trong ngôn ngữ Ruby (thông qua JRuby):
Liệt kê 11. Cái tiếp hợp bằng lớp mở trong ngôn ngữ Ruby
class SquarePeg
attr_reader :width
def initialize(width)
@width = width
end
end
class SquarePeg
def radius
Math.sqrt(((@width/2) ** 2) * 2 )
end
end
class RoundPeg
attr_reader :radius
def initialize(radius)
@radius = radius
end
def width
@radius * @radius
end
end
class RoundHole
attr_reader :radius
def initialize(r)
@radius = r
end
def peg_fits?( peg )
peg.radius <= radius
end
end
Định nghĩa thứ hai của lớp SquarePeg trong Liệt kê 11 không phải là một sai sót: Cú pháp của lớp
mở của Ruby trông giống như một định nghĩa lớp thông thường. Khi bạn sử dụng một tên lớp, Ruby
sẽ kiểm tra xem nó đã nạp một lớp với chính tên này từ đường dẫn lớp (classpath) chưa, và nếu đã
được nạp rồi, thì cá thể thể hiện thứ hai sẽ mở lại lớp. Tất nhiên, trong trường hợp này tôi có thể chỉ
Kiến trúc tiến hóa và thiết kế nổi dần: Ngôn ngữ, tính biểu cảm và
thiết kế, Phần 2
Trang 8 của 13
ibm.com/developerWorks/vn/
developerWorks®
cần bổ sung phương thức radius() trực tiếp vào lớp, nhưng tôi giả định rằng lớp SquarePeg ban đầu
đã có trước mã này. Liệt kê 12 là phép kiểm thử đơn vị cho trình tiếp hợp dùng lớp mở:
Liệt kê 12. Kiểm thử trình tiếp hợp dùng lớp mở
def test_open_class_pegs
hole = RoundHole.new( 4.0 )
4.upto(7) do |i|
peg = SquarePeg.new(i.to_f)
if (i < 6)
assert hole.peg_fits?(peg)
else
assert ! hole.peg_fits?(peg)
end
end
end
Trong trường hợp này, tôi có thể gọi ra phương thức radius trực tiếp từ lớp SquarePeg bởi vì
nó bây giờ đã có một phương thức radius. Việc thêm một phương thức thông qua một lớp mở
hoàn toàn loại bỏ sự cần thiết phải có một lớp tiếp hợp riêng biệt, cho dù viết thủ công hay tạo
ra một cách tự động. Tuy nhiên, có một vấn đề tiềm tàng trong mã này: Điều gì sẽ xảy ra nếu lớp
SquarePeg đã có phương thức radius mà không có gì để làm với các lỗ tròn? Khi sử dụng các lớp
mở sẽ ghi đè lên lớp ban đầu đó, gây ra hành vi không mong muốn.
Đây là nơi mà sức mạnh của một ngôn ngữ có tính biểu cảm thật sự phát huy đầy đủ hiệu lực. Ta
hãy xem mã của ngôn ngữ Ruby trong liệt kê 13:
Liệt kê 13. Chuyển giao diện
class SquarePeg
include InterfaceSwitching
def radius
@width
end
def_interface :square, :radius
def radius
Math.sqrt(((@width/2) ** 2) * 2)
end
def_interface :holes, :radius
def initialize(width)
set_interface :square
@width = width
end
end
Mã này hoàn toàn không thể viết được bằng ngôn ngữ Java hay Groovy. Bạn lưu ý rằng tôi đã định
nghĩa hai phương thức cùng có tên radius. Trong Groovy, trình biên dịch sẽ không biên dịch mã
này. Tuy nhiên, Ruby (và do đó JRuby) là một ngôn ngữ thông dịch, cho phép bạn thi hành mã
vào thời điểm thông dịch. Khi bạn nghe một số môn đồ của Ruby nói đến các cấu kiện trong Ruby
như là “các công dân hạng nhất”, nghĩa là tất cả các bộ phận của ngôn ngữ này có sẵn tại mọi thời
điểm. Điều kỳ diệu ở đây nằm trong lời gọi phương thức def_interface (giống như từ khóa). Đây là
Kiến trúc tiến hóa và thiết kế nổi dần: Ngôn ngữ, tính biểu cảm và
thiết kế, Phần 2
Trang 9 của 13
developerWorks®
ibm.com/developerWorks/vn/
một phương thức của siêu lập trình được định nghĩa trên lớp Class, được thi hành vào thời điểm
thông dịch. Mã này cho phép bạn định nghĩa một giao diện cụ thể cho một phương thức, cho phép
phương thức đó chỉ tồn tại trong một phạm vi nhất định. Phạm vi này được định nghĩa bởi lời gọi
phương thức with_interface, như được thể hiện trong liệt kê 14:
Liệt kê 14. Kiểm thử chuyển giao diện
def test_pegs_switching
hole = RoundHole.new( 4.0 )
4.upto(7) do |i|
peg = SquarePeg.new(i)
peg.with_interface(:holes) do
if (i < 6)
assert hole.peg_fits?(peg)
else
assert ! hole.peg_fits?(peg)
end
end
end
end
Trong phạm vi khối with_interface, phương thức radius được định nghĩa với tên của giao diện này
tồn tại và có thể gọi được. Mã trong liệt kê 15 để làm việc này nhỏ gọn một cách đáng ngạc nhiên
(nhưng nó hơi cô đọng). Nó được trình bày để thấy bối cảnh; phần lớn của mã này là siêu lập trình
Ruby bán cao cấp vì vậy tôi sẽ không bàn luận chi tiết về nó.
Liệt kê 15. Điều kỳ diệu của chuyển giao diện
class Class
def def_interface(interface, *syms)
@__interface__ = {}
a = (@__interface__[interface] = [])
syms.each do |s|
a << s unless a.include? s
alias_method "__ể{s}_ể{interface}__".intern, s
remove_method s
end
end
end
module InterfaceSwitching
def set_interface(interface)
unless self.class.instance_eval{ @__interface__[interface] }
raise "Interface for ể{self.inspect} not understood."
end
i_hash = self.class.instance_eval "@__interface__[interface]"
i_hash.each do |meth|
class << self; self end.class_eval <<-EOF
def ể{meth}(*args, &block)
send(:__ể{meth}_ể{interface}__, *args, &block)
end
EOF
end
@__interface__ = interface
end
def with_interface(interface)
oldinterface = @__interface__
set_interface(interface)
begin
Kiến trúc tiến hóa và thiết kế nổi dần: Ngôn ngữ, tính biểu cảm và
thiết kế, Phần 2
Trang 10 của 13
ibm.com/developerWorks/vn/
developerWorks®
yield self
ensure
set_interface(oldinterface)
end
end
end
Đoạn mã thú vị trong Liệt kê 15 xuất hiện ở phần cuối của định nghĩa lớp mở Class, nơi mà phương
thức đã đặt tên được gán cho một tên khác (dựa trên giao diện) và sau đó bị loại bỏ khỏi lớp
theo cách lập trình. Đoạn mã thú vị hơn xuất hiện trong hỗn hợp InterfaceSwitching: Phương thức
set_interface định nghĩa lại phương thức ban đầu (đã đổi tên) cho phạm vi của khối lệnh được tạo
ra bên trong phương thức with_interface. Phiên bản Ruby của khối finally là khối ensure ở cuối.
Mục đích của bài tập này không nhất thiết phải là đi sâu vào các phù phép siêu lập trình trong
Ruby, mà là để chứng tỏ những gì có thể làm được trong các ngôn ngữ có tính biểu cảm rất cao.
Các ngôn ngữ thông dịch luôn luôn có một lợi thế hơn các ngôn ngữ biên dịch, vì chúng có thể
thi hành mã vào các thời điểm mà các ngôn ngữ biên dịch không thể làm được. Thực vậy, Groovy
đã đưa vào một cơ chế siêu lập trình thời gian biên dịch được gọi là AST Transformations (Phép
chuyển đổi AST), nhờ đó mà bạn có thể viết mã để giao tiếp với trình biên dịch (xem phần Tài
nguyên).
Tóm tắt
Vậy thì tất cả những thứ này chứng minh điều gì? Trong ngôn ngữ, tính biểu cảm tương đồng với
quyền lực. Bạn không thấy nhiều những kỹ thuật như thế trong ngôn ngữ Java, mặc dù chúng có thể
thực hiện được về mặt kỹ thuật thông qua các khía cạnh (aspects) và việc sinh mã bytecode bằng
cách sử dụng các công cụ như Javassist (xem phần Tài nguyên). Tuy nhiên, giải quyết vấn đề bằng
cách sử dụng các cơ chế như vậy là quá rườm rà đến nỗi không ai muốn bận tâm. Thái độ này cũng
ảnh hưởng đến các mẫu diễn đạt đặc trưng. Thậm chí, nếu bạn có thể thấy các mẫu đó là riêng cho
ứng dụng của bạn, nếu cách để thu thập chúng là quá khó khăn, thì bạn sẽ không bận tâm, từ đó
tạo ra món nợ về mặt kỹ thuật không cần thiết cho dự án của bạn. Ý nghĩa của tính biểu cảm trong
các ngôn ngữ máy tính là ... rất nhiều!
Kiến trúc tiến hóa và thiết kế nổi dần: Ngôn ngữ, tính biểu cảm và
thiết kế, Phần 2
Trang 11 của 13
developerWorks®
ibm.com/developerWorks/vn/
Tài nguyên
Học tập
• Nhà lập trình năng suất cao (Tác giả: Neal Ford, nhà xuất bản: O'Reilly Media, 2008): Cuốn
sách gần đây nhất của Neal Ford mở rộng một số chủ đề của loạt bài viết này.
• Các mẫu thiết kế: Các phần tử của phần mềm hướng đối tượng có thể sử dụng lại được (Tác
giả: Erich Gamma et al, nhà xuất bản: Addison.-Wesley, 1994). Tác phẩm kinh điển của Gang
of Four về các mẫu thiết kế.
• Practically Groovy: Khám phá loạt bài viết này trên trang developerWorks để tìm hiểu thêm về
Groovy, một ngôn ngữ năng động cho các JVM.
• "alt.lang.jre: Hãy đam mê JRuby" (Các tác giả: Michael Squillace và Barry Feigenbaum,
developerWorks, tháng 9 năm 2004): Tìm hiểu cách JRuby kết hợp sức mạnh hướng đối
tượng của ngôn ngữ Smalltalk, tính biểu cảm của ngôn ngữ Perl và tính linh hoạt của các thư
viện lớp của Java.
• Sách hướng dẫn về Scala cho các nhà phát triển Java bận rộn: Đào sâu loạt bài viết này trên
developerWorks để tìm hiểu thêm về Scala.
• "Vượt qua biên giới: Vẻ đẹp của Lisp" (Tác giả: Bruce Tate, developerWorks, February 2007):
Nếu bạn tò mò muốn biết lý do tại sao các ngôn ngữ khác vay mượn từ Lisp thì hãy đọc bài
viết này.
• AspectJ: Nền tảng lập trình hướng khía cạnh phổ biến nhất để phát triển bằng Java.
• Javassist: Thư viện Java cho việc sinh mã bytecode.
• Phép chuyển đổi AST: Tìm hiểu thêm về siêu lập trình thời gian biên dịch trong ngôn ngữ
Groovy.
• Duyệt qua hiệu sách công nghệ để tìm các sách về chủ đề này và các chủ đề kỹ thuật khác.
• Vùng công nghệ Java trên trang developerWorks: Tìm hàng trăm bài viết về mọi khía cạnh
của lập trình Java.
Lấy sản phẩm và công nghệ
• JRuby: Tải về JRuby.
• Groovy: Tải về Groovy.
Thảo luận
• Hãy tham gia vào cộng đồng My developerWorks community.
Kiến trúc tiến hóa và thiết kế nổi dần: Ngôn ngữ, tính biểu cảm và
thiết kế, Phần 2
Trang 12 của 13
ibm.com/developerWorks/vn/
developerWorks®
Đôi nét về tác giả
Neal Ford
Neal Ford là một kiến trúc sư phần mềm và Meme Wrangler tại Thought Works, một
văn phòng tư vấn CNTT toàn cầu. Ông cũng thiết kế và phát triển các ứng dụng, tài
liệu hướng dẫn, các bài báo trên tạp chí, học liệu và các bài thuyết trình video/DVD;
và ông là tác giả hoặc người biên tập các cuốn sách bao trùm nhiều loại công nghệ,
bao gồm cả cuốn sách gần đây nhất là The Productive Programmer. Ông tập trung
vào việc thiết kế và xây dựng ứng dụng doanh nghiệp có quy mô lớn. Ông cũng là một
diễn giả được quốc tế hoan nghênh tại hội nghị của các nhà phát triển trên toàn thế
giới
© Copyright IBM Corporation 2010
(www.ibm.com/legal/copytrade.shtml)
Nhẫn hiệu đăng ký
(www.ibm.com/developerworks/vn/ibm/trademarks/)
Kiến trúc tiến hóa và thiết kế nổi dần: Ngôn ngữ, tính biểu cảm và
thiết kế, Phần 2
Trang 13 của 13