Menu
Coddy logo textTech

Interface Comum

Parte da seção Object Oriented Programming do Journey de Lua da Coddy — lição 40 de 70.

O Duck typing nos mostrou que classes não relacionadas podem trabalhar juntas se compartilharem nomes de métodos. Essa ideia leva a um conceito de design poderoso: a interface comum. Quando você projeta intencionalmente várias classes para terem as mesmas assinaturas de método, você cria componentes intercambiáveis.

Imagine que você está construindo um sistema de log. Você pode querer enviar mensagens para diferentes destinos — um arquivo, a tela ou até mesmo uma rede. Em vez de escrever um código separado para cada um, você projeta todas as suas classes de saída para implementar o mesmo método :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

Agora, qualquer código que precise exibir mensagens pode aceitar qualquer um dos tipos:

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] Iniciando
logMessage(screen, "Starting up")  -- [SCREEN] Iniciando

A função logMessage não sabe ou se importa com qual classe recebe. Ela apenas espera um método :write(). Isso torna seu código flexível — você pode adicionar novos tipos de saída mais tarde sem alterar as funções existentes.

challenge icon

Desafio

Fácil

Vamos construir um sistema de processamento de pagamentos onde diferentes métodos de pagamento podem ser usados de forma intercambiável! Você criará duas classes de pagamento completamente não relacionadas que compartilham uma interface comum—um método :process(amount)—permitindo que qualquer código que manipule pagamentos funcione com qualquer um dos tipos sem saber a diferença.

Você organizará seu código em três arquivos:

  • CreditCard.lua: Uma classe de pagamento com um construtor :new(cardNumber) que armazena o número do cartão. Seu método :process(amount) deve imprimir Charging {amount} to card {cardNumber}.
  • BankTransfer.lua: Uma classe de pagamento completamente separada com um construtor :new(accountId). Seu método :process(amount) deve imprimir Transferring {amount} from account {accountId}.
  • main.lua: Crie uma função chamada handlePayment(paymentMethod, amount) que aceite qualquer objeto de pagamento e um valor, e então chame :process(amount) nele. Esta função não precisa saber se está lidando com um cartão de crédito ou transferência bancária—ela apenas espera que o objeto tenha um método :process().

Você receberá três entradas:

  1. Um número de cartão de crédito (ex: 4532-1234-5678)
  2. Um ID de conta bancária (ex: ACC-9876)
  3. Um valor de pagamento (ex: 150)

No seu arquivo principal, crie uma instância de cada tipo de pagamento usando as duas primeiras entradas. Em seguida, use sua função handlePayment para processar o valor fornecido através de ambos os métodos de pagamento—primeiro o cartão de crédito, depois a transferência bancária.

Por exemplo, se as entradas forem 1111-2222-3333, ACC-5555 e 75, a saída deve ser:

Charging 75 to card 1111-2222-3333
Transferring 75 from account ACC-5555

A beleza deste design é que sua função handlePayment funciona com qualquer objeto que implemente :process(). Você poderia adicionar uma classe PayPal amanhã e, desde que ela tenha um método :process(amount), ela funcionaria com sua função existente sem nenhuma alteração!

Folha de consulta

Uma interface comum é um padrão de design onde múltiplas classes não relacionadas implementam intencionalmente as mesmas assinaturas de método, tornando-as intercambiáveis.

Ao projetar classes para compartilhar nomes de métodos, você pode escrever funções que funcionam com qualquer objeto que implemente essa 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

Funções podem aceitar qualquer objeto que implemente o método esperado:

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

A função logMessage não precisa saber qual classe ela recebe — ela apenas espera um método :write(). Isso torna o código flexível e permite adicionar novos tipos sem modificar as funções existentes.

Experimente você mesmo

-- Importar as classes de pagamento
local CreditCard = require('CreditCard')
local BankTransfer = require('BankTransfer')

-- Ler as entradas
local cardNumber = io.read()
local accountId = io.read()
local amount = tonumber(io.read())

-- TODO: Criar a função handlePayment que aceita qualquer objeto de pagamento
-- e um valor, e então chama :process(amount) nele


-- TODO: Criar instâncias de CreditCard e BankTransfer usando as entradas


-- TODO: Usar handlePayment para processar o valor através de ambos os métodos de pagamento
-- Primeiro o cartão de crédito, depois a transferência bancária
quiz iconTeste seus conhecimentos

Esta lição inclui um quiz rápido. Comece a lição para respondê-lo e acompanhar seu progresso.

Todas as lições de Object Oriented Programming