OOP, Organization, and Tips

So I’m starting a new project and I finally got around using OOP and better organization. I was wondering if I’m doing any bad practices or anything wrong. I would also like a few tips.

--main client script
local var = {
	
	dead = true,
	
	char=nil,
	humanoi=nil,
	
	equipped=nil,
	
	tracks={},
	
	weps={},
	keysDown={},
	states={},
	others={}
}

local constants
constants = {
	services = {
		UIS=game:GetService("UserInputService")
	},
	modules = {
		clientModules={}
	},
	funcs = {
		fillModules=function(parent,tbl)
			for i, v in pairs(parent:GetChildren()) do
				if v:IsA("Folder") and not tbl[v.Name] then
					tbl[v.Name]={}
					constants.funcs.fillModules(v,tbl[v.Name])
				end
				if v:IsA("ModuleScript") then
					tbl[v.Name]=require(v)
				end
			end
		end,
		lifeEnd=function()
			var.dead = true
			if var.equipped then
				var.weps[var.equipped]:Unequip(var.tracks[constants.player],constants.modules.clientModules.Gui,constants.Main)
				var.equipped=nil
			end
		end,
		equip=function(equipNum)
			if not var.dead then
				print("Here")
				if var.equipped then
					var.weps[var.equipped]:Unequip(var.tracks[constants.player],constants.modules.clientModules.Gui,constants.Main)
				end
				var.equipped=equipNum
				var.weps[var.equipped]:Equip(constants.modules,constants.Main)
			end
		end,
		leftDown=function()
			if var.weps[var.equipped] then
				if var.weps[var.equipped].weptype == "Firearm" then
					var.weps[var.equipped]:Fire(var.tracks[constants.player],constants.player,constants.modules,constants.Main)
				end
				if var.weps[var.equipped].weptype == "Melee" then
					var.weps[var.equipped]:Attack()
				end
			end
		end,
		shiftDown=function()
			if var.weps[var.equipped] then
				var.weps[var.equipped]:Sprint(var.tracks[constants.player],constants.modules)
			end
		end,
		shiftUp=function()
			if var.weps[var.equipped] then
				var.weps[var.equipped]:UnSprint(var.tracks[constants.player],constants.modules)
			end
		end
	},
	player=game.Players.LocalPlayer,
} 

--get modules
for i, v in pairs(script:GetChildren()) do
	constants.modules.clientModules[v.Name]=require(v)
end

constants.funcs.fillModules(game.ReplicatedStorage.Modules,constants.modules)


--give tracks table
var.tracks[constants.player]={}

--bc table has to be set first
constants.playerGui=constants.player:WaitForChild("PlayerGui")
constants.Main=constants.playerGui:WaitForChild("Main")

--give weps
var.weps[1]=constants.modules.Superclasses.Weapon.Subclasses.Firearm.new()
--var.weps[2]=constants.modules.Weapon.new("Melee")

constants.services.UIS.InputBegan:connect(function(inputObj)
	var.keysDown[inputObj.KeyCode] = true
	--equipping weps
	if var.weps[inputObj.KeyCode.Value-48] then
		if var.equipped ~= (inputObj.KeyCode.Value-48) then
			constants.funcs.equip(inputObj.KeyCode.Value-48)
		else
			var.weps[var.equipped]:Unequip(var.tracks[constants.player],constants.modules.clientModules.Gui,constants.Main)
			var.equipped=nil
		end
	end
	--reload
	if inputObj.KeyCode == Enum.KeyCode.R then
		if var.weps[var.equipped] then
			if var.weps[var.equipped].weptype == "Firearm" then
				var.weps[var.equipped]:Reload(constants.modules.clientModules.Gui,constants.Main)
			end
		end
	end
	--shiftdown
	if inputObj.KeyCode == Enum.KeyCode.LeftShift then
		constants.funcs.shiftDown()
	end
	if inputObj.UserInputType == Enum.UserInputType.MouseButton1 then
		constants.funcs.leftDown()
	end
end)

constants.services.UIS.InputEnded:connect(function(inputObj)
	var.keysDown[inputObj.KeyCode] = nil
	if inputObj.KeyCode == Enum.KeyCode.LeftShift then
		constants.funcs.shiftUp()
	end
end)

