Lua でオブジェクト指向プログラミング - メンバ変数やメソッド名を保護する方法

はじめに

Lua で OOP: self を使わないオブジェクト指向プログラミング 」 では、 メソッドの引数にも、メソッド中にも self と書くことなく、 Lua でも C++ や C# のようなオブジェクト指向プログラミングを可能にする方法を 紹介しました(もちろん、コロンによるシンタックスシュガーも使用しません)。

しかし、メンバ変数やメソッド(=メンバ関数)の保護については言及しておらず、以下のコードのようにメソッドをユーザーが上書きできてしまうという問題点がありました。

> --  メンバ変数の値を上書き
> stack=Stack.new()
> stack.push(10)
> stack.numOfElements
1
> stack.numOfElements=0 -- スタックには何も入って無いことにしてしまう
> stack.pop() -- 10 というデータが積まれているにも関わらずエラーとなる
./noSelf.lua:31: stack is empty.
stack traceback:
	[C]: in function 'error'
	./noSelf.lua:31: in function <./noSelf.lua:29>
	(...tail calls...)
	[C]: in ?
> stack=Stack.new()
> stack.push=function() print("hello, world") end
> stack.push(10) -- push すると hello, world が…(もう、滅茶苦茶)
hellow, world

この文書では、C++ や C# と同様に、

  1. 上書き可能なメンバ変数以外には、値の代入を許さない
  2. また、private なメンバについては、外部からアクセスできない

Lua でのオブジェクト指向プログラミングを紹介します。

動作確認した Lua のバージョン

Lua-5.3.4。メタテーブルの機能を有する最近の Lua であれば動作すると思います。

しくみ

変数の保護については、以前書いた「 Lua で定数もしくは読取専用の変数を実現する方法 」を利用します。各クラス毎に以下のテーブルを用意し、シンボル名を格納しておきます。

  • private なメンバ変数およびメソッド名を格納する、private テーブル
  • 上書き可能な public なメンバ変数およびメソッド名を格納する、writable テーブル

インスタンスのメタテーブルにある、__index および __newindex にて、これらのテーブルを適宜参照しつつ、外部からはアクセスできないもの(private なもの)や、上書き可能なもの(writable なもの)を実現します。

例として取り上げるクラスの仕様

ここでは例題として、前回と同様 Stack クラスを使って説明します。

Stack クラスのインスタンには、以下のメンバを持つものとします。private は、外部からアクセスできないメンバを、writable は、外部からアクセスでき、かつ、値も変更可能なメンバを意味します。特に何も説明がないものは、外部からアクセスできるが、読取り専用なメンバを表すものとします(メソッドなど)。

private なもの:

  • body : スタックの本体。実態はテーブル。ここに push された値が格納されるものとします。

writable なもの:

  • Name : スタックの名前

public であるが、読取り専用なもの

  • NumOfElements : スタックに存在しているデータの数を表す
  • push : スタックに値を積むメソッド
  • pop : スタックトップから値を取り出すメソッド
  • dup : スタックトップを複製するメソッド

具体的なコード

言葉で説明するよりも、実際のコードを見たほうが分かりやすいと思いますので、いつものようにコードを以下に記します。

Stack={}

-- インスタンスメソッド:
-- self が無いなどは、前回の記事と同じ(詳細は前回の記事をみて下さい)
Stack.push=function(inValue)
    table.insert(body,inValue)
    NumOfElements=NumOfElements+1
end
Stack.pop=function()
    if NumOfElements<=0 then
        error("stack is empty.")
    end
    NumOfElements=NumOfElements-1
    return table.remove(body)
end
Stack.dup=function()
    local t=pop()
    push(t)
    push(t)
end

-- バイトコードを取得
Stack.methodBytecode={}
Stack.methodBytecode.push=string.dump(Stack.push)
Stack.methodBytecode.pop =string.dump(Stack.pop)
Stack.methodBytecode.dup =string.dump(Stack.dup)

-- ユーティリティ関数
function buildInstanceMethod(inInstance,inBytecodeOfMethod)
    return load(inBytecodeOfMethod,nil,"b",inInstance)
end

Stack.new=function()
    local newInstance={}

    -- インスタンスのメンバに対するアクセスコントロール情報
    local member={}
    local writable={}
    local private={}

    -- member に、全てのインスタンスのメンバを登録しておく
    -- 後述する、writable や private に登録されているもの以外は
    -- 全て public かつ read only とする。
    member.body={}
    member.NumOfElements=0
    member.Name="(no name)"

    -- でも、writable に登録されているインスタンスのメンバは
    -- 上書き可能とする。
    writable["Name"]=true -- Name メンバ変数は書き込み OK とする
   
    -- 一方、private に登録されているインスタンスのメンバは
    -- そもそも、外部からアクセスできないものとする。
    private["body"]=true -- body は外部からアクセス不可とする

    setmetatable(newInstance,
        {
            __index=function(inTable,inKey)
                if private[inKey] then -- private なものはアクセスさせない
                    error(inKey .. " is a private member.")
                end
                return member[inKey]
            end,

            -- newInstance.member=value などの書込みは禁止
            __newindex=function(inTable,inKey,inValue)
                if writable[inKey] then
                    member[inKey]=inValue
                else -- writable 以外のものは上書きさせない
                    error(inKey .. " is not writable.")
                end
            end
        })

    setmetatable(member,{__index=_ENV})

    local i,methodName
    for methodName,bytecode in pairs(Stack.methodBytecode) do
        member[methodName]=buildInstanceMethod(member,bytecode)
    end

    return newInstance
end

実行例

> dofile("protected_member.lua")
> stack=Stack.new()

> -- そもそも private なメンバにはアクセスすることができない
> stack.body
protected_member.lua:89: body is a private member.
stack traceback:
	[C]: in function 'error'
	protected_member.lua:89: in metamethod '__index'
	stdin:1: in main chunk
	[C]: in ?

> -- メソッドの上書きはできない
> stack.push=function() print("hello, world") end
protected_member.lua:99: push is not writable.
stack traceback:
	[C]: in function 'error'
	protected_member.lua:99: in metamethod '__newindex'
	stdin:1: in main chunk
	[C]: in ?

> stack.push(10)
> -- 守られているメンバ変数は上書きできない
> stack.NumOfElements=0
protected_member.lua:99: NumOfElements is not writable.
stack traceback:
	[C]: in function 'error'
	protected_member.lua:99: in metamethod '__newindex'
	stdin:1: in main chunk
	[C]: in ?

> -- しかし、上書き可能なメンバには値を代入することができる
> stack.Name
(no name)
> stack.Name="TestStack"
> stack.Name
TestStack

まとめ

Lua で読取り専用の変数名を実現する手法を使って、オブジェクトのメンバを適切に保護する仕組みを紹介しました。これで、オブジェクトの内部状態を勝手に変更されることもなくなりました。また機会があれば、今回の方法を応用した文章についても書いてみたく思います。