ADR-0001 — In-house object-oriented class factory¶
- Status: ✅ Accepted — implemented in
core/class.lua, used by every class in the library. - Scope:
core/class.lua,ritnlib.classFactory
Context¶
RitnLib exposes around thirty classes: runtime wrappers (RitnLibPlayer, RitnLibForce, RitnLibGui…) and data manipulators (RitnPrototype → RitnProto*). Several structural needs:
- Single inheritance internally:
RitnPrototype → RitnProto*,RitnLibPlayer → RitnLibGui → RitnLibInformatron. - Consumer-side specialization: mods extend the base classes (
RitnCorePlayerextendsRitnLibPlayer,RitnGuiMenuButtonextendsRitnLibGui,RitnLeaderboardForceextendsRitnCoreForce…) and fill in the fields the base leaves empty (the extension contract, e.g.self.gui[1]). - Factorio constraints: Lua 5.1, multiplayer determinism, and runtime instances that are temporary wrappers (never stored in
storage— see temporary wrappers).
We therefore needed an object-oriented mechanism: a callable constructor, single inheritance, parent-constructor call, type test — all lightweight and dependency-free.
Decision¶
An in-house factory of about sixty lines, ritnlib.classFactory.newclass(super, init), in core/class.lua:
-- no parent
local A = ritnlib.classFactory.newclass(function(self, arg) self.value = arg end)
-- with parent
local B = ritnlib.classFactory.newclass(A, function(self, arg)
A.init(self, arg) -- explicit parent-constructor call
self.extra = 42
end)
local obj = B(10) -- construction via __call
obj:is_a(A) -- true
Mechanics:
- The class is the metatable of its instances (
c.__index = c) — instances resolve their methods there. - Constructor via
__call:MyClass(args)builds the table, sets its metatable, then runsinit. - Inheritance: shallow-copy of the parent's fields into the child, plus
c._super = super. The parent constructor is called explicitly by the subclass (Parent.init(self, ...)). :is_a(klass)walks the_superchain.- Flexible signature:
newclass(initFn)(no parent) ornewclass(ParentClass, initFn).
Consequences¶
Positive¶
- Zero external dependency, ~60 readable lines, full control, Lua 5.1-compatible.
- A single pattern across the whole library and the consumer mods — proven in production (
RitnCorePlayer,RitnGuiMenuButton,RitnLeaderboardForce,RitnCharacter…). - The explicit parent constructor (
Parent.init(self, …)) keeps specialization flexible and readable.
Negative / pitfalls¶
- ⚠ Shallow-copy of parent fields: a table defined at the parent class level stays shared by reference with the child. Harmless in practice because fields are (re)assigned in
init(instance fields); but a class-level table field would be a shared mutable. - Single inheritance only — no mixins or multiple inheritance.
Parent.initmust be called by hand in each subclass (easy to forget → uninitialized parent fields).- No automatic
super:method(): referenceParent.method(self, …). - A
new()variant (deprecated, unused) remains in the file — to be removed.
Alternatives considered¶
- Lua OO libraries (middleclass, classic…): external dependency and a larger surface than actually needed.
- Ad-hoc metatables per file: duplication and inconsistency.
- No OOP (plain function tables): doesn't cover the inheritance and consumer specialization we need.