local lastRan=tick()
while true do 
	if var.char ~= constants.player.Character then
		var.char=constants.player.Character
		var.humanoid=var.char:WaitForChild("Humanoid")
		var.dead = false
		var.humanoid.Died:connect(function()
			constants.funcs.lifeEnd()
		end)
	end
	wait()
end

Here’s my explorer hiearchy:

2 Likes

Did I hear a request for a code review?!? I love reviewing code. This may have been better posted in the code review section, but it unfortunately isn’t too popular right now. I’ll comment on it and see if it gets moved.

After I got my first job as a software tester at age 15, one of my first purchases was a book on software development best practices (I was an odd one, I’ll admit it…). I’d recommend it in addition to searching on the web because on this topic books aim to be a more comprehensive resource whereas most webpages aim to critique a small area of coding practices.

I’ll first start with some medium-level critiques, continue to the nitpicky personal preference ones, and finally the large picture.

Medium-level

  • The constant’s table, is this passed anywhere? For constants that are only used in one script, it is idiomatic to place them near the top of the file as local variables. If it is used elsewhere, then I’d move it to another module to live alone in a dark corner and rededicate the main file to code. In general, try to avoid nested structure unless absolutely necessary to divide unlike groups of information.
  • A common best practice is to name constants using ALL_UPPERCASE_WITH_UNDERSCORES. This makes the intention of not editing it clear.
  • Functions in tables is generally avoided, especially with this deep of nesting! Most of a programmers time is spent staring at the leftmost 3 inches of their screen. It makes moving around code and exploring sections easy. Also with this deep of nesting, it is likely to go over the recommended maximum 80-to-90-ish character line width, causing developers working with smaller screens, rotated screens (taller than wide for code), or with multiple windows on a screen I’ve posted a picture below of the 80 character width inserted with comments into my current project below. Notice how close the 80 character limit is to the edge of the screen. If you are wondering, this is the Sublime text editor with SublimeLiner-luacheck and SublimeLinter-gcc installed. I recommend Sublime Text 3! (Although Lua is difficult to install on Windows machines…)
  • Looking closer at the constants table, I suspect that specific modules will only use specific sections. I’d recommend breaking up this table in any way you see fit to satisfy the principle of least privilege – code shouldn’t be able to do or to see more than it is supposed to do/see.
  • I like how in the var table you initialize some fields to nil. This makes your code act as documentation, which is the best form of documentation. Going down the rabbit hole, comments may become out of date when the code they comment is updated. Same for other forms of documentation. However, when the code highly readable and properly written it is able to act as its own documentation. Variable names and function names are usually true to their use and changed if not. Functions which have at least and at most a single return statement make their return types more apparent. Back to initializing some fields, this actually causes a slight difference in the way the table is created. Instead of lua_newtable(lua_State *L) being called internally, lua_createtable(lua_State *L, int narr, int nrec) is called internally with the appropriate number of array indices and hash table records preallocated rather than allocated at run time.
  • There is a common, although not required, naming convention for event handler callbacks. They usually start with ‘on’ or ‘On’ followed by the name of the event they are for. This makes mentally associating them to their events possible before you even see the connection made. Again, excellent documentation!
  • I like this comment, --bc table has to be set first, because it draws attention to the fact that the following calls are intentionally asynchronous and should pause execution until they are completed. In larger projects, if functions that are asynchronous are not marked then a accidental call to one during, for example, load time would cause valuable time to be wasted in blocked serialized processing rather than parallel. Translated: angry bosses, slow code, and depression. It is a common practice to label any function with asynchronous code as such by putting Async in their name, like Players:GetCharacterAppearanceAsync().
if inputObj.KeyCode == Enum.KeyCode.R then
    if var.weps[var.equipped] then
        if var.weps[var.equipped].weptype == "Firearm" then
            var.weps[var.equipped]:Reload(constants.modules.clientModules.Gui,constants.Main)
        end
    end
end
  • The above code’s logic can be simplified to this, reducing indentation, using less keywords and other characters not necessary to tell what is going on, and is further from the 80 character limit. Where to place the ‘then’ and placing the 'and’s at the end of the previous line or start of the next line is debated. I note that you have probably unconsciously used the idiom of placing the variable on the left-hand side of the comparison. Nice!
