Scala: Trait và lập trình hướng đối tượng nâng cao
Scala
50
White

Vu Nhat Minh viết ngày 10/04/2016

Chương trình phần mềm phát triển cho các hệ thống lớn và lâu đời quan trọng nhất là tính đóng gói và cách lắp ghép các đơn vị gói đó lại với nhau. Khi lập trình hướng đối tượng ra đời, tính đóng gói được phát triển thành một lý thuyết có chiều sâu và nhiều pattern đi kèm với nó.
Đối với lập trình hướng đối tượng hay cả lập trình hàm thì tính đóng gói vẫn là những khái niệm cơ bản và quan trọng nhất. Hôm nay chúng ta sẽ tìm hiểu một khái niệm quan trọng nhất trong lý thuyết hướng đối tượng của Scala: Trait.

Object Oriented Programming

Cơ bản về Trait

Trait trong Scala có những điểm khác biệt chủ yếu với class như sau

  • Một Trait hoặc một class có thể kế thừa nhiều trait khác nhau (tính năng này gọi là Mixin hoặc Multiple inheritance)
  • Không thể khởi tạo trực tiếp instance từ Trait.
  • Không thể nhận parameter (constructor parameter) như class.

Chúng ta sẽ đi vào từng điểm một

Multiple inheritance

Đọc tiêu đề có lẽ bạn đã hiểu được phần nào. Dưới đây là ví dụ

trait Saiyan
trait Namek
class Human
class God

// OK
class SuperHero extends Human with Saiyan with Namek

// Compile error
class SemiGod extends Human with God

Như bạn thấy ở dòng cuối, một class không thể kế thừa 2 class khác cùng một lúc. Tuy nhiên với trait thì không có giới hạn. Ở trường hợp cuối khi compile sẽ ra lỗi

class God needs to be a trait to be mixed in

Không thể tạo instance trực tiếp

Nếu cố gắng tạo instance thì sẽ gặp lỗi như sau

trait Saiyan
object  {
  val bardock = new Saiyan
  // Compile error
  // trait Saiyan is abstract; cannot be instantiated
}

Khi gặp trường hợp thế này thì bạn có 2 lựa chọn:

  • Tạo một class kế thừa trait nói trên và tạo instance của class đó
  • Tạo instance thông qua việc thêm implement
trait Saiyan
class SaiyanSoldier extends Saiyan

object SaiyanArmy {
  val bardock = new SaiyanSoldier // OK
  val kingVegeta = new Saiyan {} // OK
}

Không thể nhận constructor như class

Vì không thể tạo instance nên Trait cũng không có khả năng nhận constructor parameter như class

trait Saiyan(name: String) {}
// Compile error
// traits or objects may not have parameters

Để giải quyết tình huống này cũng có 2 cách tương tự như trên:

  • Tạo class kế thừa và gọi new với class đó
  • truyền thẳng paramter vào implement của trait
trait Saiyan {
  val name: String
}
class SaiyanSoldier(val name: String) extends Saiyan

object SaiyanArmy {
  val bardock = new SaiyanSoldier("bardock")
  val kingVegeta = new Saiyan { val name = "kingVegeta" }
}

Diamond Problem

Diamond Problem
Diamond Problem là một danh từ khá nổi tiếng chỉ về một vấn đề trong cấu trúc kế thừa của lập trình hướng đối tượng. Với lý thuyết về Trait bên trên, bạn thử tưởng tượng nếu 2 trait cùng kế thừa một trait khởi điểm và override cùng một method, vậy khi class mới kế thừa 2 trait nói trên thì chuyện gì sẽ xảy ra ?

trait A {
  def greet(): Unit
}

trait B extends A {
  def greet(): Unit = println("Hello!")
}

trait C extends A {
  def greet(): Unit = println("Hi!")
}

class D extends B with C 
// Copile error
/** 
error: class D inherits conflicting members:
  method greet in trait B of type ()Unit  and
  method greet in trait C of type ()Unit
(Note: this can be resolved by declaring an override in class D.)
       class D extends B with C
*/

Cách giải quyết y như thông báo lỗi đã gợi ý, chúng ta sẽ override method trong class D. Khi overide bạn có thể gọi lại method từ trait nào đã kế thừa tùy thích hoặc thậm chí gọi cả 2 luôn cũng được.

class D extends B with C {
  override def greet(): Unit = {
    super[B].greet()
    super[C].greet()
  }
}

Stackable Trait

Giờ mình sẽ nhắc đến một trường hợp đặc biệt hơn. Nếu định nghĩa hàm greet trong cả trait BC đều có từ khóa override thì thứ tự kế thừa sau cùng trong câu khai báo class sẽ quyết định hàm nào được sử dụng. Điều này gọi là Linearization. Các trait ở trường hợp này gọi là Stackable Trait.

trait A {
  def greet(): Unit = print(" Borned! ")
}

trait B extends A {
  super.greet()
  override def greet(): Unit = println("Hello!")
}

trait C extends A {
  super.greet()
  override def greet(): Unit = println("Hi!")
}

class D extends B with C
class E extends C with B
(new D).greet() 
// Borned!
// Hello
// Hi
(new E).greet()
// Borned!
// Hi
// Hello

