Menu
Coddy logo textTech

Interfaz común

Parte de la sección Object Oriented Programming del Journey de Lua de Coddy — lección 40 de 70.

El duck typing nos mostró que las clases no relacionadas pueden trabajar juntas si comparten nombres de métodos. Esta idea conduce a un concepto de diseño potente: la interfaz común. Cuando diseñas intencionalmente múltiples clases para que tengan las mismas firmas de método, creas componentes intercambiables.

Imagina que estás construyendo un sistema de registro. Es posible que desees enviar mensajes a diferentes destinos: un archivo, la pantalla o incluso una red. En lugar de escribir código por separado para cada uno, diseñas todas tus clases de salida para que implementen el mismo 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

Ahora cualquier código que necesite mostrar mensajes puede aceptar cualquier tipo:

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

La función logMessage no sabe ni le importa qué clase recibe. Solo espera un método :write(). Esto hace que tu código sea flexible: puedes añadir nuevos tipos de salida más adelante sin cambiar las funciones existentes.

challenge icon

Desafío

Fácil

¡Vamos a construir un sistema de procesamiento de pagos donde se puedan usar diferentes métodos de pago indistintamente! Crearás dos clases de pago completamente independientes que comparten una interfaz común—un método :process(amount)—permitiendo que cualquier código que maneje pagos funcione con cualquiera de los dos tipos sin notar la diferencia.

Organizarás tu código en tres archivos:

  • CreditCard.lua: Una clase de pago con un constructor :new(cardNumber) que almacena el número de tarjeta. Su método :process(amount) debe imprimir Charging {amount} to card {cardNumber}.
  • BankTransfer.lua: Una clase de pago completamente separada con un constructor :new(accountId). Su método :process(amount) debe imprimir Transferring {amount} from account {accountId}.
  • main.lua: Crea una función llamada handlePayment(paymentMethod, amount) que acepte cualquier objeto de pago y un monto, y luego llame a :process(amount) en él. Esta función no necesita saber si está tratando con una tarjeta de crédito o una transferencia bancaria—solo espera que el objeto tenga un método :process().

Recibirás tres entradas:

  1. Un número de tarjeta de crédito (por ejemplo, 4532-1234-5678)
  2. Un ID de cuenta bancaria (por ejemplo, ACC-9876)
  3. Un monto de pago (por ejemplo, 150)

En tu archivo principal, crea una instancia de cada tipo de pago utilizando las dos primeras entradas. Luego, usa tu función handlePayment para procesar el monto dado a través de ambos métodos de pago—primero la tarjeta de crédito, luego la transferencia bancaria.

Por ejemplo, si las entradas son 1111-2222-3333, ACC-5555, y 75, la salida debería ser:

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

La belleza de este diseño es que tu función handlePayment funciona con cualquier objeto que implemente :process(). ¡Podrías añadir una clase PayPal mañana y, mientras tenga un método :process(amount), funcionaría con tu función existente sin ningún cambio!

Hoja de referencia

Una interfaz común es un patrón de diseño donde múltiples clases no relacionadas implementan intencionalmente las mismas firmas de método, haciéndolas intercambiables.

Al diseñar clases para que compartan nombres de métodos, puedes escribir funciones que funcionen con cualquier objeto que implemente esa interfaz:

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

Las funciones pueden aceptar cualquier objeto que implemente el 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

La función logMessage no necesita saber qué clase recibe; solo espera un método :write(). Esto hace que el código sea flexible y permite añadir nuevos tipos sin modificar las funciones existentes.

Pruébalo tú mismo

-- Importar las clases de pago
local CreditCard = require('CreditCard')
local BankTransfer = require('BankTransfer')

-- Leer las entradas
local cardNumber = io.read()
local accountId = io.read()
local amount = tonumber(io.read())

-- TODO: Crear la función handlePayment que acepte cualquier objeto de pago
-- y un monto, luego llame a :process(amount) en él


-- TODO: Crear instancias de CreditCard y BankTransfer usando las entradas


-- TODO: Usar handlePayment para procesar el monto a través de ambos métodos de pago
-- Primero la tarjeta de crédito, luego la transferencia bancaria
quiz iconPonte a prueba

Esta lección incluye un breve cuestionario. Empieza la lección para responderlo y registrar tu progreso.

Todas las lecciones de Object Oriented Programming