if inputObj.KeyCode == Enum.KeyCode.R
    and var.weps[var.equipped]
    and var.weps[var.equipped].weptype == "Firearm"
then
    var.weps[var.equipped]:Reload(constants.modules.clientModules.Gui,constants.Main)
end
  • In the same code above, var.webs[var.equipped] is fairly long and reused three times. I like making another local variable to temporarily hold this. It is debatable if it is more performant (if it is commonly nil, then it is only accessed once and assigning it to a local variable adds overhead), but it is much easier to read! If the wep isn’t normally nil, then this also saves lua from performing a bunch of indexes and hash table lookups.
local wep = var.weps[var.equipped]
if inputObj.KeyCode == Enum.KeyCode.R
    and wep
    and wep.weptype == "Firearm"
then
   wep:Reload(constants.modules.clientModules.Gui,constants.Main)
end
  • I also prefer in most cases separating the connection from the handler. Some will argue with that, but I like separating the static code (what isn’t run when it is encountered, like function declarations) from the execution code. Here is a before and after:
constants.services.UIS.InputEnded:connect(function(inputObj)
	var.keysDown[inputObj.KeyCode] = nil
	if inputObj.KeyCode == Enum.KeyCode.LeftShift then
		constants.funcs.shiftUp()
	end
end)
-- top of the file
local UserInputService = game:GetService 'UserInputService'

-- with other function declarations
local function onInputEnded(inputObj)
    var.keysDown[inputObj.KeyCode] = nil
    if inputObj.KeyCode == Enum.KeyCode.LeftShift then
        constants.funcs.shiftUp()
    end
end

-- at the bottom with the executable code
UserInputService.InputEnded:Connect(onInputEnded)
  • Also note how I changed the UserInputService to a local variable at the top of the file using the full service name. This allows me to easily identify what it is. Also, services needed are often very different between files, so it is a best practice to include the required services in each. This also prevents other code from editing the service out from under you accidentally. As a general principle, all code (in functions, modules, or down to statements) should be as decoupled as possible. This is to prevent bugs caused by other sections of code. Tightly coupled code is a nightmare to maintain because a change in one area may affect most of the program through cascading effects. This means that all shared “state” (like the constants table and vars table) is generally a bad idea. Shared constants is a little more redeemable, since it shouldn’t be edited in the first place, but I’d still condemn it.

Personal nitpicking

  • I really like using quotes alone instead of parentheses and quotes. It looks a lot cleaner to me. It is more concise and has less symbols clouding the meaning of the function. Granted, to those unfamiliar with it, it can look strange. That is what I did above with the user services local variable. Some will disagree with me (I started a debate on this while I worked at Roblox, but it wasn’t the most popular opinion :smile:)
  • I also like using single quotes instead of double ones. They add less to the first-look complexity of the file. Double quotes require hitting the same key as the single quotes, but with shift.
  • For tables, I end every line with a ;, including the last line. This makes adding other elements easy and shows that it is different from other types of lists (argument lists, variable lists, expression lists). I do still use commas though when making a single line array.

High-level Concepts

OOP

I don't like OOP as taught in classes. Java is an abomination in my sight. I prefer functional programming -- the time-tested means of programming so influential that it has prevented pure OOP in most Java and other OO code. The vary basis of math, combinatorics, and lambda calculus (the fundamental form of computing equivalent to a Turing machine and made by the professor of the creator of the Turing machine) is the pure function. ANYWAYS, if you are going to use OOP, you have to do it right. Don't do as Roblox did with their instances and pay for now with a difficult to change API. Instead favor what is called in the biz, "Composition Over Inheritance".

That means that instead of a structure like this:

Object
    - VisibleMesh
        - MovingMesh
            - VisibleMovingSolidMesh
                - Player
            - Cloud

We have this:

Object
    - MeshBehavior
        - Visible
        - Movable
        - Solid
    - Mesh
    - Player
    - Cloud

Where we can add MeshBehavior objects to the meshes of a player or cloud. This makes adding classes like a Building possible without a tons of refactoring as in the previous example by giving it a mesh with Visible and Solid behaviors.

