Общий интерфейс
Часть раздела Object Oriented Programming путешествия по Lua на Coddy — урок 40 из 70.
Утиная типизация показала нам, что несвязанные классы могут работать вместе, если у них совпадают имена методов. Эта идея ведет к мощной концепции проектирования: общему интерфейсу. Когда вы намеренно проектируете несколько классов так, чтобы они имели одинаковые сигнатуры методов, вы создаете взаимозаменяемые компоненты.
Представьте, что вы создаете систему логирования. Возможно, вы захотите отправлять сообщения в разные места назначения — в файл, на экран или даже в сеть. Вместо того чтобы писать отдельный код для каждого случая, вы проектируете все свои классы вывода так, чтобы они реализовывали один и тот же метод :write():
local LogFile = {}
LogFile.__index = LogFile
function LogFile:new(filename)
local obj = {filename = filename}
setmetatable(obj, LogFile)
return obj
end
function LogFile:write(message)
print("[FILE:" .. self.filename .. "] " .. message)
end
local Screen = {}
Screen.__index = Screen
function Screen:new()
local obj = {}
setmetatable(obj, Screen)
return obj
end
function Screen:write(message)
print("[SCREEN] " .. message)
endТеперь любой код, которому требуется выводить сообщения, может принимать любой из этих типов:
local function logMessage(output, msg)
output:write(msg)
end
local file = LogFile:new("app.log")
local screen = Screen:new()
logMessage(file, "Starting up") -- [FILE:app.log] Запуск
logMessage(screen, "Starting up") -- [SCREEN] ЗапускФункция logMessage не знает и не заботится о том, какой класс она получает. Она ожидает только наличие метода :write(). Это делает ваш код гибким — вы сможете добавлять новые типы вывода позже, не изменяя существующие функции.
Задание
ЛегкоДавайте создадим систему обработки платежей, в которой различные способы оплаты могут использоваться взаимозаменяемо! Вы создадите два совершенно не связанных между собой класса платежей, которые имеют общий интерфейс — метод :process(amount) — что позволит любому коду, обрабатывающему платежи, работать с любым типом, не замечая разницы.
Вы распределите свой код по трем файлам:
CreditCard.lua: Класс платежа с конструктором:new(cardNumber), который сохраняет номер карты. Его метод:process(amount)должен выводитьCharging {amount} to card {cardNumber}.BankTransfer.lua: Совершенно отдельный класс платежа с конструктором:new(accountId). Его метод:process(amount)должен выводитьTransferring {amount} from account {accountId}.main.lua: Создайте функцию с именемhandlePayment(paymentMethod, amount), которая принимает любой объект платежа и сумму, а затем вызывает у него метод:process(amount). Этой функции не нужно знать, имеет ли она дело с кредитной картой или банковским переводом — она просто ожидает, что у объекта есть метод:process().
Вы получите три входных значения:
- Номер кредитной карты (например,
4532-1234-5678) - ID банковского счета (например,
ACC-9876) - Сумма платежа (например,
150)
В основном файле создайте по одному экземпляру каждого типа платежа, используя первые два входных значения. Затем используйте функцию handlePayment для обработки заданной суммы обоими способами оплаты — сначала кредитной картой, затем банковским переводом.
Например, если входные данные 1111-2222-3333, ACC-5555 и 75, вывод должен быть следующим:
Charging 75 to card 1111-2222-3333
Transferring 75 from account ACC-5555Прелесть такой архитектуры в том, что ваша функция handlePayment работает с любым объектом, реализующим :process(). Вы могли бы добавить класс PayPal завтра, и пока у него есть метод :process(amount), он будет работать с вашей существующей функцией без каких-либо изменений!
Шпаргалка
Общий интерфейс (common interface) — это паттерн проектирования, при котором несколько несвязанных классов намеренно реализуют одни и те же сигнатуры методов, что делает их взаимозаменяемыми.
Проектируя классы с общими именами методов, вы можете писать функции, которые работают с любым объектом, реализующим этот интерфейс:
local LogFile = {}
LogFile.__index = LogFile
function LogFile:new(filename)
local obj = {filename = filename}
setmetatable(obj, LogFile)
return obj
end
function LogFile:write(message)
print("[FILE:" .. self.filename .. "] " .. message)
end
local Screen = {}
Screen.__index = Screen
function Screen:new()
local obj = {}
setmetatable(obj, Screen)
return obj
end
function Screen:write(message)
print("[SCREEN] " .. message)
endФункции могут принимать любой объект, реализующий ожидаемый метод:
local function logMessage(output, msg)
output:write(msg)
end
local file = LogFile:new("app.log")
local screen = Screen:new()
logMessage(file, "Starting up") -- [FILE:app.log] Starting up
logMessage(screen, "Starting up") -- [SCREEN] Starting upФункции logMessage не нужно знать, какой класс она получает — она ожидает только наличие метода :write(). Это делает код гибким и позволяет добавлять новые типы без изменения существующих функций.
Попробуйте сами
-- Импорт классов оплаты
local CreditCard = require('CreditCard')
local BankTransfer = require('BankTransfer')
-- Чтение входных данных
local cardNumber = io.read()
local accountId = io.read()
local amount = tonumber(io.read())
-- TODO: Создайте функцию handlePayment, которая принимает любой объект оплаты
-- и сумму, а затем вызывает :process(amount) для него
-- TODO: Создайте экземпляры CreditCard и BankTransfer, используя входные данные
-- TODO: Используйте handlePayment для обработки суммы обоими способами оплаты
-- Сначала кредитной картой, затем банковским переводом
В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.
Все уроки раздела Object Oriented Programming
1Концепция 'self'
Таблицы с функциямиЯвный 'self'Синтаксис с двоеточиемТочка против двоеточияИтоги — Движущаяся точка4Проект: Цифровой банк
Настройка проектаМетод пополнения7Полиморфизм и переопределение
Переопределение методовВызов родительских методовУтиная типизацияОбщий интерфейсПроверка типаИтоги — Роли сотрудников2Паттерн прототипа класса
Концепция прототипаСвязывание через __indexКонструктор :new()Инициализация атрибутовНезависимые экземплярыИтоги — Автозавод5Перегрузка операторов в ООП
Сложение объектовВычитание объектовКонкатенация объектовСравнение объектов (<, >)Повторение — Математика кошелька8Инкапсуляция
Соглашения об именованииЗамыкания для приватностиДоступ через замыканияТаблицы только для чтенияЛогика валидацииИтоги — Защищенное хранилище11Паттерны проектирования (Lite)
Фабричные функцииТаблица SingletonПаттерн IteratorObserver (Слушатель)Итоги — Logger Factory3Состояние и поведение объектов
Переменные экземпляраГеттерыСеттерыВычисляемые свойстваФорматирование строкПроверка на равенствоПовторение — Оценка студента6Основы наследования
Настройка наследованияНаследование методовРасширение конструктораДобавление дочерних методовОбщее и уникальноеИтоги: Иерархия фигур