Menu
Coddy logo textTech

共通インターフェース

CoddyのLuaジャーニー「Object Oriented Programming」セクションの一部 — レッスン 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) メソッド)を共有する、完全に独立した2つの支払いクラスを作成します。これにより、支払いを処理するコードは、どちらのタイプであるかを意識することなく、どちらのタイプでも動作できるようになります。

コードは以下の3つのファイルに分けて構成します:

  • CreditCard.lua: カード番号を保存する :new(cardNumber) コンストラクタを持つ支払いクラスです。その :process(amount) メソッドは、Charging {amount} to card {cardNumber} と出力する必要があります。
  • BankTransfer.lua: 完全に別の支払いクラスで、:new(accountId) コンストラクタを持ちます。その :process(amount) メソッドは、Transferring {amount} from account {accountId} と出力する必要があります。
  • main.lua: 任意の支払いオブジェクトと金額を受け取り、そのオブジェクトに対して :process(amount) を呼び出す handlePayment(paymentMethod, amount) という関数を作成します。この関数は、扱っているのがクレジットカードなのか銀行振込なのかを知る必要はありません。単にオブジェクトが :process() メソッドを持っていることだけを期待します。

3つの入力を受け取ります:

  1. クレジットカード番号 (例: 4532-1234-5678)
  2. 銀行口座ID (例: ACC-9876)
  3. 支払金額 (例: 150)

メインファイルで、最初の2つの入力を使用して各支払いタイプのインスタンスを1つずつ作成します。次に、handlePayment 関数を使用して、指定された金額を両方の支払い方法で処理します。最初にクレジットカード、次に銀行振込の順で行ってください。

例えば、入力が 1111-2222-3333ACC-555575 の場合、出力は以下のようになります:

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

この設計の素晴らしい点は、handlePayment 関数が :process() を実装している任意のオブジェクトで動作することです。明日 PayPal クラスを追加したとしても、それが :process(amount) メソッドを持っている限り、既存の関数を一切変更することなく動作します!

チートシート

共通インターフェースとは、複数の無関係なクラスが意図的に同じメソッドシグネチャを実装することで、それらを交換可能にするデザインパターンです。

メソッド名を共有するようにクラスを設計することで、そのインターフェースを実装している任意のオブジェクトで動作する関数を記述できます。

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: 任意の支払いオブジェクトと金額を受け取り、
-- そのオブジェクトの :process(amount) を呼び出す handlePayment 関数を作成してください


-- TODO: 入力値を使用して CreditCard と BankTransfer のインスタンスを作成してください


-- TODO: handlePayment を使用して、両方の支払い方法で金額を処理してください
-- 最初にクレジットカード、次に銀行振込の順で行います
quiz icon腕試し

このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。

Object Oriented Programmingのすべてのレッスン