You should also note that Lua doesn’t have built in objects. It is what is called an impure functional language – it can create closures with upvalues and functions affect more then their arguments. It is flexible enough though that OOP can be implemented in multiple ways. Two common methods are through closures and metatables. The most common is metatables though. I didn’t get to see your class implementations, but here is a good layout to follow:

local ClassName = {
    defaultProperty = false;
}

local privateStaticProperty = true

function ClassName.new()
    return setmetatable({
        property = 'foo';
    }, ClassName)
end

function ClassName:methodName()
    return self.property
end

ClassName.__index = ClassName
return ClassName

So, I’ve gotten to this point and realized that there is other things I need to do (like eat). So if you apply some of the suggestions and ask again, I’ll supply more feedback.

25 Likes

Services are also constants. Would it be weird just capitalizing them too?

local USER_INPUT_SERVICE = game:GetService("UserInputService")
1 Like

Do yourself and future collaborators a favor, teach yourself to write it as

local UserInputService = game:GetService("UserInputService")

General form: local A = game:GetService("A")

Reserve the uppercase-underscore notation for static values that you expect someone might want to tweak or when it leads to better code semantics when they are named instead of anonymous values (i.e. magic values in your code).

6 Likes

Here’s an example of my class implementation btw:

HIERARCHY
image

WEAPON CLASS

local Weapon = {}
Weapon.__index=Weapon

function Weapon:Equip(modules,Main)
	print(self.weptype.." equipped")
	modules.clientModules.Gui:ShowWepInfo(Main)
	modules.clientModules.Gui:UpdateWepInfo(self,Main)
end

function Weapon:Unequip(trackFile,Gui,Main)
	print(self.weptype.. " unequipped!")
	Gui:HideWepInfo(Main)
	--stop tracks
	for i, v in pairs(trackFile) do
		v:Stop()
	end
end

function Weapon:Sprint(trackFile,modules)
	if trackFile.unSprint then
		trackFile.unSprint:Stop()
	end
	trackFile["sprint"]=modules.clientModules.Animate.new("Fire")
	trackFile["sprint"]:Play()
end

function Weapon:UnSprint(trackFile,modules)
	if trackFile.sprint then
		trackFile.sprint:Stop()
	end
	trackFile["unSprint"]=modules.clientModules.Animate.new("Fire")
	trackFile["unSprint"]:Play()
end

function Weapon.new(weptype)
	local wep = {
		weptype=weptype,
	}
	setmetatable(wep,Weapon) --inherit all wep functions
	return wep
end

return Weapon

FIREARM CLASS (subclass of superclass “Weapon”)

local Weapon = require(script.Parent.Parent)

local Firearm = {}
Firearm.__index=Firearm
setmetatable(Firearm,Weapon)

function Firearm:Fire(trackFile,player,modules,Main)
	if self.mag>0 then
		if self.fireType == "Semi" then
			self.mag=self.mag-1
			--print("mag - "..self.mag)
			modules.clientModules.Gui:UpdateWepInfo(self,Main)
			trackFile["fireTrack"]=modules.clientModules.Animate.new("Fire")
			trackFile["fireTrack"]:Play()
		end
	else
		self:Reload(modules.clientModules.Gui,Main)
	end
end

function Firearm:Reload(Gui,Main)
	if self.mag<self.max and self.pool > 0 then
		local needed = self.max-self.mag
		local toAdd
		if self.pool-needed > 0 then
			toAdd=needed
			self.pool=self.pool-needed
		else
			toAdd=self.pool
			self.pool=0
		end
		self.mag=self.mag+toAdd
		--print("pool - "..self.pool)
		--print("mag - "..self.mag)
		Gui:UpdateWepInfo(self,Main)
	end
end

function Firearm.new()
	print("new")
	local firearm = Weapon.new("Firearm")
	
	firearm.pool=100
	firearm.max=10
	firearm.mag=10
	firearm.fireType="Semi"
	
	setmetatable(firearm,Firearm)
	return firearm
end

return Firearm

Anything I should fix or “improper”?

1 Like

