Trang chủ Development

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

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


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.
Ảnh 1.

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ặcMultiple 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

Ảnh 8.

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ả hai 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 B và C đề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

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, cần phải implement method greet thông qua một trait khác và kế thừa trait đó.

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()

Self types

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 =... và val goku: SuperSaiyanGod =... nhé!

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.

>> Tham khảo thêm: Thử tìm động lực học Scala