0

Exercise 21.4: A variation of the dual representation is to implement objects using proxies (the section called “Tracking table accesses”). Each object is represented by an empty proxy table. An internal table maps proxies to tables that carry the object state. This internal table is not accessible from the outside, but methods use it to translate their self parameters to the real tables where they operate. Implement the Account example using this approach and discuss its pros and cons.

local AccountProxy = {}

-- Internal table mapping proxies to actual Account tables
local accounts = {}

function AccountProxy:new()
 local proxy = {}
 local account = {balance = 0}
 accounts[proxy] = account

 setmetatable(proxy, {   
    __index = self,
 })

 return proxy
end

function AccountProxy:withdraw(v)
   accounts[self].balance = (accounts[self].balance or 0) - v
end

function AccountProxy:deposit(v)    
    accounts[self].balance = (accounts[self].balance or 0) + v     
end

function AccountProxy:getBalance()
   return accounts[self].balance or 0
end

-- Example usage
local acc_1 = AccountProxy:new() -- acc_1 is the proxy
 
acc_1.deposit(100.0) 
print(acc_1.getBalance())  -- 100.0
acc_1.withdraw(50.0)
print(acc_1.getBalance())  -- 50.0

I get attempt to index a nil value (field '?'). Any suggestion?

I need encapsulation on Account object and balance as Account's property

1 Answer 1

0

acc_1:deposit(100.0) is syntax sugar for acc_1.deposit(acc_1, 100.0). The immediate issue you have is that acc_1.deposit is nil. First off, acc_1 is empty. This is sort of by design -- it is a proxy returned by AccountProxy:new. Because the key doesn't exist, it will check the metatable. The __index method looks in the corresponding account table, but this only has balance. There is nothing connecting acc_1 to AccountProxy.deposit. You could call AccountProxy.deposit(acc_1, 100.0), but this is not really a traditional object-oriented pattern.

You want acc_1.deposit to be associated with the actual method, so you must either put deposit directly in acc_1, or else make it accessible via the metatable. I think these are both fine, but I probably prefer the second given that you don't want to access any local variables defined in new from your instance methods. E.g.

function AccountProxy:new()
  local proxy = {}
  -- ...
  function proxy:deposit()
    -- do the deposit ...
  end
  -- ...
  return proxy
end

or

function AccountProxy:new()
  local proxy = {}
  setmetatable(proxy, {
    __index = AccountProxy,
  })
  -- ...
  return proxy
end

Using the metatable for this purpose could interfere with your existing metatable, but I think your existing metatable is sabotaging your purpose. Supposedly the purpose of the proxy here is encapsulation -- to prevent outside code from accessing balance. The metatable you give just allows any code (inside or outside) to access balance. (Yes, it is the kind of metatable used in the referenced example, but as I see things, it is just an example of forcing access to go through a certain path rather than specifically making that path use metatable.)

Without the metatable, you need another way to retrieve balance from proxy; and you have already partially implemented this as suggested by the exercise. Given proxy, you can lookup accounts[proxy] e.g.

function AccountProxy:getBalance()
   return accounts[self].balance or 0
end

Assuming outside code doesn't have access to accounts (which can be enforced by lexical scoping), it then can't directly interact with balance except through the provided methods.

(Using accounts is not the only way to do such a mapping or get this kind of encapsulation. I would tend to use closure instead, but I'm not sure if that's a pattern discussed elsewhere.)

Additional stylistic notes:

  • AccountProxy:new doesn't use self; things that are conceptually not instance methods may be preferable to just use . instead of :.
  • You methods all handle the case that the balance might be nil. I don't necessarily have a problem with this, but I would rather not do it if I know the balance is never nil (which I think is true here).
Sign up to request clarification or add additional context in comments.

3 Comments

ur article is great but i cannot make it to run wth metatables.. take a look at the new post in my question.
@kos You still need : when calling deposit, withdraw, or getBalance because these methods define and use the self parameter.
sorry my mistake, thnx, everything is fine

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.