Just a couple notes.

  • For the file structure in explorer, I’d make the subclasses direct children of their parent class, without the intermediate Subclasses file. It’ll make requiring them much less of a pain.
  • It is a common convention to start method names with a lowercase character. Classes begin with an Uppercase character. This also helps everyone tell the difference between Roblox userdatas and your classes: Roblox uses Uppercase characters for their method names. Other variables like Gui and Main should always start with lowercase characters.
  • Another debate at the Roblox office was the use of [“property”] and .property. Can you guess which I like? Using .property seems clearer to me because it uses less symbols that add to the first-look complexity, but there is a valid argument to sometimes use [“property”]. The difference is based on if the table is being used as a C hash table or C structure. A structure’s structure is, well, known ahead of time so .property should be used. The circumstances when the table is being used as a dictionary and you can use [“property”] instead of [varPropName] are rare, but exist. I argue that Lua isn’t C, as much as I love C, and that Lua designers intentionally made tables be the only method of defining structure without differentiating between arrays, sparse arrays, hash tables, objects, structures and what not.

A quick look over the rest looks good, but I need to go again and will be back in a bit.

2 Likes

I also have modules under the client main script

image

Animate

local Animate = {}
Animate.__index=Animate

function Animate:Update(dt)
	if self.playing then
		self.t=math.min(self.max,self.t+dt)
		print(self.t)
	end
end

function Animate:Play()
	self.playing=true
end

function Animate:Pause()
	self.playing = false
end

function Animate:Stop()
	self.t=self.max
end

function Animate.new(animName)
	local animTrack = {
		playing=false,
		t=0,
		max=1,
	}
	setmetatable(animTrack,Animate)
	return animTrack
end

return Animate

Gui

local Gui = {}

--static functions
function Gui:ShowWepInfo(Main)
	Main.WepInfo.Visible = true
end

function Gui:HideWepInfo(Main)
	Main.WepInfo.Visible = false
end

function Gui:UpdateWepInfo(wep,Main)
	if wep.weptype == "Firearm" then
		Main.WepInfo.Pool.Text=wep.pool
		Main.WepInfo.Mag.Text=wep.mag
	else
		Main.WepInfo.Pool.Text="-"
		Main.WepInfo.Mag.Text="-"
	end
end

return Gui

Should I change up this organization?

Some more notes about the Weapon and Firearm classes:

  • Whitespace is sacred. There are times when it is important to use, and other when it is inappropriate. Around binary operators (+, ^, %, …), comparisons (<=, ==), assignments (=), lists (item1, item2) it is important to use. firearm.pool=100 should be firearm.pool = 100. These files have mixed use, which is an abomination in the sight of all that is holy! A place that is optional to use is after { and before }. Some think it is easier to read, other are indifferent.
  • In the weapon class I see that the bane of all programmers’ existence was used. It creates two animations and names both “Fire”, which is clearly not what is should be named. In this case I’d remove the option to name the new animation and in Animate always keep the same name. However in the future when you find yourself copying and pasting snippets of code, stop and consider if you can easily or should put the code into a function. Compositional functions are the functional programmer’s bread and butter. In strict OOP, copying and pasting is a code smell as one of the purposes of inheritance and composition is to eliminate copying and pasting and encourage code reuse. When you do resort to copying and pasting, be VERY careful make sure that you change everything you need to. It is easy to simply skip over and not think about the logic going on when you copy and paste. However, unlike information hiding functions, copying and pasting code doesn’t explicitly list its input. You need to mentally re-evaluate all the code each time you copy and paste.
  • For properties inside of classes, don’t prepend the class name. I know it is tempting when weptype turns into type which the IDE then highlights as a special function, but look for other ways to name it. In this case, I believe className would be more appropriate.
  • weptype if it was to be used should also be wepType
  • weptype (now className) should be defined by the child, not passed into the parent
  • To enable full inheritance cleanly, there needs to be a difference between instantiating a class, and the class’s constructor. I see these two are confused in this code (and the example code I gave would do the same, it wasn’t for full inheritance). Instead something like this would be better:
-- Class Weapon
--function Weapon.new()
--...
--end
-- nothing needs to be initialized in a weapon
function Weapon:init()
end

-- Class Firearm
local CYCLE = {
    SEMI = 'Semi';
}

local Firearm = {
    super = Weapon;
}
Firearm.__index = Firearm
setmetatable(Firearm, Firearm.super)

-- ...

function Firearm:init()
    Firearm.super.init(self)
    self.pool = 100
    self.max = 10
    self.mag = 10
    self.cycle = CYCLE.SEMI
