Menu
Coddy logo textTech

Утиная типизация

Часть раздела Object Oriented Programming путешествия по Lua на Coddy — урок 39 из 70.

Lua не важен класс объекта — ей важно только то, что объект может делать. Эта концепция называется утиной типизацией: «Если это ходит как утка и крякает как утка, то это утка». С точки зрения программирования, если у объекта есть метод, который вы вызываете, он работает.

Это означает, что совершенно не связанные классы могут использоваться вместе, если у них совпадает имя метода:

local Circle = {}
Circle.__index = Circle
function Circle:new() 
    local obj = {}
    setmetatable(obj, Circle)
    return obj
end
function Circle:draw()
    print("Drawing a circle")
end

local Square = {}
Square.__index = Square
function Square:new()
    local obj = {}
    setmetatable(obj, Square)
    return obj
end
function Square:draw()
    print("Drawing a square")
end

Эти классы не имеют отношений наследования — они полностью независимы. Тем не менее, мы можем обращаться с ними одинаково:

local shapes = {Circle:new(), Square:new(), Circle:new()}

for _, shape in ipairs(shapes) do
    shape:draw()
end
-- Вывод:
-- Рисование круга
-- Рисование квадрата
-- Рисование круга

Цикл не проверяет, к какому типу относится каждый объект. Он просто вызывает :draw() и полагается на то, что этот метод существует. Эта гибкость очень эффективна — вы можете добавлять новые типы фигур, вообще не изменяя код цикла. До тех пор, пока новый класс реализует :draw(), всё будет работать.

challenge icon

Задание

Легко

Давайте продемонстрируем мощь утиной типизации, создав систему уведомлений с совершенно не связанными друг с другом классами, которые можно обрабатывать одинаково!

Вы создадите три независимых класса уведомлений — ни один из них не наследуется от другого — но все они реализуют метод :send(). Это означает, что вы можете перебирать смешанную коллекцию и вызывать :send() для каждого объекта, не заботясь о его фактическом типе.

Вы организуете свой код в четырех файлах:

  • Email.lua: Класс с конструктором :new(recipient), который сохраняет адрес получателя письма. Его метод :send() должен выводить Emailing {recipient}.
  • SMS.lua: Совершенно отдельный класс с конструктором :new(phoneNumber). Его метод :send() должен выводить Texting {phoneNumber}.
  • PushNotification.lua: Еще один независимый класс с конструктором :new(deviceId). Его метод :send() должен выводить Pushing to {deviceId}.
  • main.lua: Соберите все вместе! Прочитайте три входных значения, создайте по одному экземпляру каждого типа уведомлений, поместите их все в одну таблицу, затем переберите таблицу и вызовите :send() для каждого объекта.

Вы получите три входных значения:

  1. Получатель электронной почты (например, alice@example.com)
  2. Номер телефона (например, 555-1234)
  3. ID устройства (например, device_99)

В вашем основном файле создайте все три объекта уведомлений, сохраните их в таблицу и итерируйте по таблице, вызывая :send() для каждого. Порядок должен быть следующим: сначала Email, затем SMS, затем PushNotification.

Например, если на вход подаются bob@mail.com, 555-9876 и phone_42, вывод должен быть следующим:

Emailing bob@mail.com
Texting 555-9876
Pushing to phone_42

Обратите внимание, что вашему циклу не нужно проверять тип каждого объекта — он просто доверяет тому, что у каждого объекта в коллекции есть метод :send(). Это и есть утиная типизация в действии! Если объект может :send(), значит, это уведомление.

Шпаргалка

Lua использует утиную типизацию: если у объекта есть метод, который вы вызываете, он работает, независимо от его класса. Принцип таков: «если это ходит как утка и крякает как утка, то это утка».

Совершенно не связанные между собой классы могут использоваться взаимозаменяемо, если у них совпадает имя метода:

local Circle = {}
Circle.__index = Circle
function Circle:new() 
    local obj = {}
    setmetatable(obj, Circle)
    return obj
end
function Circle:draw()
    print("Drawing a circle")
end

local Square = {}
Square.__index = Square
function Square:new()
    local obj = {}
    setmetatable(obj, Square)
    return obj
end
function Square:draw()
    print("Drawing a square")
end

Эти классы не имеют отношений наследования, однако с ними можно обращаться одинаково:

local shapes = {Circle:new(), Square:new(), Circle:new()}

for _, shape in ipairs(shapes) do
    shape:draw()
end
-- Output:
-- Drawing a circle
-- Drawing a square
-- Drawing a circle

Цикл не проверяет типы объектов — он просто вызывает метод и доверяет тому, что он существует. Вы можете добавлять новые типы без изменения существующего кода, если они реализуют необходимый метод.

Попробуйте сами

-- main.lua
-- Объедините все типы уведомлений и продемонстрируйте утиную типизацию

-- Импорт классов уведомлений
local Email = require('Email')
local SMS = require('SMS')
local PushNotification = require('PushNotification')

-- Чтение входных данных
local recipient = io.read()
local phoneNumber = io.read()
local deviceId = io.read()

-- TODO: Создайте по одному экземпляру каждого типа уведомления

-- TODO: Поместите все уведомления в одну таблицу (сначала Email, затем SMS, затем PushNotification)

-- TODO: Пройдите циклом по таблице и вызовите :send() для каждого уведомления
-- Это демонстрирует утиную типизацию — мы не проверяем тип, мы просто вызываем :send()
quiz iconПроверьте себя

В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.

Все уроки раздела Object Oriented Programming