Type safe builder pattern trong scala
Scala
50
White

huydx viết ngày 22/03/2016

Mở đầu

Chú ý: bài viết khá khó với những ai không biết về một số khái niệm của scala như implicit, type programming

Bài viết tham khảo kĩ thuật trong blog của Rafael tại : http://blog.rafaelferreira.net/2008/07/type-safe-builder-pattern-in-scala.html
Đây là một kĩ thuật rất thú vị trong type programming của scala nhằm để tạo ra các "luật" (constraint) cho một class hay một số logic nhất định. Chắc những ai có hứng thú với các ngôn ngữ có "kiểu" (như java/scala/haskell...) đều sẽ nhận thấy kiểu (type) là một phương thức để chúng ta viết ra đoạn code an toàn hơn, dựa vào việc tạo ra các "bộ luật" được biểu diễn dưới dạng type.

Một ví dụ vô cùng đơn giản của việc tạo ra "luật rừng" nó tốt hơn thế nào: giả sử bạn có một đoạn logic như dưới đây

scala

val x: Int = "1"
val y: String = ""

println(x * y)

Và kết quả sẽ là không compile được

scala> x * y
<console>:10: error: overloaded method value * with alternatives:
  (x: Double)Double <and>
  (x: Float)Float <and>
  (x: Long)Long <and>
  (x: Int)Int <and>
  (x: Char)Int <and>
  (x: Short)Int <and>
  (x: Byte)Int
 cannot be applied to (String)
              x * y

Tuy nhiên nếu đoạn code tương tự được viết bằng ruby

ruby

 x = 1
 y = ""
 p (x * y)

thì kết quả sẽ là exception được ném ra tại Run time thay vì Compile time giống scala

 TypeError: String can't be coerced into Fixnum from (pry):3:in `*'

Như vậy type đã "giúp" chúng ta có một đoạn code an toàn hơn, thông báo cho chúng ta về lỗi tại thời điểm sớm hơn, dễ khắc phục hơn là compile time.

Tuy nhiên có những lúc ngay cả có type cũng không thể ngăn chúng ta tạo ra những lỗi tại Run time, thì khi đó một số ngôn ngữ có compiler "mạnh" , mà điển hình là scala, sẽ hỗ trợ chúng ta một số technique (gọi là type programming) để giúp chúng ta tạo ra nhứng "luật" chặt chẽ hơn so thông thường.

Bài toán: Builder pattern trên scala

Builder Pattern là gì :)

Chắc hẳn bạn nào lập trình java, từng đọc qua cuốn Effective Java của Joshua Bloch đều thông thạo một design gọi là Builder Pattern. Về cơ bản pattern này giúp chúng ta 2 việc

  • Set parameter cho một object một cách lần lượt (thay vì ném toàn bộ vào constructor parameter), giúp cho code nhìn đẹp hơn
  • Parameter được set vào theo builder pattern sẽ là Immutable (tức là không thể thay đổi)

Bạn nào chưa biết thì có thể google, còn nếu không có thể tham khảo builder pattern theo "phong cách java", và được "viết bằng scala" dưới đây

sealed abstract class Preparation  /* This is one way of coding enum-like things in scala */
case object Neat extends Preparation
case object OnTheRocks extends Preparation
case object WithWater extends Preparation

sealed abstract class Glass
case object Short extends Glass
case object Tall extends Glass
case object Tulip extends Glass

case class OrderOfScotch(val brand:String, val mode:Preparation, val isDouble:Boolean, val glass:Option[Glass])


class ScotchBuilder {
  private var theBrand:Option[String] = None
  private var theMode:Option[Preparation] = None
  private var theDoubleStatus:Option[Boolean] = None
  private var theGlass:Option[Glass] = None

  def withBrand(b:Brand) = {theBrand = Some(b); this} /* returning this to enable method chaining. */
  def withMode(p:Preparation) = {theMode = Some(p); this}
  def isDouble(b:Boolean) = {theDoubleStatus = some(b); this}
  def withGlass(g:Glass) = {theGlass = Some(g); this}

  def build() = new OrderOfScotch(theBrand.get, theMode.get, theDoubleStatus.get, theGlass);
}

import ScotchBuilder._
val builder = new ScotchBuilder
builder.withBrand("Lua moi").withMode(Neat ).withDoubleStatus(false).withGlass(Tall).build()

Ok, vậy là chúng ta đã có một chai rượu Scotch "Lúa mới" được build bằng Builder Pattern style.
Cơ mà ai tinh ý sẽ thấy chúng ta đang dùng scala cơ mà, tại sao lại dùng var theo phong cách set/get, phản cách mạng quá :D.

Builder Pattern theo phong cách functional

Như đã từng viết ở bài viết Functional Programming và state Sửa bài viết , theo phong cách functional thì chúng ta sẽ truyền state vào thông qua parameter thay vì dùng set/get.
Áp dụng vào bài toán ở trên chúng ta sẽ có đoạn code sau:

object BuilderPattern {
  class ScotchBuilder(theBrand:Option[String], theMode:Option[Preparation], theDoubleStatus:Option[Boolean], theGlass:Option[Glass]) {
    def withBrand(b:String) = new ScotchBuilder(Some(b), theMode, theDoubleStatus, theGlass)
    def withMode(p:Preparation) = new ScotchBuilder(theBrand, Some(p), theDoubleStatus, theGlass)
    def isDouble(b:Boolean) = new ScotchBuilder(theBrand, theMode, Some(b), theGlass)
    def withGlass(g:Glass) = new ScotchBuilder(theBrand, theMode, theDoubleStatus, Some(g))

    def build() = new OrderOfScotch(theBrand.get, theMode.get, theDoubleStatus.get, theGlass);
  }

  def builder = new ScotchBuilder(None, None, None, None)
}

Ok vậy chúng ta "tạm" giải quyết vấn đề về việc không dùng var. Tuy nhiên nếu ai đã từng làm việc nhiều với builder pattern sẽ biết là tồn tại một vấn đề khá thú vị là

Với những parameter "bắt buộc" mà chúng ta quên mất không set thì chuyện gì sẽ xảy ra???

Tất nhiên là chúng ta sẽ chết lúc execute hàm build() rồi!
Khi đó chúng ta sẽ gọi get trên biến None và scala sẽ ném cho chúng ta một cái Exception vào mặt, tại RunTime!!!

java.util.NoSuchElementException: None.get
  at scala.None$.get(Option.scala:322)
  ... 33 elided

Vậy bao công sức của chúng ta để tạo ra một chương trình safe tại Run time là bỏ đi??
Không có chuyện đó được :D, may mắn là chúng ta đang làm việc với một ngôn ngữ với một compiler vô cùng mạnh là Scala

Type safe Builder Pattern sử dụng Type Programming

Đầu tiên chúng ta sẽ "encode" việc có hay không có một parameter bằng 2 type, gọi là TRUE và FALSE

abstract class TRUE
abstract class FALSE

Sau đó chúng ta sẽ "encode" tiếp builder của chúng ta bằng việc thêm vào type parameter tương ứng với việc từng parameter có tồn tại hay không

class ScotchBuilder[HB, HM, HD](val theBrand:Option[String], val theMode:Option[Preparation], val theDoubleStatus:Option[Boolean], val theGlass:Option[Glass]) {

Ở đây HB, HM, HD đại diện cho viẹc parameter Brand, Mode và Double "có được truyền vào hay không"
Sau đó chúng ta sẽ "encode" tiếp từng function của builder bằng type parameter TRUE|FALSE tương ứng

class ScotchBuilder[HB, HM, HD](val theBrand:Option[String], val theMode:Option[Preparation], val theDoubleStatus:Option[Boolean], val theGlass:Option[Glass]) {
  def withBrand(b:String) = 
      new ScotchBuilder[TRUE, HM, HD](Some(b), theMode, theDoubleStatus, theGlass)

  def withMode(p:Preparation) = 
    new ScotchBuilder[HB, TRUE, HD](theBrand, Some(p), theDoubleStatus, theGlass)

  def isDouble(b:Boolean) = 
    new ScotchBuilder[HB, HM, TRUE](theBrand, theMode, Some(b), theGlass)

  def withGlass(g:Glass) = 
    new ScotchBuilder[HB, HM, HD](theBrand, theMode, theDoubleStatus, Some(g))
}

Và cuối cùng chính là phần hay nhất, để qui định "constraint" cho builder của chúng ta là: cần sự hiện diện của cả 3 parameter Brand, Mode và Double, chúng ta sẽ sử dụng kĩ thuật pimp my library nổi tiếng của scala thông qua implicit class

implicit def enableBuild(builder:ScotchBuilder[TRUE, TRUE, TRUE]) = new {
  def build() = 
    new OrderOfScotch(builder.theBrand.get, builder.theMode.get, builder.theDoubleStatus.get, builder.theGlass);
}

Ở đây chúng ta "encode" cả 3 type parameter dưới dạng TRUE, và compile sẽ làm hộ chúng ta việc quan trọng nhất

Chỉ khi cả 3 parameter Brand, Mode và Double đều là TRUE, thì chúng ta mới gọi được hàm build()

Đây chính là điều thú vị của type programming, nó cho phép chúng ta tạo ra những "constraint" hay là "luật rừng", chỉ dựa vào việc type encode.
Hãy thử test xem nào

scala> builder withBrand("hi") isDouble(false) withGlass(Tall)  build()                
<console>:9: error: value build is not a member of BuilderPattern.ScotchBuilder[BuilderPattern.TRUE,BuilderPattern.FALSE,BuilderPattern.TRUE]
       builder withBrand("hi") isDouble(false) withGlass(Tall)  build()

Như vậy chúng ta đã đạt được mục đích của mình, là sử dụng type để tạo ra builder với constraint về việc parameter nào là bắt buộc, parameter nào không. 2 type TRUEFALSE được tạo ra chỉ với mục đích là "tạo constraint" mà không hề được khởi tạo lần nào, và trong functional programming nó được gọi là "Phantom type".

Ở trong bài viết này các bạn đã thấy một số điểm thú vị sau:

  • Bản chất của type programming là việc "encode", làm sao bạn có thể "nhét" được càng nhiều thông tin vào type càng tốt.
  • Sử dụng implicit là một kĩ thuật phổ biến trong type programming. Lý do tại sao ? Tại vì khi bạn gọi một hàm implicit, khi đó compiler sẽ thực hiện việc "tính toán" type. Việc "tính toán" này của scala compiler đã được chứng minh là Turing complete, do đó vấn đề chỉ là bạn encode thế nào thôi. Encoding trong type programming đòi hỏi rất nhiều kỹ thuật như là số peano, mà mình sẽ giới thiệu chi tiết sau.

Toàn bộ đoạn code full ở dưới đây, các bạn có thể copy về máy cá nhân để nghịch :D

object BuilderPattern {
  sealed abstract class Preparation
  case object Neat extends Preparation
  case object OnTheRocks extends Preparation
  case object WithWater extends Preparation

  sealed abstract class Glass
  case object Short extends Glass
  case object Tall extends Glass
  case object Tulip extends Glass

  case class OrderOfScotch(val brand:String, val mode:Preparation, val isDouble:Boolean, val glass:Option[Glass])

  abstract class TRUE
  abstract class FALSE

  class ScotchBuilder
  [HB, HM, HD]
  (val theBrand:Option[String], val theMode:Option[Preparation], val theDoubleStatus:Option[Boolean], val theGlass:Option[Glass]) {
    def withBrand(b:String) = 
      new ScotchBuilder[TRUE, HM, HD](Some(b), theMode, theDoubleStatus, theGlass)

    def withMode(p:Preparation) = 
      new ScotchBuilder[HB, TRUE, HD](theBrand, Some(p), theDoubleStatus, theGlass)

    def isDouble(b:Boolean) = 
      new ScotchBuilder[HB, HM, TRUE](theBrand, theMode, Some(b), theGlass)

    def withGlass(g:Glass) = new ScotchBuilder[HB, HM, HD](theBrand, theMode, theDoubleStatus, Some(g))
  }

  implicit def enableBuild(builder:ScotchBuilder[TRUE, TRUE, TRUE]) = new {
    def build() = 
      new OrderOfScotch(builder.theBrand.get, builder.theMode.get, builder.theDoubleStatus.get, builder.theGlass);
  }

  def builder = new ScotchBuilder[FALSE, FALSE, FALSE](None, None, None, None)
}
Bình luận


White
{{ comment.user.name }}
Bỏ hay Hay
{{comment.like_count}}
Male avatar
{{ comment_error }}
Hủy
   

Hiển thị thử

Chỉnh sửa

White

huydx

115 bài viết.
858 người follow
Kipalog
{{userFollowed ? 'Following' : 'Follow'}}
Cùng một tác giả
White
135 8
Introduction (Link) là một cuộc thi ở Nhật, và cũng chỉ có riêng ở Nhật. Đây là một cuộc thi khá đặc trưng bởi sự thú vị của cách thi của nó, những...
huydx viết hơn 1 năm trước
135 8
White
109 14
Happy programmer là gì nhỉ, chắc ai đọc xong title của bài post này cũng không hiểu ý mình định nói đến là gì :D. Đầu tiên với cá nhân mình thì hap...
huydx viết gần 3 năm trước
109 14
White
86 10
(Ảnh) Mở đầu Chắc nhiều bạn đã nghe đến khái niệm oauth. Về cơ bản thì oauth là một phương thức chứng thực, mà nhờ đó một web service hay một ap...
huydx viết hơn 2 năm trước
86 10
Bài viết liên quan
White
10 0
Kí tự Regex cơ bản Về cơ bản thì các sử lý matching của scala.util.matching.Regex sẽ được "phó thác" (delegate) cho java Regex. Bạn có thể tạo một ...
huydx viết hơn 2 năm trước
10 0
White
7 1
Trong scala kí tự _ được dùng với khá nhiều mục đích .. không liên quan đến nhau. Tạm note lại cái đã khi nào có time sẽ quay lại viết cẩn thận sa...
huydx viết hơn 2 năm trước
7 1
White
0 0
(Bài viết hơi khó hiểu, dành cho bạn nào có hứng thú với type programming trong scala với các thư viện như shapeless chẳng hạn) Thông thường với m...
huydx viết gần 2 năm trước
0 0
{{like_count}}

kipalog

{{ comment_count }}

bình luận

{{liked ? "Đã kipalog" : "Kipalog"}}


White
{{userFollowed ? 'Following' : 'Follow'}}
115 bài viết.
858 người follow

 Đầu mục bài viết

Vẫn còn nữa! x

Kipalog vẫn còn rất nhiều bài viết hay và chủ đề thú vị chờ bạn khám phá!