end

--function Firearm.new()
--...
--end

-- In a file of shared functions
local function new(class, ...)
    local obj = {}
    class.init(obj, ...)
    return setmetatable(obj, class)
end

This prevents both the super and the sub class setting the object’s metatable.

  • Also note that I changed the fireType to cycle and made a constant table, much like an enumeration. In looking for a better name, wikipedia said this: “Firearms are also categorized by their functioning cycle or “action” which describes its loading, firing, and unloading cycle.” So I took the common word, cycle, to describe that functionality rather than generic fireType.
  • Many functions here take a table as an argument and only use one field from it. This doesn’t obey the principle of least privilege and is really scary because if fields in a table are mutated in a function, unlike other types of values like numbers or immutable strings, the change also can be seen from the parent. Instead, try to directly pass in the least amount of information possible.
  • When arguments like ‘modules’ are needed in many functions, (especially in recursive functions) you should consider making it an upvalue of all of those functions. This often cleans up code significantly. In this case because trackFile actually needs to be the same between all the method calls, I’d actually make it a property assignable by :init(trackFile).
  • I like the size of the class methods. They obey the principle of single purpose and are concise.
		local toAdd
		if self.pool-needed > 0 then
		    toAdd=needed
		    self.pool=self.pool-needed
		else
		    toAdd=self.pool
		    self.pool=0
		end
		self.mag=self.mag+toAdd
  • In the above code, toAdd is only used once and so self.mag = self.mag + [something] can be inlined and self.pool - needed > 0 is mathematically equivilent to self.pool > needed which changes produce this:
		if self.pool > needed then
		    self.mag = self.mag + needed
		    self.pool = self.pool - needed
		else
		    self.mag = self.mag + self.pool
		    self.pool = 0
		end

or since 0 = self.pool - self.pool,

                local delta = math.min(needed, self.pool)
		self.mag = self.mag + delta
		self.pool = self.pool - delta

As for project organization, I recommend putting most client modules in Replicated Storage, but the way you have it is fine. This was a recent topic on the forum: How do you optimize and organize your code?

Hmm. Looking over these posts, I see why good software engineers are paid so much… there really is a lot to it!

3 Likes

Another thing, why is it bad to have a var table?

This is how I deal with the 60 upvalues thing.

What happens if a connected module (my client is broken up into different modules) needs to change a value on the main client code? It would need a table wouldn’t it?

Also why do I need an init method?

Should calling the .new constructor with the necessary parameters to set the variables be enough?

Seems like having an init thingy makes it more complicated.

Using a var table shared in your code is like using the shared or _g table in your code. It tightly binds your functions together and tracing where a value was edited or all the places that can affect a variable can become difficult. In large projects, it becomes impossible. Not using shared state and following the principle of least privilege reduces debugging time and makes creating bugs more difficult.

Separating the initialization of a class from instantiation is required for clean inheritance. For example, when instantiating a child class, both the initialization of the parent and the child must be run, but only one instance is created. I changed the name from new to init to signify the difference.

hm, what do i do if i want to change something from the main code like a variable thats not a table (without a vars table) on a module script connected to that main code??

is it bad practice to have a module script act as an “extension” to your main script??

Information exchange is usually done using arguments and results, function input and output. The main script would create a wholly independent instance of a class, which may in turn create more instances. The main file will then use class methods to manipulate the instance it created, and the instance will return values which are results of that operation which the main script can use with conditional branching to decide what to do next.

Class, as much as possible, should be completely independent and each object should, again for the most part, contain its own state. This help isolate errors and bugs. Most of the effort of good program design is in determining the relationships between information. OOP most of the time can accurately capture the relationships efficiently and create highly compartmentalized programs, but not always. This is one reason why I like Data Oriented Programming rather than Object Oriented Programming. Your project structure will most likely fit into an OOP model, but you may have to do some rejiggering to get it there.

I’d note that Java programs (a language that tried and failed to be fully OOP) do not allow a main method outside of a class. They don’t allow any methods outside of classes. That is a functional language feature, not an OOP one. The more code and data residing in your main function rather than cleanly split out into classes and participating in relationships with other objects, the more you are trying to mash two methods of programming together and difficulty you will have.

ECS (entity-component-system) frameworks are designed to help large projects organize their code and reduce dependencies. When I used my ECS implementation in a project, the main file simply loaded all the components and systems into the framework, then created the first entity, applied the Main assembly to that entity, and finally began the update loop (calling ECS.update() after every wait()). It was a very simple file. All of the program’s logic was contained inside of the framework.

By “extension” I think your implying tightly connected and sharing lots of information. Not all of the time. Sometimes with OOP, creating large classes is the only way handle complex relationships. When this happens, it is desirable to split the class’s implementation into multiple files. Is this optimal? No. Optimally all of your files will by 70 - 80 lines (about two screens worth) of code and contain simple logic. None of the files would have duplicate logic. Information flow through the program would be simple and predictable. But alas, the optimal program is ever elusive to most program designers.

1 Like

One last thing about folder organization. I have a module with methods but they’re all static. There’s no object creation in them. What folder name would these type of modules go under? ‘Services’??

A collection of functions is called a library. Lua naturally has the string, table, math, and other libraries built in. A common custom library name is ‘util’, short for utility, and contains lots of commonly used functions.

On a side note, OOP classes have methods (in Lua, functions called with ‘:’) and methods are functions with a ‘this’ or ‘self’ bound onto it. Pure functional programs don’t have methods and pure OO programs don’t have functions.

So let’s say for my Gui Util, which is organized like this:

image

(tell me if there’s anything wrong with this btw)

Here’s the code:

--constants
local PLAYER = game.Players.LocalPlayer
local PLAYERGUI = PLAYER:WaitForChild("PlayerGui")
local MAIN = PLAYERGUI:WaitForChild("Main")

local Gui = {}

--static functions
function Gui:ShowWepInfo()
	MAIN.WepInfo.Visible = true
end

function Gui:HideWepInfo()
	MAIN.WepInfo.Visible = false
end

function Gui:UpdateWepInfo(wep)
	if wep.type == "Firearm" then
		MAIN.WepInfo.Pool.Text=wep.pool
		MAIN.WepInfo.Mag.Text=wep.mag
	else
		MAIN.WepInfo.Pool.Text="-"
		MAIN.WepInfo.Mag.Text="-"
	end
end

return Gui

If I want to make it so you click Main.Button would I write that function directly into the Gui module code?

OR would I write it in my main code and call a static method on Gui?

For example:

--main code

--constants
local PLAYER = game.Players.LocalPlayer
local PLAYERGUI = PLAYER:WaitForChild("PlayerGui")
local MAIN = PLAYERGUI:WaitForChild("Main")

local MODULES = ReplicatedStorage:WaitForChild("Modules")

local UTIL = MODULES:WaitForChild("Util")
local GUI = require(UTIL:WaitForChild("Gui"))

MAIN.Button.MouseButton1Click:connect(function()
    GUI:SomeMethod()
end)

Which way is better practice and the proper way to do it??

You have an interesting problem on your hands. But first, some feedback.

  • So… I may have been a bit excessive when I said every constant value that shouldn’t change should be in uppercase. I like the way @buildthomas said it:

He said there are two situation in which you should use uppercase variables:

  1. Values someone may want to tweak in the future
  2. To put a label on anonymous magic values

