Menu
Coddy logo textTech

Общий интерфейс

Часть раздела 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(). Это делает ваш код гибким — вы сможете добавлять новые типы вывода позже, не изменяя существующие функции.

challenge icon

Задание

Легко

Давайте создадим систему обработки платежей, в которой различные способы оплаты могут использоваться взаимозаменяемо! Вы создадите два совершенно не связанных между собой класса платежей, которые имеют общий интерфейс — метод :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().

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

  1. Номер кредитной карты (например, 4532-1234-5678)
  2. ID банковского счета (например, ACC-9876)
  3. Сумма платежа (например, 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 для обработки суммы обоими способами оплаты
-- Сначала кредитной картой, затем банковским переводом
quiz iconПроверьте себя

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

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