Trong bài viết này, chúng ta sẽ xem xét những vấn đề này là gì và cách chúng ta có thể giải quyết chúng bằng bố cục.
vấn đề với các ngôn ngữ hướng đối tượng là chúng có tất cả môi trường ẩn mà chúng mang theo bên mình. Bạn muốn có một quả chuối nhưng thứ bạn nhận được là một con khỉ đột đang ôm quả chuối và toàn bộ khu rừng. - Joe Armstrong, người tạo ra Erlang
Hãy xem xét quá trình tạo hệ thống phân cấp nhân vật trong game nhập vai. Ban đầu, cần có hai loại nhân vật - Chiến binh và Pháp sư, mỗi loại đều có một lượng máu và tên nhất định. Các thuộc tính này là công khai và có thể được chuyển đến lớp Character cha.
class Character { constructor(name) { this.name = name; this.health = 100; } }
Một chiến binh có thể tấn công, tiêu tốn sức lực của mình:
class Warrior extends Character { constructor(name) { super(name); this.stamina = 100; } fight() { console.log(`${this.name} takes a mighty swing!`); this.stamina--; } }
Và một pháp sư có thể sử dụng phép thuật tiêu tốn một lượng mana:
class Mage extends Character { constructor(name) { super(name); this.mana = 100; } cast() { console.log(`${this.name} casts a fireball!`); this.mana--; } }
Bây giờ, hãy giới thiệu một lớp mới, Paladin . Một Paladin có thể vừa chiến đấu vừa sử dụng phép thuật. Làm thế nào chúng ta có thể giải quyết điều này? Dưới đây là một vài giải pháp có chung sự thiếu thanh lịch:
fight()
và cast()
trong đó từ đầu. Trong trường hợp này, nguyên tắc DRY bị vi phạm vì mỗi phương thức sẽ được sao chép khi tạo và sẽ cần đồng bộ hóa liên tục với các phương thức của lớp Mage và Fighter để theo dõi các thay đổi.
fight()
và cast()
có thể được triển khai ở cấp lớp Nhân vật để cả ba loại nhân vật đều có chúng. Đây là một giải pháp tốt hơn một chút, nhưng trong trường hợp này, nhà phát triển phải ghi đè phương thức fight()
cho pháp sư và phương thức cast () cho chiến binh, thay thế chúng bằng các phương thức trống hoặc khắc phục lỗi.Những vấn đề này có thể được giải quyết bằng cách tiếp cận chức năng bằng cách sử dụng thành phần. Nó là đủ để bắt đầu không phải từ các loại của chúng, mà từ các chức năng của chúng. Về cơ bản, chúng ta có hai tính năng chính quyết định khả năng của các nhân vật - khả năng chiến đấu và khả năng sử dụng phép thuật.
Các tính năng này có thể được đặt bằng cách sử dụng các hàm gốc mở rộng trạng thái xác định ký tự:
const canCast = (state) => ({ cast: (spell) => { console.log(`${state.name} casts ${spell}!`); state.mana--; } }) const canFight = (state) => ({ fight: () => { console.log(`${state.name} slashes at the foe!`); state.stamina--; } })
Do đó, một nhân vật được xác định bởi một tập hợp các khả năng và thuộc tính ban đầu này, cả chung (tên và sức khỏe) và riêng (sức chịu đựng và năng lượng):
const fighter = (name) => { let state = { name, health: 100, stamina: 100 } return Object.assign(state, canFight(state)); } const mage = (name) => { let state = { name, health: 100, mana: 100 } return Object.assign(state, canCast(state)); } const paladin = (name) => { let state = { name, health: 100, mana: 100, stamina: 100 } return Object.assign(state, canCast(state), canFight(state)); }
Với thành phần, bạn có thể tránh các sự cố thừa kế bằng cách sử dụng thành phần và javascript là ngôn ngữ hoàn hảo để làm điều đó.