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

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 pot

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 (212.9 KB, 30 trang )

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, Kiến trúc phần mềm, ThoughtWorks
Tóm tắt: 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)).
Đâ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ữ 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())
}
}

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 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.
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 = ArrayList.new
l << 1 << 2 << 3
puts list.first
puts 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ử 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 đượ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ã 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ỉ 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à 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|

×