Trong lập trình một ngôn ngữ, chúng ta sẽ không bao giờ có thể tránh khỏi lỗi xảy ra ở trong chương trình dù nguyên nhân lỗi đó có thể là do khách quan mà chúng ta thậm trí không kiểm soát được. Các ngôn ngữ đều hỗ trợ chúng ta kiểm soát những lỗi không mong muốn này và tất nhiên Scala cũng vậy.
Scala giúp chúng ta xử lý và kiểm soát các lỗi này một cách rất tự nhiên. Trong bài viết này chúng ta sẽ cùng nhau tìm hiểu cách tiếp cận xử lý các lỗi dựa trên kiểu Try
.
Throw và Catch các Exception
Trước khi chúng ta đi tìm hiểu cách tiếp cận của Scala trong việc xử lý các lỗi, chúng ta hãy cùng xem qua cách tiếp cận mà chúng ta sử dụng tương đối phổ biến trong các ngôn ngữ khác khi làm việc với các điều kiện lỗi. Giống như các ngôn ngữ khác, Scala cũng cho phép chúng ta throw một Exception
case class Customer(age: Int) class Cigarettes case class UnderAgeException(message: String) extends Exception(message) def buyCigarettes(customer: Customer): Cigarettes = if (customer.age < 16) throw UnderAgeException(s"Customer must be older than 16 but was ${customer.age}") else new Cigarettes
Khi throw các Exception, chúng ta có thể dễ dàng Catch và xử lý tương tự như trong Java, sử dụng một hàm partial để xác định các exception mà chúng ta muốn xử lý. Tương tự như thế, try/catch
của Scala là một expression, nên theo như đoạn code bên dưới đây chúng ta sẽ trả về thông báo lỗi
val youngCustomer = Customer(15) try { buyCigarettes(youngCustomer) "Yo, here are your cancer sticks! Happy smokin'!" } catch { case UnderAgeException(msg) => msg }
Xử lý lỗi theo cách Functional
Với cách xử lý lỗi như bên trên, chúng ta có thể dễ dàng thấy rằng nó sẽ khiến cho code trở nên bẩn thỉu rất nhanh và nó thực sự không hợp lý với Functional Programming. Nó cũng không tốt đối với các ứng dụng có nhiều xử lý đồng thời. Ví dụ nếu bạn cần xử lý một Throw Exception trong một Actor mà được chạy trong các thread khác nhau, bạn chắc chắn sẽ không thể xử lý catch Exception – Bạn chắc chắn sẽ mong muốn có thể nhận được thông báo lỗi khi có một điều kiện lỗi xảy ra.
Do đó trong Scala, nó thường hiển thị một lỗi xảy ra bằng cách trả về một giá trị thích hợp từ hàm của bạn. Scala sử dụng một kiểu riêng đại diện cho các tính toán với kết quả trả về là một Exception.
Thuật ngữ Try
Thuật ngữ Try là cách tốt nhất để giải thích khi so sánh nó với kiểu Option
Option[A]
là một container cho giá trị của kiểu A
mà nó có thể tồn tại hoặc không. Try[A]
biểu diễn cho một tính toán mà kết quả có thể là một giá trị kiểu A
nếu không có lỗi gì xảy ra, hoặc là một vài Throwable
nếu có lỗi gì đó xảy ra. Trường hợp trả về như một kiểu container mục đích để các lỗi có thể xảy ra dễ dàng được truyền qua lại giữa các phần đang thực hiện chạy bất đồng bộ trong ứng dụng.
Có hai kiểu Try
khác nhau: Nếu một instace của Try[A]
đại diện cho một tính toán thành công, thì nó là một instace của Success[A]
, đơn giản là đóng gói giá trị của kiểu A
. Ngoài ra nó đại diện cho một tính toán xảy ra lỗi, nó là instace của Failure[A]
, đóng gói một Throwable
ví dụ một Exception hoặc các kiểu lỗi khác.
Nếu chúng ta biết rằng một tính toán có thể xảy ra lỗi, chúng ta chỉ cần đơn giản sử dụng Try[A]
như một kiểu trả về cho hàm chúng ta. Điều này giúp cho nó trở nên rõ ràng hơn và bắt buộc các client sử dụng hàm phải xử lý trong một vài trường hợp lỗi có thể xảy ra.
Ví dụ chúng ta có thể viết một hàm dùng để tải một trang web về. Người dùng có thể nhập vào một URL của trang web mà họ muốn tải về. Hàm sẽ thực hiện phân tích Url nhập vào và tạo một java.net.URL
từ đấy
import scala.util.Try import java.net.URL def parseURL(url: String): Try[URL] = Try(new URL(url))
Như bạn nhìn thấy nó sẽ trả về một giá trị của kiểu Try[URL]
. Nếu url truyền vào có format chuẩn, thì nó sẽ trả về một Success[URL]
. Nếu URL throw một MalformedURLException
, thì nó sẽ trả về một Failure[MalformedURLException]
.
Để thực hiện điều này, chúng ta đang sử dụng phương thức factory apply
trên Try
là một companion object. Phương thức này nhận đầu vào là tên của tham số với kiểu là A
ở đây là URL
. Trong ví dụ của chúng ta, điều này nghĩa là new URL(url)
được thực hiện bên trong phương thức apply
của Try
object. Trong phương thức, các Exception được bắt và trả về một Failure
chứa Exception tương ứng.
Do đó, parseURL("https://itexpertvn.com")
sẽ trả về một Success[URL]
chứa URL được tạo, còn parseURL("url")
sẽ trả về một Failure[URL]
chứa một MalformedURLException
Giá trị của Try
Làm việc với các instance của Try
chắc chắn là rất giống với với các giá trị của Option
, nên bạn sẽ không thấy điều gì mới mẻ ở đây.
Bạn có thể kiểm tra nếu một Try
trả về thành công bằng cách gọi phương thức isSuccess
của nó và sau đó lấy giá trị được đóng gói bằng cách gọi hàm get
. Nhưng tất nhiên là trong thực tế sẽ không có nhiều trường hợp mà chúng ta cần xử lý như trên
Nó cũng có thể sử dụng getOrElse
để truyền giá trị mặc định được trả về nếu Try
là một Failure
val url = parseURL(Console.readLine("URL: ")) getOrElse new URL("https://default-url.com")
Nếu URL truyền vào bởi người dùng là sai format, chúng ta sẽ sử dụng default-url.com
như một dự phòng thay thế
Các hoạt động
Một trong những đặc điểm quan trọng của Try
đó là giống như Option
nó hỗ trợ tất các các phương thức higher-order mà bạn biết sử dụng trong các kiểu collection.
Mapping và flat maping
Mapping một Try[A]
mà là một Success[A]
thành một kết quả Try[B]
trong một Success[B]
. Nếu nó là một Failure[A]
, kết quả Try[B]
sẽ là một Failure[B]
, ngoài ra chứa Exception giống với Failure[A]
.
parseURL("https://itexpertvn.com").map(_.getProtocol) // Kết quả là Success("https") parseURL("garbage").map(_.getProtocol) // Kết quả là Failure(java.net.MalformedURLException: no protocol: garbage)
Nếu chúng ta sử dụng một chuỗi các hoạt động map
, kết quả trả về sẽ là nhiều Try
lồng vào nhau mà bạn không mong muốn. Hãy cùng xem ví dụ sau trả về một InputStream
cho URL
truyền vào
import java.io.InputStream def inputStreamForURL(url: String): Try[Try[Try[InputStream]]] = parseURL(url).map { u => Try(u.openConnection()).map(conn => Try(conn.getInputStream)) }
Vì các hàm anonymous được truyền để 2 map gọi mỗi lần trả về một Try
và kiểu trả về là một Try[Try[Try[InputStream]]]
Đây là nơi mà bạn có thể sử dụng flatMap
một Try
có ích. Phương thức flatMap
trên Try[A]
được mong đợi để một hàm được truyền sẽ nhận được A
và trả về một Try[B]
. Nếu instace Try[A] của chúng ta là một Failure[A]
, lỗi này sẽ được trả về như một Failure[B]
. Nếu Try[A]
là một Success[A]
, flatMap
mở giá trị A
trong nó và map nó thành Try[B]
bằng cách truyền giá trị này đến hàm map.
Điều này nghĩa là chúng ta có thể tạo một chuỗi các hoạt động mà yêu cầu các giá trị thực hiện trên Success
instance bằng cách kết nối một số lượng các cuộc gọi flatMap
tuỳ ý. Bất ký Exception nào xảy ra trong quá trình xử lý được đóng gói trong một Failure
, nghĩa là kết quả cuối cùng cho chuỗi các hoạt động này cũng là một Failure
.
Chúng ta hãy thử viết lại phương thức inputStreamForURL
trong ví dụ bên trên sử dụng flatMap
def inputStreamForURL(url: String): Try[InputStream] = parseURL(url).flatMap { u => Try(u.openConnection()).flatMap(conn => Try(conn.getInputStream)) }
Bây giờ, chúng ta có thể nhận được một Try[InputStream]
mà có thể đóng gói một Failure
của một Exception khi có bất kỳ một throw Exception nào xảy ra, hay một Success
đóng gói trực tiếp InputStream
, kết quả cuối cùng của một chuỗi các hoạt động.
Filter và foreach
Tất nhiên là chúng ta cũng có thể thực hiện lọc một Try
hoặc gọi phương thức foreach
. Cả 2 đều hoạt động chính xác giống như chúng ta sử dụng Option
Phương thức filter
sẽ trả về một Failure
nếu Try
được gọi là một Failure
hoặc nếu điều kiện lọc trả về false
(trong trường hợp này nó sẽ đóng gói một Exception là NoSuchElementException
). Nếu Try
được gọi là một Success
và điều kiện lọc là true
, Success
được trả về sẽ không thay đổi.
def parseHttpURL(url: String) = parseURL(url).filter(_.getProtocol == "http") parseHttpURL("https://apache.openmirror.de") // kết quả là Success[URL] parseHttpURL("ftp://mirror.netcologne.de/apache.org") // kết quả là Failure[URL]
Hàm được truyền đến foreach
chỉ được chạy nếu Try
là một Success
, cho phép bạn thực hiện một side-effect. Hàm được truyền đến foreach chỉ chạy đúng một lần trong trường hợp này
parseHttpURL("https://itexpertvn.com").foreach(println)
For comprehension
Hỗ trợ map, flatMap, foreach và filter nghĩa rằng chúng ta cũng có thể sử dụng For comprehension mục đích để xử lý một chuỗi các hoạt động trên Try
. Thông thường sử dụng For
thì code đọc sẽ dễ hiểu hơn. Để chứng minh điều này chúng ta hãy cùng xem ví dụ sau đây, tạo một phương thức trả về nội dung của trang web với URL nhập vào sử dụng for comprehension
import scala.io.Source def getURLContent(url: String): Try[Iterator[String]] = for { url <- parseURL(url) connection <- Try(url.openConnection()) is <- Try(connection.getInputStream) source = Source.fromInputStream(is) } yield source.getLines()
Có 3 phần mà có thể xảy ra lỗi, tất cả đều được chúng ta kiểm soát bằng cách sử dụng kiểu Try.
Đầu tiên chúng ta đã tạo phương thức parseURL
trả về một Try[URL]
. Chỉ khi nó trả về Success[URL]
, chúng ta sẽ thử mở một kết nối dùng openConnection
và tạo một InputStream
mới từ nó sử dụng getInputStream
. Nếu mở một kết nối và tạo InputStream
thành công, nó sẽ tiếp tục chạy tiếp, cuối cùng sử dụng yield
để lầy các hàng của trang web sử dụng getLine
. Bởi vì chúng ta thực hiện một chuỗi các cuộc gọi nên hàm flatMap được gọi trong for comprehension. Kiểu của kết quả trả về là “phẳng” Try[Iterator[String]]
Pattern matching
Trong một vài thời điểm trong code của bạn, bạn thường mong muốn được biết liệu Try
instance mà bạn đã nhận được như là kết quả của các tính toán trả về thành công hoặc lỗi xảy ra và thực hiện các nhánh source code khác nhau dựa trên kết quả. Thông thường chúng ta sẽ sử dụng pattern matching trong trường hợp này. Điều này có thể thực hiện dễ dàng bởi vì cả Success
và Failure
đều là case class.
Chúng ta muốn render ra một trang web nếu nó có thể được lấy, hoặc in ra một thông tin lỗi nếu không thể
import scala.util.Success import scala.util.Failure getURLContent("http://danielwestheide.com/foobar") match { case Success(lines) => lines.foreach(println) case Failure(ex) => println(s"Problem rendering URL content: ${ex.getMessage}") }
Recover từ Failure
Nếu bạn muốn thiết lập một vại hoạt động mặc định trong trường hợp một Failure
, bạn không phải sử dụng getOrElse
. Sử dụng recover
thay thế, mà dự đoạn một partial function và trả về một Try
khác. Nếu recover
được gọi trên một Success
instance, instance đó sẽ được trả về. Ngoài ra nếu partial function được định nghĩa giống với Failure
instace đang xảy ra, kết qủa của nó được trả về như một Success
Hãy cùng xem ví dụ bên dưới đây
import java.net.MalformedURLException import java.io.FileNotFoundException val content = getURLContent("garbage") recover { case e: FileNotFoundException => Iterator("Requested page does not exist") case e: MalformedURLException => Iterator("Please make sure to enter a valid URL") case _ => Iterator("An unexpected error has occurred. We are so sorry!") }
Chúng ta có thể gọi .get một cách an toàn vì trong mọi trường hợp cả khi lỗi xảy ra các giá trị đều được đóng gói trả về trong Try[Interator[String]]
được gán vào content. Thử gọi content.get.foreach(println)
kết quả là Please make sure to enter a valid URL
được in ra trên console