Skip to content

Feature Request: Abstract classes and protected visibility #2

@tullisar

Description

@tullisar

First off, as an embedded C/C++ dev who has only just been getting into Lua for fun, this library has been a blessing! I've mostly been using it for my own library addon to provide enhancements and reusable code for other addons like WeakAuras and such. However, I was drafting a design to take advantage of Interfaces, but also realized that there are a few things I couldn't do with it. I wanted to create an abstract base class that implemented an interface to provide things like common base constructor. Below is an example of what I have now (sorry for the wall of code!)

Current Code
-- Assumes package is already defined.
local IFoo = package:CreateInterface("IFoo", {
  GetText   = {type = "function", returns = "string"},
  GetNumber = {type = "function", returns = "number"}
  Print     = {type = "function" }
}

--[[
Intended to be an interface "default" function. Maybe I did something wrong,
but I was unable to do something like:
function IFoo:Print(data) print(self:GetText() .. self:GetNumber()) end
--]]
function IFoo.Print(obj)
   print(obj:GetText() .. obj:GetNumber())
end

-- "abstract" base class and constructor definition
local CAbstractFoo = package:CreateClass("CAbstractFoo", nil, IFoo)
package:DefineParams("number")
function CAbstractFoo:__Construct(data, number)
  data.number = number
  self:CheckNotAbstract()
end

-- A function used to somewhat enforce the abstract nature of the class.
package:DefineVirtual()
function CAbstractFoo:CheckNotAbstract()
  error("This class is abstract and should not be instantiated!")
end

-- Base implementation to return common state
package:DefineReturns("number")
function CAbstractFoo:GetNumber(data)
  return data.number
end

-- "abstract" method that I would have left out in the base class were it
-- possible
package:DefineVirtual()
package:DefineReturns("string")
function CAbstractFoo:GetText()
  error("This function is abstract, and should not be called!")
end

-- Print implementation using the interface default method.
function CAbstractFoo:Print()
  IFoo.Print(self)
end

-- Concrete class and constructor definition
local CConcreteFoo = package:CreateClass("CConcreteFoo", CAbstractFoo)
package:DefineParams("string", "number")
function CConcreteFoo:__Construct(data, text, number)
  self:Super(number)
  data.number = number
end

-- Implementation to shadow base class to avoid "abstract" error.
function CConcreteFoo:CheckNotAbstract()
  -- Do nothing dummy function
end

-- Implementation to shadow base class to avoid "abstract" error.
package:DefineReturns("string")
function CConcreteFoo:GetText(data)
  return data.text
end

There ends up being a lot of boilerplate code for things that could be implemented as interface default methods. If the library supported it, maybe the code in my example would look something like this?

Code with Suggested Features
-- Assumes package is already defined.
local IFoo = package:CreateInterface("IFoo", {
  GetText   = {type = "function", returns = "string"},
  GetNumber = {type = "function", returns = "number"}
  Print     = {type = "function" }
}

--[[
Something like this could be used to define an interface method that doesn't
know about any object state other than that defined by the interface itself.
--]]
package:DefineDefault()
function IFoo:Print()
   print(self:GetText() .. self:GetNumber())

   --[[
   Behavior like these should result in an error however. When the scope of
   self is the interface, giving access to methods from child classes
   amounts to reflection. Also, allowing direct modification of an object's
   properties that aren't exposed by the interface's definition would be
   asking for coupling issues galore in client code down the road.
   I've seen some gross things done in C++ with stateful pure virtual
   classes and it makes me wince 😅.

   if self:GetName then print(self:GetName()) end
   self.sideEffect = true

   --]]
end

--[[
Something like this could be used to mark a class as abstract, and if that
class implements any interfaces, the framework would not treat missing
interface functions as errors. Called right before the creation of the class,
it would accept a table of abstract function definitions that must also be
implemented by non-abstract children in addition to any unimplemented
interfaces.
--]]
package:DefineAbstract({
  GetName = {returns = "string"}
  DoubleNumberAndAdd = {params = "number", returns ="number" }
}

--[[
Something like this could be used to ensure that only classes in the same
package can declare the protected class as a parent. For example, calling
CreateClass("CConcreteFoo2", CAbstractFoo) from a different package would
result in an error from the framework.
--]]
package:DefinePackageVisibility()

--[[
Create the abstract class, and no abstract instantiation check is required
by client code. Attempting to call CAbstractFoo(0) would result in an error
from the framework.
--]]
local CAbstractFoo = package:CreateClass("CAbstractFoo", nil, IFoo)
package:DefineParams("number")
function CAbstractFoo:__Construct(data, number)
  data.number = number
end

--[[
Something like this could be allowed to provide a default implementation for
DoubleNumberAndAdd, but still require that children implement it. Children may
call it explicitly like any other parent method however.
--]]
function CAbstractFoo:DoubleNumberAndAdd(number)
  return (self:GetNumber() * 2) + number
end

--[[
Something like this would be used to override the default implementation of
the interface, giving more visibility into the object's state. I've used
self.Interfaces as an example of how it might be accessed (similar to calling)
parent methods.
--]]
function CAbstractFoo:Print(data)
  self.Interfaces:Print()
  print(self:GetName())
end

-- Base implementation to return common state
package:DefineReturns("number")
function CAbstractFoo:GetNumber(data)
  return data.number
end

--[[
Something like this could be the mechanism for defining protected functions.
Just like functions defined on the Private table, they would not be visible
outside the scope of the current class, with the exception that child classes
would be able to call them using data:Call(). These methods could also be
allowed to have package:DefinesVirtual() to allow a child to override the
method.
--]]
package:DefineReturns("number")
function CAbstractFoo.Protected:GetHalfNumber()
  return self:GetNumber() / 2
end

-- Concrete class definition
local CConcreteFoo = package:CreateClass("CConcreteFoo", CAbstractFoo)
local fooInstance = CFooConcrete("test", 0)

-- Concrete class constructor
package:DefineParams("string", "number")
function CConcreteFoo:__Construct(data, text, number)
  self:Super(number)
  data.text = text
end

-- GetText required implementation from interface
function CConcreteFoo:GetText(data)
  return data.text
end

-- GetName required implementation from abstract class
function CConcreteFoo:GetName(data)
  return "childUsingProtected:" .. data:Call("GetHalfNumber")
end

I'm still getting the hang of Lua in general, so hopefully things make sense!

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions