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
acc_1:deposit(100.0)is syntax sugar foracc_1.deposit(acc_1, 100.0). The immediate issue you have is thatacc_1.depositisnil. First off,acc_1is empty. This is sort of by design -- it is aproxyreturned byAccountProxy:new. Because the key doesn't exist, it will check the metatable. The__indexmethod looks in the correspondingaccounttable, but this only hasbalance. There is nothing connectingacc_1toAccountProxy.deposit. You could callAccountProxy.deposit(acc_1, 100.0), but this is not really a traditional object-oriented pattern.You want
acc_1.depositto be associated with the actual method, so you must either putdepositdirectly inacc_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 innewfrom your instance methods. E.g.or
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
balancefromproxy; and you have already partially implemented this as suggested by the exercise. Givenproxy, you can lookupaccounts[proxy]e.g.Assuming outside code doesn't have access to
accounts(which can be enforced by lexical scoping), it then can't directly interact withbalanceexcept through the provided methods.(Using
accountsis 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:newdoesn't useself; things that are conceptually not instance methods may be preferable to just use.instead of:.nil. I don't necessarily have a problem with this, but I would rather not do it if I know the balance is nevernil(which I think is true here).