My take on object orientated programming

When I acknowledged the fact that functions are stored as references to them. I thought why we even do module.newindex if functions are anyway a singleton object. So I did this:

export type Class = {
   new:(self:Class) -> Class;
}

return {
   new = table.clone
}::Class

Inheritence:

local Class:Class = require(Class)

export type Vehicle = {
   Speed:number;
   Steering:number;
   RPM:number;
}&Class

local Vehicle: Vehicle = Class:new()
Vehicle.Speed = 0
Vehicle.Steering = 0
Vehicle.RPM = 0

return Vehicle

I want to ask you what features are missing from this take on OOP that can prevent you from using it

1 Like

Using metatables would give the Vehicle class access to Class methods without explicitly copying them each time which is inefficient. This setup also does not include a clear mechanism to call parent class methods if a subclass method overrides them.

Suggestions:

  • Support polymorphism by allowing subclasses to override methods while still accessing parent class methods.
  • Use metatables for inheritance. This is more efficient and supports polymorphism better.
  • Add Constructor Logic to customize initialization for each subclass while preserving inheritance.
  • Encapsulations With Closures: To simulate private properties, you can use closures, which limit the scope of variables.

Example Code:

-- Base Class definition using metatables
local Class = {}
Class.__index = Class

-- Constructor for Class (Base Class)
function Class:new()
    -- Private variables using closures
    local privateVar = "This is a private value"
    
    -- Public instance creation
    local instance = setmetatable({}, Class)

    -- Encapsulation: Function to get private variable (emulating private variables)
    function instance:getPrivateVar()
        return privateVar
    end

    return instance
end

-- Base class method
function Class:speak()
    print("I am a base class object.")
end

-- Vehicle Class Inherits from Class
local Vehicle = {}
Vehicle.__index = Vehicle
setmetatable(Vehicle, {__index = Class}) -- Set inheritance

-- Constructor for Vehicle, which calls the parent constructor
function Vehicle:new(speed, steering, rpm)
    -- Call the parent (Class) constructor
    local instance = Class.new(self)

    -- Add Vehicle-specific properties
    instance.Speed = speed or 0
    instance.Steering = steering or 0
    instance.RPM = rpm or 0

    return instance
end

-- Method Overriding: Override the 'speak' method
function Vehicle:speak()
    -- Call the base class's method
    Class.speak(self) -- Superclass method call
    print("I am a Vehicle with speed:", self.Speed)
end

-- Additional method for Vehicle
function Vehicle:drive()
    print("Driving at speed", self.Speed)
end

-- Subclass: Car Inherits from Vehicle
local Car = {}
Car.__index = Car
setmetatable(Car, {__index = Vehicle}) -- Set inheritance

-- Constructor for Car
function Car:new(speed, steering, rpm, make, model)
    -- Call the parent (Vehicle) constructor
    local instance = Vehicle.new(self, speed, steering, rpm)

    -- Add Car-specific properties
    instance.Make = make or "Unknown"
    instance.Model = model or "Unknown"

    return instance
end

-- Override 'speak' again in Car
function Car:speak()
    -- Call the parent (Vehicle) speak method
    Vehicle.speak(self)
    print("This car is a " .. self.Make .. " " .. self.Model)
end

-- Create instances and test behavior
local myVehicle = Vehicle:new(60, 5, 3000)
myVehicle:speak()  -- Should call Vehicle's version of speak

local myCar = Car:new(120, 10, 4000, "Toyota", "Corolla")
myCar:speak()  -- Should call Car's version of speak

-- Access private variables in Class
print("Private var in Car:", myCar:getPrivateVar())

As I said:

With which I meant that functions are never copied; they are singletons similar to tables, you can check it by using this code:

module

return function()
print("Check")
end

script

local module = PathToModule
local CheckFunction1 = require(module)
local CheckFunction2 = require(module)
print(CheckFunction1)
print(CheckFunction2)

Code above will output same address 2 times

Overriding methods is also quite easy, as well as private variables in case of you really needing them:

local Class:Class = require(Class)

export type Vehicle = {
   Speed:number;
   Steering:number;
   RPM:number;
}&Class

local Vehicle: Vehicle = Class:new()
Vehicle.Speed = 0
Vehicle.Steering = 0
Vehicle.RPM = 0

function Vehicle:new(CustomParam:number) -- override basic Class:new()
   local newSelf:Vehicle = Class.new(self)
   --Define custom behavior
   local privateVar = CustomParam
   function newSelf:getPrivateVar() -- bad practice since you create a new function for each of the Vehicle Instances
      return privateVar
   end
   return newSelf
end

return Vehicle

Edit: forgot to mention that my take on oop came from simply thinking of how can you implement oop without metatables

That’s fun


I will mention that table.clone can only shallow copy a table. So if you have any tables as properties you’ll see the following:

local BaseClass = {
	new = table.clone,
}

local SomeClass = BaseClass:new()
SomeClass.someTable = {
	Hello = true,
}


local foo = SomeClass:new()
local bar = SomeClass:new() -- the pointer to someTable will be the same

foo.someTable.Hello = false
print(bar.someTable.Hello) -- prints false

I intended it, I think in cases where you need to deep copy properties, you can override the constructor to clone itself fully