Menu
Coddy logo textTech

Common Interface

Part of the Object Oriented Programming section of Coddy's Lua journey — lesson 40 of 70.

Duck typing showed us that unrelated classes can work together if they share method names. This idea leads to a powerful design concept: the common interface. When you intentionally design multiple classes to have the same method signatures, you create interchangeable components.

Imagine you're building a logging system. You might want to send messages to different destinations—a file, the screen, or even a network. Instead of writing separate code for each, you design all your output classes to implement the same :write() method:

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

Now any code that needs to output messages can accept either type:

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

The logMessage function doesn't know or care which class it receives. It only expects a :write() method. This makes your code flexible—you can add new output types later without changing existing functions.

challenge icon

Challenge

Easy

Let's build a payment processing system where different payment methods can be used interchangeably! You'll create two completely unrelated payment classes that share a common interface—a :process(amount) method—allowing any code that handles payments to work with either type without knowing the difference.

You'll organize your code across three files:

  • CreditCard.lua: A payment class with a :new(cardNumber) constructor that stores the card number. Its :process(amount) method should print Charging {amount} to card {cardNumber}.
  • BankTransfer.lua: A completely separate payment class with a :new(accountId) constructor. Its :process(amount) method should print Transferring {amount} from account {accountId}.
  • main.lua: Create a function called handlePayment(paymentMethod, amount) that accepts any payment object and an amount, then calls :process(amount) on it. This function doesn't need to know whether it's dealing with a credit card or bank transfer—it just expects the object to have a :process() method.

You will receive three inputs:

  1. A credit card number (e.g., 4532-1234-5678)
  2. A bank account ID (e.g., ACC-9876)
  3. A payment amount (e.g., 150)

In your main file, create one instance of each payment type using the first two inputs. Then use your handlePayment function to process the given amount through both payment methods—first the credit card, then the bank transfer.

For example, if the inputs are 1111-2222-3333, ACC-5555, and 75, the output should be:

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

The beauty of this design is that your handlePayment function works with any object that implements :process(). You could add a PayPal class tomorrow, and as long as it has a :process(amount) method, it would work with your existing function without any changes!

Cheat sheet

A common interface is a design pattern where multiple unrelated classes intentionally implement the same method signatures, making them interchangeable.

By designing classes to share method names, you can write functions that work with any object implementing that 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

Functions can accept any object that implements the expected method:

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

The logMessage function doesn't need to know which class it receives—it only expects a :write() method. This makes code flexible and allows adding new types without modifying existing functions.

Try it yourself

-- Import the payment classes
local CreditCard = require('CreditCard')
local BankTransfer = require('BankTransfer')

-- Read inputs
local cardNumber = io.read()
local accountId = io.read()
local amount = tonumber(io.read())

-- TODO: Create the handlePayment function that accepts any payment object
-- and an amount, then calls :process(amount) on it


-- TODO: Create instances of CreditCard and BankTransfer using the inputs


-- TODO: Use handlePayment to process the amount through both payment methods
-- First the credit card, then the bank transfer
quiz iconTest yourself

This lesson includes a short quiz. Start the lesson to answer it and track your progress.

All lessons in Object Oriented Programming