Ở trường hợp trên, nếu method greettrait A không được định nghĩa thì (dĩ nhiên là) bạn không thể gọi super.greet() một cách hồn nhiên như vậy được :smile:. Khi này định nghĩa cho greet sẽ cần thêm từ khóa abstract.

trait A {
  def greet(): Unit
}

trait B extends A {
  abstract override def greet(): Unit = {
    super.greet()
    override def greet(): Unit = println("Hello!")
  }
}

Và (lại dĩ nhiên rằng) với kiểu khai báo trên thì bạn không thể tạo ra một class kế thừa trực tiếp trait B được. Lý do là, method greet đã được định nghĩa đâu ? Muốn thoát khỏi tình huống này thì cần phải implement method greet thông qua một trait khác và kế thừa trait đó

trait C extends A {
  def greet(): Unit = println("Hello!")
}

// Compile error
// class D needs to be a mixin, since method greet in trait B of type ()Unit is marked `abstract' and `override', but no concrete implementation could be found in a base
class D extends B

// Ok
class ClassB extends C with B

Self types

Những kiến thức ở phần trình bày bên trên thật dễ phải không :smile: Chỉ cần làm lập trình hướng đối tượng một thời gian thì sẽ không khó khăn để hình dung. Bây giờ mình sẽ trình bày một phần advance hơn một chút. Chúng ta thử tạo trait với self types xem nhé!

trait Saiyan {
  def kamehameha(): Unit
}

trait SuperSaiyan {
  self: Saiyan =>

  def damage(): Unit = kamehameha()
}

trait SaiyanSoldier extends Saiyan {
  def kamehameha(): Unit = println("KaaaaMeeeeHaaaMeeeeHaaaa!")
}

val vegeta = new SuperSaiyan with SaiyanSoldier
vegeta.damage()

Khi định nghĩa SuperSaiyan với từ khóa self: Saiyan như trên, trait SuperSaiyan có thể sử dụng method kamehameha trong khi bản thân không extends trực tiếp Saiyan. Khi tạo một trait SaiyanSoldier để implement thì có thể tạo được instance của SuperSaiyan.
Tạo sao lại phải tạo trait để implement thì lý do khá giống với phần abstract override bên trên, Scala không cho phép tạo instance với trait có method chưa được implement.

Vậy self types khác với extends trực tiếp thế nào ? SuperSaiyanGod dưới đấy có hành vi gì khác với SuperSaiyan ?

trait Saiyan {
  def kamehameha(): Unit
}

trait SuperSaiyanGod extends Saiyan {
  def damage(): Unit = kamehameha()
}

Câu trả lời là khi khởi tạo instance và chỉ rõ kiểu SuperSaiyan hay SuperSaiyanGod thì cách nhìn của instance đó với method gốc kamehameha() sẽ khác nhau!

val vegeta: SuperSaiyan = new SuperSaiyan with SaiyanSoldier
vegeta.kamehameha() // error: value kamehameha is not a member of SuperSaiyan
val goku: SuperSaiyanGod = new SuperSaiyanGod with SaiyanSoldier
goku.kamehameha() // KaaaaMeeeeHaaaMeeeeHaaaa

Bạn hãy chú ý đoạn val vegeta: SuperSaiyan =...val goku: SuperSaiyanGod =... nhé!

Self types cho phép định nghĩa trước một trait trừu tượng và thêm implement về sau. Self types thường được dùng khi muốn ứng dụng Dependency Injection hay gọi tắt là DI. Mỗi khi nào bạn nhìn thấy kiểu self types thế này thì có thể chuẩn bị tinh thần rằng "chuẩn bị có DI xảy ra đây" :smile:

Lời cuối

Lập trình hướng đối tượng trong Scala là ... không dễ, và để áp dụng được thuần thục cũng ... không dễ. Để hiểu hết thì phải làm thử việc refactoring cho một đoạn code chưa theo chuẩn hướng đối tượng, mà việc đó thì hơi quá sức với mình tại thời điểm này. Khi nào mình có thời gian sẽ nghiên cứu và viết tiếp.

Happy coding with Scala!

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

Vu Nhat Minh

54 bài viết.
775 người follow
Kipalog
{{userFollowed ? 'Following' : 'Follow'}}
Cùng một tác giả
White
119 30
Nếu bạn thường vào trang mua sắm của amazon, chắc sẽ chẳng lạ gì với menu Shop by Department. Tốc độ hiển thị nội dung của menu là tức thì so với d...
Vu Nhat Minh viết gần 3 năm trước
119 30
White
93 4
Lời người dịch Người dịch là một developer , sau khi tìm đọc được bài viết này bằng bản gốc tiếng Anh đã cảm thấy như được "khai sáng" về khả năng...
Vu Nhat Minh viết 3 năm trước
93 4
White
61 7
Form là thành phần quan trọng nhất khi design flow đăng ký của 1 web hay 1 app, dù là view gồm nhiều bước hay chỉ là một màn hình đơn điệu. Bài này...
Vu Nhat Minh viết hơn 1 năm trước
61 7
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 3 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 3 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 hơn 2 năm trước
0 0
{{like_count}}

kipalog

{{ comment_count }}

bình luận

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


White
{{userFollowed ? 'Following' : 'Follow'}}
54 bài viết.
775 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á!