Mixin và kế thừa
Thuộc phần Lập trình hướng đối tượng trong hành trình Lua của Coddy. Bài học 53 trên 70.
Bạn đã học hai cách để chia sẻ hành vi: kế thừa tạo mối quan hệ “là một” thông qua chuỗi prototype, còn mixin sao chép các hàm trực tiếp vào một lớp. Nhưng khi nào bạn nên dùng từng cách?
Dùng kế thừa (inheritance) khi các đối tượng có chung một bản chất cơ bản. Bird là một Animal. Việc Bird kế thừa các hành vi cốt lõi của động vật như :eat() hoặc :sleep() là hợp lý. Mối quan hệ này mang tính lâu dài và xác định bản chất của đối tượng.
Dùng mixin khi bạn muốn thêm một khả năng không xác định danh tính cốt lõi của đối tượng.
Không phải loài chim nào cũng biết bay: chim cánh cụt và đà điểu là chim, nhưng chúng không bay. Khả năng bay là một năng lực, không phải đặc điểm định nghĩa loài chim. Mixin Flyable cho phép bạn thêm khả năng bay cho những loài chim cần nó mà không bắt buộc tất cả loài chim đều phải có khả năng này.
Hai cách tiếp cận này bổ trợ cho nhau. Ở đây, cả Eagle và Penguin đều kế thừa từ Bird, nhưng chỉ Eagle có mixin:
-- Bird kế thừa Animal (quan hệ là một)
local Bird = {}
Bird.__index = Bird
setmetatable(Bird, {__index = Animal})
-- Eagle và Penguin đều là chim
local Eagle = setmetatable({}, {__index = Bird})
Eagle.__index = Eagle
local Penguin = setmetatable({}, {__index = Bird})
Penguin.__index = Penguin
-- Flyable là một khả năng (mixin)
local Flyable = {}
function Flyable:fly()
print(self.name .. " soars through the sky!")
end
-- Chỉ áp dụng mixin cho những loài chim biết bay
applyMixin(Eagle, Flyable)Đại bàng có thể gọi :fly(). Chim cánh cụt thì không thể: fly không có trong Penguin, Bird hay Animal, nên penguin:fly() sẽ gây lỗi. Nếu áp dụng mixin cho Bird, thì fly sẽ được sao chép vào mọi loài chim, kể cả chim cánh cụt.
Thử thách
DễHãy xây dựng một hệ sinh thái nhỏ gồm các sinh vật để tìm hiểu khi nào nên dùng tính kế thừa và khi nào nên dùng mixin! Bạn sẽ tạo một lớp cơ sở Animal, một lớp Bird kế thừa từ lớp đó và một mixin Swimmable, vì mọi loài chim đều là động vật nhưng không phải loài chim nào cũng biết bơi.
Bạn sẽ sắp xếp code trong bốn tệp:
Animal.lua: Tạo lớp Animal cơ sở với hàm khởi tạo:new(name)để lưu tên. Thêm phương thức:eat()để in ra{name} is eating. Đây là đặc điểm cốt lõi mà mọi loài động vật đều có.Swimmable.lua: Tạo một mixin có phương thức:swim()để in ra{self.name} is swimming. Đây là một khả năng, không phải sinh vật nào cũng biết bơi, vì vậy khả năng này không nên nằm trong chuỗi kế thừa.Bird.lua: Tạo lớp Bird kế thừa từ Animal. Hàm khởi tạo:new(name)cần gọi hàm khởi tạo của lớp cha. Thêm phương thức:chirp()để in ra{name} chirps!. Đây là hành vi riêng của loài chim. Đừng thêm khả năng bơi vào Bird: không phải loài chim nào cũng biết bơi.main.lua: Viết hàm trợ giúpapplyMixin(target, mixin)ở đây: hàm này sao chép mọi hàm trong mixin vào đối tượng đích. Đọc tên của chim và thông tin chim có biết bơi hay không (yeshoặcno). Tạo một đối tượng Bird, và nếu chim biết bơi, áp dụng mixin Swimmable chỉ cho chim đó. Sau đó gọi:eat()(được kế thừa từ Animal) và:chirp()(phương thức riêng của Bird), rồi chỉ gọi:swim()nếu chim này có phương thức đó (if bird.swim then).
Bạn sẽ nhận hai đầu vào:
- Tên của chim (một chuỗi)
- Chim có biết bơi hay không:
yeshoặcno
Nếu chim biết bơi, đầu ra của bạn cần là:
{name} is eating
{name} chirps!
{name} is swimmingNếu chim không biết bơi, đầu ra của bạn cần là:
{name} is eating
{name} chirps!Ví dụ, nếu đầu vào là Duck và yes, đầu ra cần là:
Duck is eating
Duck chirps!
Duck is swimmingNếu đầu vào là Sparrow và no, đầu ra cần là:
Sparrow is eating
Sparrow chirps!Thử thách này cho thấy điểm khác biệt cốt lõi: Bird là một Animal (tính kế thừa), còn bơi là một khả năng tùy chọn (mixin). Vì mixin chỉ được thêm vào những loài chim biết bơi, chim không biết bơi hoàn toàn không có phương thức swim.
Tự làm thử
-- main.lua: Điểm bắt đầu, tạo một con chim và minh họa khả năng của nó
-- Hàm hỗ trợ để áp dụng mixin cho một lớp hoặc một đối tượng riêng lẻ
local function applyMixin(target, mixin)
-- TODO: Sao chép mọi hàm từ mixin vào target
end
local Bird = require('Bird')
local Swimmable = require('Swimmable')
-- Đọc dữ liệu đầu vào
local name = io.read()
local canSwimInput = io.read()
-- TODO: Tạo một instance Bird với tên đã nhập
-- TODO: Nếu đầu vào là "yes", chỉ áp dụng mixin Swimmable cho con chim này
-- TODO: Gọi eat(), phương thức kế thừa từ Animal
-- TODO: Gọi chirp(), phương thức riêng của Bird
-- TODO: Chỉ gọi swim() nếu con chim này có phương thức đó
Bài học này có một bài trắc nghiệm ngắn. Bắt đầu bài học để trả lời và theo dõi tiến độ.
Tất cả bài học trong Lập trình hướng đối tượng
1Khái niệm “self”
Bảng với hàm“self” tường minhCú pháp dấu hai chấmDấu chấm và dấu hai chấmÔn tập: Điểm chuyển động4Dự án: Ngân hàng số
Thiết lập dự ánPhương thức nạp tiền2Mẫu Prototype cho lớp
Khái niệm PrototypeLiên kết bằng __indexHàm khởi tạo :new()Khởi tạo thuộc tínhCác instance độc lậpÔn tập: Nhà máy ô tô5Nạp chồng toán tử trong OOP
Cộng các đối tượngTrừ các đối tượngNối các đối tượngSo sánh các đối tượng (<, >)Ôn tập: phép tính với ví tiền8Đóng gói
Quy ước đặt tênClosure để bảo mậtTruy cập qua closureBảng chỉ đọcLogic kiểm tra hợp lệTổng kết: Két an toàn11Mẫu thiết kế (cơ bản)
Hàm tạo đối tượngBảng SingletonMẫu IteratorObserver (Listener)Ôn tập: Factory Logger3Trạng thái và hành vi của đối tượng
Biến instancePhương thức getterPhương thức setterThuộc tính tính toánĐịnh dạng chuỗiKiểm tra tính bằng nhauÔn tập: điểm số học sinh6Cơ bản về kế thừa
Thiết lập kế thừaKế thừa phương thứcMở rộng hàm khởi tạoThêm phương thức cho lớp conDùng chung và riêngÔn tập: hệ phân cấp hình dạng9Kết hợp và mixin
Quan hệ có-mộtỦy quyềnMixin đơn giảnÁp dụng nhiều mixinMixin và kế thừaÔn tập: Lắp ráp robotTự luyện tập: Trình biên dịch Lua online