Optional trong Java - Quen mà lạ
Chào bài
Hi anh em. Nếu anh em nào code Java thì chắc hẳn sẽ không lạ lẫm với thằng Optional. Đây là một Utiity class dùng để null check, được sử dụng trong JPARepository rất nhiều. Cái này thì không lạ với anh em xài Spring rồi. Trước đây thì mình không có xài thằng này nhiều, gần như chỉ là để trả kết quả từ dao lên service. Nhưng mà gần đây sau khi dinh quá nhiều NullPointerException thì mình nhận ra là thằng này có một vài tiềm năng khá là thú vị khi kết hợp vớilambda function (functional programming) trong Java. Nó có thể biến Java từ một ngôn ngữ OOP thuần trở thành một Functional Programming language cực kỳ mạnh mẽ giúp code trở nên vừa xịn xò, chắc chắn, cũng như giúp cho code đọc hiểu nhanh bá đạo.
Optional For Clean Code
Ví dụ kinh điển: bạn muốn đi pha một ly smooothies và nó có vài bước như sau:
- đầu tiên là đi mua hoa quả
- gọt hoa quả,
- chuẩn bị đá
- bật máy xay và thưởng thức.
Với cách tiếp cận thông thường thì chúng ta sẽ chia mấy cái bước này ra các method con rồi gọi chúng liên tiếp với nhau như sau:
public Smoothies makingSmoothies() {
//buying fruit from the supermarket
Fruit boughtFruit = buyFruit();
//slicing fruit that you buy
SliceFruit slicedFruit = slicingFruit(boughtFruit);
//get some ice for your smoothies
Ice iceForSmoothies = getIce();
//blend the fruit with ice please and now you can enjoy the smoothies
return blendWithIce(slicedFruit, iceForSmoothies);
}
Dễ đúng không, nhưng viết như vậy thì khi vô trong production thì chúng ta sẽ dễ gặp NullPointerException. Và chúng ta buộc phải null check các object sau mỗi bước. Và nó sẽ thành gần như thế này
public Smoothies makingSmoothiesWithNullCheck() {
//buying fruit from the supermarket
Fruit boughtFruit = buyFruit();
if (boughtFruit != null) {
//slicing fruit that you buy
SliceFruit slicedFruit = slicingFruit(boughtFruit);
if (slicedFruit != null) {
//get some ice for your smoothies
Ice iceForSmoothies = getIce();
if (iceForSmoothies != null) {
//blend the fruit with ice please and now you can enjoy the smoothies
return blendWithIce(slicedFruit, iceForSmoothies);
} else {
throw new RuntimeException();
}
} else {
throw new RuntimeException();
}
} else {
throw new RuntimeException();
}
}
Khá là kinh khủng đúng không, nhưng mà nếu ai tinh mắt thì mình có thể làm cho nó sáng hơn bằng cách loại bỏ vòng **if **
public Smoothies makingSmoothiesWithNullCheckAndTerminateIf() {
//buying fruit from the supermarket
Fruit boughtFruit = buyFruit();
if (boughtFruit == null) {
//check if we bought no fruit and now we have nothing to slice
throw new RuntimeException();
}
//slicing fruit that you buy
SliceFruit slicedFruit = slicingFruit(boughtFruit);
if (slicedFruit == null) {
//slicing fruit is drop on the floor, so now we have no fruit to put in blender
throw new RuntimeException();
}
//get some ice for your smoothies
Ice iceForSmoothies = getIce();
if (iceForSmoothies == null) {
//fridge broke now we have no ice
throw new RuntimeException();
}
//blend the fruit with ice please and now you can enjoy the smoothies
//but wait blender can be broken so we dont have any smoothie
return blendWithIce(slicedFruit, iceForSmoothies);
}
OPTIONAL ĐỂ NULL CHECK
Tốt hơn khá nhiều rồi, nhưng mà đọc thì cũng khá mệt khi mà phải liên tục check null sau mỗi lần gọi. Có thể trong một lần nào đó chúng ta sẽ miss null check cho 1 object và NullPointerException đập vào mặt chúng ta. Và đây chúng ta sẽ đến với công dụng chính đầu tiên của Optional (null check)
public Smoothies makingSmoothiesWithNullCheckByOptional() {
//buying fruit from the supermarket
Fruit boughtFruit = Optional.ofNullable(buyFruit()).orElseThrow(RuntimeException::new);
//slicing fruit that you buy
SliceFruit slicedFruit = Optional.ofNullable(slicingFruit(boughtFruit)).orElseThrow(RuntimeException::new);
//get some ice for your smoothies
Ice iceForSmoothies = Optional.ofNullable(getIce()).orElseThrow(RuntimeException::new);
//blend the fruit with ice please and now you can enjoy the smoothies
return Optional.ofNullable(blendWithIce(slicedFruit, iceForSmoothies)).orElseThrow(RuntimeException::new);
}
Khá là đẹp và dễ đọc, sau mỗi lần mình làm một hành động gì đó thì mình dừng lại để check gía trị xem mình có nhận được gía trị mong muốn không. Nếu nó trả về null thì mình sẽ throw lỗi, mình còn được bonus thêm một pha null check trước khi return giá trị nữa. Việc null check như vậy sẽ giúp chúng tập trung vào business chính mà không còn phải tập trung vào coding requirement nữa. Và không có vòng if nào trong đây cả. Một điểm cộng khá lớn đấy.
OPTIONAL ĐỂ TẠO RA MỘT DATA STREAM
Nhưng Optional có thể làm nhiều hơn thế, ở đoạn code trên chúng ta vẫn phải ghi nhớ xem chúng ta đã thực hiện tới bước nào, có các nguyên liệu nào được chuẩn bị và thành quả của chúng sẽ là gì kế tiếp. Liệu có cách nào để chúng ta tách đoạn code trên thành các step và các step này sẽ có ràng buộc với nhau hay không. Cuối cùng thì coding là cách chúng ta ràng buộc các logic với nhau mà đúng không. Chúng ta có thể làm điều này với Optional và hai method map() và flatMap() như sau:
public Smoothies makingSmoothiesWithOptional() {
return Optional.ofNullable(buyFruit())
.map(this::slicingFruit)
.map(slicedFruit -> Optional.ofNullable(getIce())
.map(ice -> blendWithIce(slicedFruit, ice))
.orElseThrow(RuntimeException::new))
.orElseThrow(RuntimeException::new);
}
Như vậy là chúng ta đã phân chia việc làm Smoothies thành các bước nhỏ hơn và chúng ta đã có ràng buộc khi mà đầu ra của bước này trở thành đầu vào của bước kia, khá là giống một stream. Chúng ta cũng không cần thiết phải nullcheck qua mỗi bước nữa khi mà những object được push vào stream chắc chắn đã khác null. Hơn nữa ta cũng biết được những hành động nào sẽ được thực hiện vào lúc nào cũng như thứ tự của chúng với nhau. Ở đây ta còn thấy được việc chuẩn bị đá sẽ được thực hiện độc lập với các bước của hoa quả và chúng ta có thể thay đổi việc chuẩn bị đá này qua các bước khác nhau một cách dễ dàng.
OPTIONAL ĐỂ RÀNG BUỘC CÁC BƯỚC VỚI NHAU
Ở ví dụ trên thì ta thấy là Optional dùng để biến đổi từ trái cây thành sinh tố. Data flow ở đây được thể hiện khá rõ ràng và tường minh. Tuy nhiên nếu chúng ta chỉ tương tác trên một object duy nhất thì việc sử dụng Optional có còn tốt nữa không. Hãy đến với một ví dụ kế tiếp, chúng ta muốn làm sữa chua từ smoothie của mình ( not sure nhưng cứ ví dụ vậy nhé) nên chúng ta phải cho men vô. Men được thể hiện dưới dạng một attribute của class Smoothie (isFerment) . Tuy nhiên trước đó nó phải được làm lạnh trước nếu không cho men vô thì sẽ bị hỏng và nó cũng sẽ được thể hiện dứoi dạng một attribute trong class Smoothie (isFreeze). Và chúng ta có cách code thông thường như sau:
public Smoothies makingSmoothiesWithExtraStep() {
//buying fruit from the supermarket
Fruit boughtFruit = Optional.ofNullable(buyFruit()).orElseThrow(RuntimeException::new);
//slicing fruit that you buy
SliceFruit slicedFruit = Optional.ofNullable(slicingFruit(boughtFruit)).orElseThrow(RuntimeException::new);
//get some ice for your smoothies
Ice iceForSmoothies = Optional.ofNullable(getIce()).orElseThrow(RuntimeException::new);
//blend the fruit with ice please and now you can enjoy the smoothies
Smoothies smoothies = Optional.ofNullable(blendWithIce(slicedFruit, iceForSmoothies)).orElseThrow(RuntimeException::new);
//now we need to ferment it but first we need to freeze it down
smoothies.setFreeze(true);
//then now we have to check if it is freeze before we ferment it
if (smoothies.isFreeze)
//we have to check if it is freeze before it is fermented
smoothies.setFerment(true);
else
//if it is not freeze we have to throw run time exception
throw new RuntimeException();
return smoothies;
}
Như vậy trước khi lên men cho smoothies ta sẽ phải check xem nó đã được làm lạnh hay chưa. Và người đọc còn phải ghi nhớ xem liệu attribute isFreeze đã được đăt ở đâu đó trong đoạn code trên, và trạng thái hiện tại của nó là gì, đặc biệt là khi attribute isFreeze còn được set ở trong một class hay một function khác, còn với Optional ta có thể kết hợp với method filter() để đạt được kết quả như trên.
public Smoothies makingSmoothiesWithOptionalAndExtraStepsAndFilter() {
return Optional.ofNullable(buyFruit())
.map(this::slicingFruit)
.map(slicedFruit -> Optional.ofNullable(getIce())
.map(ice -> blendWithIce(slicedFruit, ice))
.orElseThrow(RuntimeException::new))
.map(smoothies -> {
smoothies.setFreeze(true);
return smoothies;
}).filter(Smoothies::isFreeze)
.map(smoothies -> {
smoothies.setFerment(true);
return smoothies;
})
.orElseThrow(RuntimeException::new);
}
Rất chi là gọn gàng với việc ta có thể ràng buộc bước làm lạnh trước khi ta lên men cốc smoothie ngon lành của mình. Hoặc ta còn có thể bỏ đi những cố smoothie chưa được làm lạnh bằng cách sử dụng method filter(). Ở đây Optional giống như một máy trạng thái với một guard ở đằng trước mỗi trạng thái. Quan trọng là ta có thể hình dung ra được cách mà data thay đổi sau mỗi bước, điều này giúp cải thiện khả năng đọc của chúng rất nhiều. Giờ đây code Java có hình dạng giống như một high order function trong Functional Programming.
Việc mở rộng code cũng rất dễ dàng khi các bước trong code được phân chia cực kỳ tường minh, ta dễ dàng biêt được ta phải thêm code ở đâu dể không ảnh hưởng tới những phần khác.
NHƯỢC ĐIỂM CỦA OPTIONAL
Dù rất đẹp về mặt thiết kế cũng như khả năng đọc hiểu, nhưng Optional cũng tồn tại rất nhiều giới hạn, một trong số đó là khả năng debug. Với việc Java không hỗ trợ quá tốt việc debug sử dụng lambda function thì khi bị lỗi, lỗi sẽ được quăng ra khá chung chung và không thể chỉ rõ cho chúng ra dòng nào trong code của chúng ta bị lỗi. Đó là lí do best practice là chuyển toàn bộ body của lambda function trên thành một private method riêng biệt. Nó vừa giúp chúng ta dễ đọc vừa giúp việc debug trở nên dễ dàng hơn ví dụ như:
public Smoothies makingSmoothiesWithOptionalAndExtraStepsAndFilter() {
return Optional.ofNullable(buyFruit())
.map(this::slicingFruit)
.map(slicedFruit -> Optional.ofNullable(getIce())
.map(ice -> blendWithIce(slicedFruit, ice))
.orElseThrow(RuntimeException::new))
.map(smoothies -> {
smoothies.setFreeze(true);
return smoothies;
}).filter(this::checkIfSmoothieIsFreeze)
.map(this::fermentSmoothies)
.orElseThrow(RuntimeException::new);
}
private boolean checkIfSmoothieIsFreeze(Smoothies smoothies) {
return smoothies.isFreeze();
}
private Smoothies fermentSmoothies(Smoothies smoothies) {
smoothies.setFerment(true);
return smoothies;
}
Như ở ví dụ trên thì ta tách việc lên men cho smoothie trở thành một function riêng, và điều này còn giúp cho code cho chúng ta trở nên dễ đọc hơn. Thực tế việc đọc code trở nên gần như không tốn sức, khi mà các method rất ngắn gọn và có tê dễ hiểu.
Tuy nhiên việc debug cho lambda function vẫn rất khó khăn và chúng ta phải logging cho các function này thật cẩn thận. Thực ra thì để debug trên các môi trường như uat hay production thì log là một thành phần quan trọng hơn hẳn so với exception. Một phần là log cho chúng ta có cái nhìn tốt hơn về context khi xảy ra lỗi. Tuy nhiên thì log là một chủ đề cực kỳ rộng và có thể sẽ được viết trong một bài khác.
KẾT BÀI
Tóm lại bằng cách sử dụng Optional một cách khôn ngoan ta có thể tạo ra những dòng code có độ tin cậy cao và cực kỳ dễ đọc. Mình đánh giá đây là một coding style khá là thú vị, và việc sử dụng để tận dụng cả sức mạnh của object programming và functional programming trong code là cực kỳ tuyệt vời, tuy nhiên còn nhiều tranh cãi về khả năng debug cũng nhu tính hiệu quả của coding style này. Tuy nhiên việc áp dụng coding style này giúp cho code của mình giảm hẳn số bug và độ phức tạp. Việc maintain code này cũng nhanh chóng hơn nhiều, và mình đang thử nghiệm theo hướng này trong vài tháng gần đây. =))) Mong là Tết này không có bug.
Discussion
Mình mong nhận được góp ý cũng như tranh luận của mọi người. Anh em dev nào muốn thảo luận hơn có thể lh qua telegram : https://telegram.org/dl .
All rights reserved