Variables that commonly fit these definitions are usually being used as constants being passed to functions / instances outside your code. Many times these are ‘configurable’ values. Values used as enumerations and UDims are also commonly put at the top of the file. Under this new definition, MAIN and PLAYER_GUI do not meet the criteria for being capitalized. Sorry about that. :stuck_out_tongue:

  • A couple small things. I still see assignments without spaces. Also I corrected this in my one of the copies of your code above hoping you would see it, but :connect is now :Connect at least in part to conform with standard Roblox capitalization standards. The old method is deprecated. I’d consider renaming Gui to be more specific like WeaponGui. If it really is a class dealing with everything to do with the gui, then it should be broken down a bit. I saw that you renamed weptype. :+1:

  • Who owns the button connection code depends on who is interested in the button’s behavior. How much is the GUI class in charge of? If it is all GUI code, then it belongs in the GUI class. But then the question becomes, where to put it in the GUI class? I’d bet this is the actual question that made you post. The problem is that you’ve fallen into a common anti-pattern some programmers think is beneficial (but don’t believe them!) and are having troubles because of it. I see the enticement of making a singleton for a GUI that is only created once per a client, but it really should be a class with only one instance. OOP isn’t about being efficient (again, a reason I detest OOP) and naturally has overhead to obey its design decisions (basic getters and setters, single instance instead of static, and other things. But I digress.

Wikipedia says this about singletons in general

But I say it is even more so in OOP. Static methods and classes are also often considered anti-patterns (see Anti-Patterns and Worst Practices). This includes Utility classes. Consider this article instead:
OOP alternative to Utility Classes They are all features of functional programming and have snuck into OOP languages like Java to taint them.

Now, that being said, you can use static methods and classes. Some think singletons are a good pattern. Where will you put the connection for the button? I’m not sure, as code outside of classes doesn’t belong in a class file but then again main shouldn’t have its nose is every other modules’ business. If you were to make GUI a class instead of a singleton, it would neatly fit into the constructor, and the PlayerGui.Main would be passed in as a constructor argument and become a property. If you choose to make it a static class, I’d remove the ‘:’ because those functions are not using self. They are not methods. (Yes, I said functions do not exist in OOP, and I stand by that). I’d recommend going on a static class refactoring spree with the knowledge in the articles above to make it all OOP.

Or, you know, decide to join me in a campaign against OOP, the disgusting paradigm that for some reason is claimed to be natural to programmers and clean? We’ve clearly seen that functional programming is more natural and ingrained to every programmer that even the designers of THE OOP language let it sneak in! It doesn’t match the data and operations, thrashes CPU caches, destroys locality of reference, and leaves anti-patterns and code smells in any actual project’s wake? Its bad for the programmers and bad for the machines. There is a reason Java VMs are written in C and C++. Its because while they may offer OOP support, the still have some sanity left in them. Yes, it would be a glorious campaign. (I’m sorry, I had to let it out…)

1 Like

Just to make it clear, you did say its good practice to keep things independent of each other? Especially my custom made classes?

Bc I did a lot of revisions to the code since the first post.

1 Like

Yes, in all cases you want code to be compartmentalized, and independent. Classes in OOP and functions are meant to hide details from their creator/callers, not simply contain operations. They simplify their users’ lives. The principle of least privilege isn’t just to prevent nefarious users from taking advantage of information (like the creation of anti-patterns) but also to prevent simple bugs and ease the strain on a developer’s mind. They shouldn’t have to worry about the whole program when working on a little part of it. Most of us arn’t on top of our game 100% of the time.

You know, when I worked at Roblox, I think people dreaded having their code reviewed by me…

Ok, bc I changed my weapon class from this:

OLD

--services
local ReplicatedStorage = game:GetService("ReplicatedStorage")

--constants
local PLAYER = game.Players.LocalPlayer
local PLAYERGUI = PLAYER:WaitForChild("PlayerGui")
local MAIN = PLAYERGUI:WaitForChild("Main")
local MODULES = ReplicatedStorage:WaitForChild("Modules")

--services
local SERVICES = MODULES:WaitForChild("Services")
local GUI = require(SERVICES:WaitForChild("Gui"))

local Weapon = {}
Weapon.__index=Weapon

function Weapon:Equip()
	print(self.type.." equipped")
	GUI:ShowWepInfo(MAIN)
	GUI:UpdateWepInfo(self,MAIN)
end

function Weapon:Unequip()
	print(self.type.. " unequipped!")
	GUI:HideWepInfo(MAIN)
	--stop tracks
	for i, v in pairs(self.trackFile) do
		v:Stop()
	end
end

function Weapon.new()
	local wep = {equipped=false}
	setmetatable(wep,Weapon) --inherit all wep functions
	return wep
end

return Weapon

to this

NEW

local Weapon = {}
Weapon.__index=Weapon

function Weapon:Equip()
	print(self.type.." equipped")
	self.equipped = true
end

function Weapon:Unequip()
	print(self.type.. " unequipped!")
	self.equipped = false
end

function Weapon.new()
	local wep = {equipped=false}
	setmetatable(wep,Weapon) --inherit all wep functions
	return wep
end

return Weapon

Is this what you mean when you say least privilege and independent? Am I doing it right?