Bài viết này mở ra một loạt bài m ới của Neal Ford trên developerWorks tiến hành
so sánh kỹ 3 thế hệ tiếp theo của ngôn ngữ JVM: Groovy, Scala, and Clojure.
Trong bài này, bạn sẽ tìm hiểu những điểm tương đồng và khác biệt giữa chúng —
và bạn sẽ lựa chọn Java™ là ngôn ngữ lập trình chính của bạn hay không
15 trang |
Chia sẻ: lylyngoc | Lượt xem: 1714 | Lượt tải: 1
Bạn đang xem nội dung tài liệu Java.next: Ngôn ngữ Java.next, để tải tài liệu về máy bạn click vào nút DOWNLOAD ở trên
Java.next: Ngôn ngữ Java.next
Bài viết này mở ra một loạt bài mới của Neal Ford trên developerWorks tiến hành
so sánh kỹ 3 thế hệ tiếp theo của ngôn ngữ JVM: Groovy, Scala, and Clojure.
Trong bài này, bạn sẽ tìm hiểu những điểm tương đồng và khác biệt giữa chúng —
và bạn sẽ lựa chọn Java™ là ngôn ngữ lập trình chính của bạn hay không
Trong một bài phát biểu mà tôi có dịp từng hợp tác với Martin Fowler, ông đã có
cái nhìn sâu sắc:
Di sản của Java chính là nền tảng, không phải ngôn ngữ.
Các kỹ sư Java đã có một quyết định tuyệt vời là tách ngôn ngữ ra khỏi thành phần
runtime, tạo điều kiện cho hơn 200 ngôn ngữ có thể chạy được trên nền tảng Java.
Kiến trúc này rất quan trọng cho các nền tảng lâu dài, bởi vì ngôn ngữ lập trình
máy tính thường có tuổi thọ ngắn. Từ năm 2008, Hội nghị thượng đỉnh Ngôn ngữ
JVM hàng năm, được tổ chức bởi Oracle, đã tạo cơ hội cho các nhà phát triển ngôn
ngữ khác cởi mở hợp tác với các kỹ sư Java.
Chào mừng bạn đến với loại bài về Java.next. Trong đó, tôi sẽ giới thiệu sơ bộ về
ba ngôn ngữ JVM hiện đại — Groovy, Scala, và Clojure — chúng cung cấp một sự
pha trộn thú vị của mô hình, lựa chọn thiết kế, và yếu tố tiện nghi. Tôi sẽ không
dành nhiều thời gian ở đây để cung cấp sự mô tả sâu của mỗi ngôn ngữ, chúng đã
có sẵn trên trang web của chúng tôi (xem phần Tài nguyên). Nhưng các trang web
ngôn ngữ cộng đồng — với mục đích chính là truyền bá — thông tin khách quan
hoặc thiếu các ví dụ về việc một ngôn ngữ là không thích hợp. So sánh nội dung tôi
sẽ thực hiện trong loạt bài này là để giúp lấp đầy khoảng trống. Điều này đặt ra giai
đoạn với tổng quan về ngôn ngữ Java.next và những lợi ích của việc học về chúng.
Nhìn lại Java
Ngôn ngữ Java nổi lên thông qua Bruce Tate, trong cuốn sách của ông Beyond
Java (xem phần Tài nguyên), có thể gọi là một cơn bão hoàn hảo: sự kết hợp các
yếu tố của sự nổi lên của web, không phù hợp của công nghệ web đã có vì nhiều lý
do, và sự gia tăng của phát triển ứng dụng đa tầng của các doanh nghiệp. Tate cũng
lập luận rằng cơn bão hoàn hảo này là một loạt các sự kiện độc đáo, và không có
ngôn ngữ nào khác sẽ vượt qua sự nổi bật tương tự bằng cùng một cách.
Ngôn ngữ Java đã chứng tỏ tính mềm dẻo, nhưng cú pháp của nó và mô hình vốn
có những hạn chế phải mất nhiều thời gian mới hiểu rõ được. Mặc dù ngôn ngữ
cũng đang có những thay đổi đầy hứa hẹn, nhưng cú pháp đơn giản không thể hỗ
trợ các mục tiêu quan trọng trong tương lai, chẳng hạn như các yếu tố của lập trình
chức năng. Nhưng nếu bạn đang cố gắng để tìm một ngôn ngữ mới duy nhất để
thay Java, thì bạn đang tìm không đúng hướng.
Lập trình đa ngôn ngữ (Polyglot programming)
Lập trình đa ngôn ngữ— một thuật ngữ mà tôi đã hồi sinh và làm cho nó phổ biến
lại trong một blog vào năm 2006 (xem phần Tài nguyên) — được dựa trên nhận
thức rằng không có ngôn ngữ đơn thuần nào phù hợp để giải quyết mọi vấn đề.
Một số ngôn ngữ đã được xây dựng trong quan điểm hoặc các tính năng phù hợp
với các vấn đề cụ thể hơn. Ví dụ, như tính tinh vi của Swing, các nhà phát triển
nhận ra rằng thật phức tạp khi viết UIS Swing trong Java, bởi vì nó đòi hỏi phải
khai báo kiểu, lớp ẩn danh bên trong vụng về để thực hiện hành động, và các thủ
tục khác. Sử dụng ngôn ngữ đó là tốt hơn phù hợp để xây dựng các UI, chẳng hạn
một Groovy với SwingBuilder làm cơ sở (xem Tài nguyên), làm cho việc xây dựng
các ứng dụng Swing dễ dàng hơn.
Sự gia tăng của ngôn ngữ chạy trên JVM làm cho ý tưởng của lập trình đa ngôn
ngữ hấp dẫn hơn, bởi vì bạn có thể trộn và kết hợp trong khi duy trì các mã byte cơ
bản và thư viện giống nhau. Ví dụ, SwingBuilder không thể thay thế Swing, nó là
lớp sẵn có trên Swing API. Tất nhiên, trong một thời gian dài, các nhà phát triển đã
tiến hành pha trộn ngôn ngữ bên ngoài JVM — ví dụ, sử dụng SQL và JavaScript
cho các mục đích chuyên môn — nhưng nó đang trở thành phổ biến trong phạm vi
JVM. Nhiều dự án ThoughtWorks kết hợp nhiều ngôn ngữ, và tất cả các công cụ
được phát triển bởi ThoughtWorks Studios sử dụng ngôn ngữ hỗn hợp.
Ngay cả khi Java vẫn phát triển ngôn ngữ chính của bạn, học cách làm việc với
ngôn ngữ thay thế cho phép bạn kết hợp chúng một cách chiến lược. Java sẽ vẫn là
một phần quan trọng của hệ sinh thái JVM, nhưng cuối cùng hơn như ngôn ngữ lắp
ráp của nền tảng — một nơi mà bạn đi hoàn toàn vì lý do hiệu suất hoặc để đáp
ứng yêu cầu chuyên môn.
Sự phát triển
Trong đầu những năm 1980, khi tôi còn trong trường đại học, chúng tôi sử dụng
một môi trường phát triển được gọi là Pecan Pascal. Tính năng độc đáo của nó là
giống như mã Pascal có thể chạy trên cả máy Apple II hoặc IBM PC. Các kỹ sư
Pecan đạt được điều này bằng cách sử dụng một cái gì đó bí ẩn được gọi là "byte
code." Các nhà phát triển biên dịch mã Pascal của họ thành "byte code," chạy trên
"máy ảo" được viết dành riêng cho mỗi nền tảng. Đó là một kinh nghiệm xương
máu! Mã kết quả đã được chậm chạm kinh khủng ngay cả đối với bài toán đơn
giản. Phần cứng vào thời điểm đó không phù hợp với sự thử thách này.
Một thập kỷ sau khi Pecan Pascal, Sun phát hành Java bằng cách sử dụng kiến trúc
tương tự, căng thẳng nhưng thành công trong môi trường phần cứng giữa những
năm 1990. Nó cũng có thêm tính năng phát triển thân thiện khác như dọn dẹp bộ
nhớ tự động. Đã từng làm việc với các ngôn ngữ như C++, tôi không bao giờ muốn
lập trình với một ngôn ngữ không tự thu gom rác (non-garbage-collected) lần nào
nữa. Tôi muốn dành nhiều thời gian ở một mức độ trừu tượng cao hơn suy nghĩ về
cách để giải quyết vấn đề kinh doanh phức tạp, chứ không phải các vấn đề hệ
thống phức tạp như việc quản lý bộ nhớ.
Một trong những lý do mà ngôn ngữ máy tính thường không có vòng đời lâu dài là
do tốc độ đổi mới trong ngôn ngữ và nền tảng thiết kế. Các nền tảng của chúng ta
trở nên mạnh hơn, chúng có thể xử lý nhiều công việc phức tạp hơn. Ví dụ, tính
năng memoization của Groovy (được bổ sung vào năm 2010) lưu trữ các kết quả
của các lời gọi chức năng. Hơn là việc bạn phải viết tay để xử lý bộ đệm, điều này
sẽ dễ gây ra lỗi, giờ đây bạn chỉ việc gọi hàm memoize(), như trong Liệt kê 1:
Liệt kê 1. Hàm chức năng xử lý bộ nhớ trong Groovy
def static sum = { number ->
factorsOf(number).inject(0, {i, j -> i + j})
}
def static sumOfFactors = sum.memoize()
Trong Liệt kê 1, các kết quả từ hàm sumOfFactors sẽ được tự động lưu trữ. Bạn
cũng có thể tùy chỉnh các hành vi của bộ nhớ đệm với các phương pháp thay thế,
memoizeAtLeast() và memoizeAtMost(). Clojure cũng bao gồm memoization, và
nó không quan trọng để thực hiện trong Scala. Tính năng tiên tiến như
memoization tồn tại trong ngôn ngữ thế hệ tiếp theo (và trong một số khuôn khổ
Java) sẽ dần dần tìm đường vào ngôn ngữ Java. Phiên bản tiếp theo của Java sẽ
thêm chức năng bậc cao, làm cho memoization dễ dàng hơn nhiều để thực
hiện. Bằng cách nghiên cứu thế hệ tiếp theo ngôn ngữ Java, bạn nhận được một
sneak đỉnh cao vào các tính năng Java tương lai.
Groovy, Scala, Clojure
Groovy là cú pháp Java ở thế kỉ 21 — được ví như tách cà phê espresso thay vì cà
phê bình thường. Mục tiêu thiết kế của Groovy là để cập nhật và loại bỏ ma sát từ
cú pháp của Java trong khi hỗ trợ các mô hình chính trong ngôn ngữ Java. Vì vậy,
Groovy "biết" về những thứ như JavaBeans, đơn giản hóa truy cập tài nguyên.
Groovy kết hợp các tính năng mới với tốc độ nhanh, trong đó có một số tính năng
chức năng quan trọng tôi sẽ nổi bật trong các phần sau. Groovy chủ yếu là một
ngôn ngữ hướng đối tượng. Hai khác biệt cơ bản giữa Java và Groovy là "động"
lúc nào cũng hơn "tĩnh" , và khả năng lập trình meta của nó tốt hơn đáng kể.
Scala là một ngôn ngữ thiết kế từ nền lên để tận dụng lợi thế của JVM, nhưng với
một cú pháp được thiết kế lại hoàn toàn. Scala là một ngôn ngữ tĩnh mạnh mẽ —
nghiêm ngặt hơn so với Java nhưng lại không gây nhiều điều khó chịu — hỗ trợ cả
hai mô hình lập trình hướng đối tượng và hướng chức năng, các ưu thế sau này. Ví
dụ, Scala thích dùng val để khai báo, năng suất một biến không thay đổi (tương tự
như đánh dấu một tham số final trong Java) thành các var, tạo ra nhiều quen thuộc
có thể thay đổi các biến. Scala cung cấp một cầu nối từ những gì bạn có thể là (một
lập trình hướng đối tượng bắt buộc) với những gì bạn có thể nên được (một lập
trình thêm chức năng) bằng cách hỗ trợ cả hai mô hình sâu sắc.
Clojure sử dụng cú pháp triệt để nhất từ các ngôn ngữ khác, là một phương ngữ
của Lisp. Là một ngôn ngữ động mạnh mẽ (giống như Groovy), nó phản ánh các
quyết định thiết kế cá tính. Mặc dù Clojure cho phép bạn khả năng tương tác đầy
đủ và sâu sắc với di sản Java, tuy nhiên nó không cố gắng xây dựng một cầu nối từ
mô hình cũ. Ví dụ, Clojure là chức năng và hỗ trợ định hướng đối tượng để cho
phép khả năng tương tác. Tuy nhiên, nó hỗ trợ tất cả các tính năng mà quen với các
lập trình viên lập trình hướng đối tượng, chẳng hạn như đa hình — nhưng trong
một kiểu như chức năng chứ không phải là hướng đối tượng. Clojure được thiết kế
xung quanh một vài nguyên tắc kỹ thuật cốt lõi, chẳng hạn như Software
Transactional Memory, điều đó phá vỡ mô hình cũ có lợi cho khả năng mới.
Mô hình
Bên ngoài của cú pháp, sự khác biệt thú vị nhất trong số những ngôn ngữ quan tâm
typing và mô hình chính cơ bản: chức năng hoặc bắt buộc.
Động và tĩnh
Tính Tĩnh trong ngôn ngữ lập trình dùng để khai báo kiểu rõ ràng, chẳng hạn như
khai báo int x; Java. Tính Động đề cập đến ngôn ngữ mà không yêu cầu loại thông
tin khai báo. Tất cả các ngôn ngữ đang được xem xét là ngôn ngữ mạnh mẽ, có
nghĩa là code của bạn có thể phản ánh các loại sau khi chuyển đổi.
Hệ thống kiểu Java đã bị chỉ trích rộng rãi như cung cấp rất nhiều các bất tiện của
typing tĩnh mà không có đủ những lợi ích. Ví dụ, trước khi loại suy luận hạn chế
hiện có, Java yêu cầu các nhà phát triển để khai báo kiểu lặp lại trên cả hai mặt của
các bài tập. Scala là tĩnh typing hơn so với Java nhưng ít hơn nhiều phức tạp trong
sử dụng hàng ngày, bởi vì nó làm cho sử dụng nhiều loại suy luận.
Groovy có một hành vi mà ở cái nhìn đầu tiên dường như thu hẹp khoảng cách tĩnh
vs năng động. Xem xét ví dụ đơn giản về collection factory được thể hiện trong
Liệt kê 2:
Liệt kê 2. Bộ sưu tập Groovy
class CollectionFactory {
def List getCollection(description) {
if (description == "Array-like")
new ArrayList()
else if (description == "Stack-like")
new Stack()
}
}
Lớp ở Liệt kê 2 hoạt động như một factory, trả lại một trong hai ứng dụng giao
diện List— ArrayList hoặc Stack — dựa trên mô tả description của đối số. Đối với
các nhà phát triển Java, điều này xuất hiện để đảm bảo rằng bất cứ điều gì được trả
về đáp ứng tương đồng đó. Tuy nhiên, hai tình huống thử nghiệm trong Liệt kê 3
thể hiện điều đó:
Liệt kê 3. Các mẫu kiểm thử trên Groovy
@Test
void test_search() {
List l = f.getCollection("Stack-like")
assertTrue l instanceof java.util.Stack
l.push("foo")
assertThat l.size(), is(1)
def r = l.search("foo")
}
@Test(expected=groovy.lang.MissingMethodException.class)
void verify_that_typing_does_not_help() {
List l = f.getCollection("Array-like")
assertTrue l instanceof java.util.ArrayList
l.add("foo")
assertThat l.size(), is(1)
def r = l.search("foo")
}
Trong các bài kiểm tra đơn vị đầu tiên trong Liệt kê 3, tôi thấy một Stack thông
qua các factory, xác minh rằng nó thực sự là một Stack, sau đó thực hiện các hoạt
động giống Stack như push(), size(), và search(). Tuy nhiên, trong các đơn vị kiểm
tra thứ hai, tôi phải bảo vệ các thử nghiệm với một biệt lệ
MissingMethodException để vượt qua quá trình thử nghiệm. Khi tôi lấy các bộ
collection giống Array vào các biến như List, tôi có thể xác nhận rằng tôi có thể
nhận về một danh sách trả về. Tuy nhiên, khi tôi thử gọi phương thức search(), nó
sẽ xuất ra một biệt lệ vì ArrayList không bao gồm phương thức search(). Do đó,
việc kê khai cung cấp không bảo vệ thời gian biên dịch đảm bảo rằng phương pháp
gọi là đúng.
Mặc dù việc này có vẻ như là một lỗi, nhưng đó là hành vi thích hợp. Các loại
trong Groovy chỉ đảm bảo hiệu lực của lệnh assignment. Ví dụ, trong Liệt kê 3,
nếu tôi trả về cái gì đó đã không thực hiện bởi giao diện List, thì một biệt lệ
(GroovyCastException) sẽ được kích hoạt khi chạy. Điều này làm cho Groovy
vững chắc và mạnh mẽ, gia đình typing động với Clojure.
Tuy nhiên, những thay đổi gần đây với ngôn ngữ đã thực hiện các phân chia tĩnh vs
động trong Groovy thậm chí không rõ ràng. Groovy 2.0 bổ sung thêm một chú
thích @TypeChecked, cho phép bạn có những quyết định về sự nghiêm khắc của
các loại kiểm tra, tại các lớp (class) hoặc phương pháp. Liệt kê 4 minh họa chú
thích này:
Liệt kê 4. Kiểm tra loại thông qua chú thích
@TypeChecked
@Test void type_checking() {
def f = new CollectionFactory()
List l = f.getCollection("Stack-like")
l.add("foo")
def r = l.pop()
assertEquals r, "foo"
}
Trong Liệt kê 4, tôi bổ sung thêm chú thích @TypeChecked, giúp xác minh cả hai
nhiệm vụ và phương thức gọi tiếp theo. Ví dụ, đoạn mã trong Liệt kê 5 sẽ không
được biên dịch nữa:
Liệt kê 5. Typing kiểm tra ngăn chặn phương pháp gọi không hợp lệ
@TypeChecked
@Test void invalid_type() {
def f = new CollectionFactory()
Stack s = (Stack) f.getCollection("Stack-like")
s.add("foo")
def result = s.search("foo")
}
Trong Liệt kê 5, tôi phải bổ sung thêm ép kiểu cho sự trả về từ các factory để cho
phép tôi gọi phương thức search() của Stack. Thiết bị này đi kèm với những hạn
chế: Nhiều tính năng động Groovy sẽ không hoạt động khi typing tĩnh được kích
hoạt. Tuy nhiên, ví dụ này cho thấy sự phát triển liên tục của Groovy trong cầu nối
khoảng cách tĩnh vs năng động.
Tất cả các ngôn ngữ có cơ sở lập trình meta khá mạnh mẽ, typing nghiêm ngặt hơn
có thể được thêm vào sau đó. Ví dụ, một số dự án bên tồn tại để thêm typing chọn
lọc để Clojure. Nói chung, tuy nhiên, nếu typing lựa chọn là tùy chọn, nó không
phải là một phần của hệ thống loại, đó là một cơ chế xác minh.
Mệnh lệnh so với chức năng
So sánh các thông tin chính giữa mệnh lệnh và chức năng. Lập trình theo
Imperative (mệnh lệnh) tập trung vào việc hướng dẫn từng bước, trong nhiều
trường hợp bắt buộc phần cứng cổ điển ở cấp thấp. Còn lập trình Functional (chức
năng) tập trung nhiều hơn vào các chức năng như cấu trúc của lớp đầu tiên và cố
gắng giảm thiểu quá trình chuyển đổi trạng thái và đột biến.
Groovy, được lấy cảm hứng mạnh mẽ từ Java, chủ yếu là một ngôn ngữ mệnh
lệnh. hưng ngay từ đầu, nhiều tính năng ngôn ngữ chức năng đã được bao gồm, và
nhiều hơn nữa đã được thêm vào theo thời gian.
Scala bắc cầu giữa hai mô hình này, hỗ trợ cả hai. Trong khi bạn thích (và được
khuyến khích) lập trình theo hướng chức năng, thì Scala vẫn hỗ trợ lập trình hướng
đối tượng và hướng mệnh lệnh. Vì vậy, sử dụng Scala đúng đòi hỏi một đội xử lý
kỷ luật để đảm bảo rằng bạn không trộn và kết hợp mô hình bừa bãi, điều này luôn
là một mối nguy hiểm trong ngôn ngữ đa mô hình.
Clojure thì không bộc lộ các tính năng. Nó hỗ trợ hướng đối tượng cho phép dễ
dàng tương tác với các ngôn ngữ JVM, nhưng nó cố gắng để không trở thành cầu
nối. Thay vào đó, thiết kế của Clojure giúp khẳng định những thực hành kỹ thuật
tốt mà các nhà thiết kế có thể nghĩ tới. Những quyết định có ảnh hưởng sâu rộng,
cho phép Clojure để giải quyết một số vấn đề dai dẳng trong thế giới Java (chẳng
hạn như vấn đề xử lý đồng thời) theo các cách đột phá.
Nhiều người trong số đó thay đổi trong suy nghĩ cần thiết để tìm hiểu các ngôn ngữ
mới sẽ đến từ sự phân chia imperative/functional, và đó là một trong những khu
vực có giá trị nhất của việc tìm hiểu loạt bài này.
Kết luận
Các nhà phát triển đang sống trong một thế giới ngày càng nhiều ngôn ngữ nơi mà
đa ngôn ngữ được sử dụng để giải quyết vấn đề. Học tập để tận dụng các ngôn ngữ
mới một cách hiệu quả giúp bạn xác định khi khi nào phương pháp này là thích
hợp. Thậm chí nếu bạn không rời bỏ Java, nó sẽ dần dần kết hợp các tính năng từ
các ngôn ngữ JVM thế hệ tiếp theo; nhìn vào chúng có thể sẽ cho bạn thấy trước
được tương lai của ngôn ngữ Java.
Trong phần tiếp theo của Java.next, tôi sẽ bắt đầu so sánh bằng cách nhìn vào
những điểm chung của Groovy, Scala, và Clojure.