Writing Modular Code - A Noob's Guide

Writing Modular Code - A Noob’s Guide

Hello developers! It’s me, the noob, again!

Today, I will be talking more about modular coding, why it’s important, and explaining some scenarios.
I hope you will enjoy what I’ve been writing.

What is modular coding, and why is it important?

Modular coding is all about splitting your logic into several, independent and smaller functions, that are shared with all other scripts. More often than not, they are intended to be independent and are meant to do only one thing, and nothing else. While this might seem weird to you if you haven’t dived into modular coding before, this is a recommended practice that will significantly help you in the future. By writing modular code, you will accomplish / experience the following:

  • It’s a lot easier to alter your code / refactor it
  • When you make changes, you only have to change one script
  • Your scripts are no longer a mess (with 1000+ lines)
  • It’s easier to test your code (if you do that)
  • It’s easier to understand the code

To understand how modular code is better, I will now be providing you with some scenarios.

Example 1 - Duplicating scripts/functions

If you are a coder, you’d be most likely lying if you said you have never done this before. At the time we’re doing it, we’re probably thinking “hey, as long as it works”, which is a horrible mindset. Let’s check out this simple code snippet below to calculate something, and then get the result of it. Note that this is only used for demonstration purposes.

function calculateAverage (...)
    local sum = 0;
    local amt = 0;
    
    for i, val in pairs(arg) do
        sum = sum + val
        amt = amt + 1
    end
    
    return (sum / amt)
end

local avg = calculateAverage(1,2,56)
print("Average is:", avg) --> "Average is: 15.5"

Ah, perfect! A simple function that easily calculates the average number from the input given. Perfect! Now, you originally intended to use this in one script on the server, and then you realize later on that you need this function on the client too. What do you do? You copy the code above, and paste it into the client script. Then, even later on, you realize that this is needed in another script. What do you do? You paste it again, in the other script. And so on…

But why is this bad? It works, right?

  • Yes and no. It does work, but it’s now not easily maintained, and you have an “error” in your script. If you ever were to change this (as you should), you’re now stuck at updating the same function 3 times in 3 different scripts. And as you know, human errors can arise and one character wrong and your script don’t work. Now, by editing a function 3 times in different scripts, you have increased your chances of new bugs to arise by a lot.

  • You just added an unnecessary amount of lines to your scripts. Instead of having a module you can require, you have copy-pasted this function in 3 different scripts, adding even more lines. This will eventually make your code unreadable, and a pain to edit.

  • What if you also forget to update the function in one of the scripts on the client? Well, you think you just fixed this so you wouldn’t notice until there is an error because of it, which may not be obvious / shown at first.

can you spot the mistake in the script above?

Example 2 - Attempting to modularize your code, but not enough

You have also probably tried to modularize your code, but did you know that in most cases you’re doing it wrong? I will now be giving you some code below, that you can “analyze”, and you should figure out what’s wrong with it (when talking about modularizing code).

local DataStores = game:GetService("DataStoreService");
local PlayerStores = DataStored:GetDataStore("PlayerStores");

function getPlayerStats (player)
   -- Remember, datastores can fail!
   local data = PlayerStores:GetAsync(player.UserId);
   return data;
end

It’s okay if you don’t see the issue immediately, but I’ll give you a hint: player

When we are writing functions first, we usually don’t think about making it modular, or we haven’t planned out enough of the technical details of what our script is doing, etc. The problem in the script above is that you’re providing player as the parameter, instead of the player’s user id.

Why is that “bad”?

  • What if you don’t currently have the player instance, but their id. Then you have to get the player using the Players:GetPlayerByUserId(id), and it’s just adding a lot of unnecessary code

  • If you were to test your code, you’re going to have to start a game instance (or play in studio), instead of just calling the function from i.e. the command bar.

What you should do instead, is:

-- ...
function getPlayerStats (userId)
   local data = PlayerStores:GetAsync(userId);
   return data;
end

Because now, if you want to call your function in the future, but only have their id, you’re not out of luck!

Example 3 - Repeating logic

This is a common mistake, and it’s a horrible one. The point of creating modules is to make them (often) independent, and callable/obtainable by any script using it. If you start making your modules “prepared” for only one script / one type of script some of the points of making it modular are gone.

Let’s assume the code below:

function HTTP (url)
    HttpService:RequestAsync({
        Url = url;
        Method = "GET";
    });
end

The problem here is that it’s only working for one kind of an HTTP request (the GET request). So, what happens if you want to make a POST request? Maybe we can make a new function!

function HTTPPost (url, body)
    HttpService:RequestAsync({
        Url = url;
        Method = "POST";
        Body = body;
    });
end

Problem solved! Right!?

  • No. Unless you have to, you don’t make new functions because of one parameter. It makes it less maintainable (which is a major convenience of writing code this way).

  • What happens when you want to add custom headers?

  • What if you have a function that processes the responses, and that you need to call each time you do an HTTP request? You’re going to have to update the code in all the HTTP functions you just made. And if you remember from example 1, this is not a good practice.

Example 4 - Depending on each other

This is also very common to do, but, before you read, know that this section doesn’t necessarily apply at all times. This is merely an example. I have several times before that most modules are intended to be independent.

Let’s assume we have two modules, Module1 and Module2. They are both modular to a certain extent, but not fully. Let’s see what their contents are!

Module1:

-- // Module 1 Contents

local afterCalculating = require(Module2);

-- Sum two numbers
return function (num1, num2)
   local sum = num1 + num2;
   return afterCalculating(sum, num1, num2);
end

Module2:

-- // Module 2 Contents

-- Do something after the calculation
return function (sum, num1, num2)
    print("Sum is:", sum);
    
    -- Tell some other scripts
    tellScript();
    updateGame()
    
    return true;
end

Ah, amazing! Now I don’t need to have so much code in my “main scripts”!
I can now just do

local summer = require(Module1),

-- Less code is better!1!1!
summer(2,3);

Now, to make it clear. Less code does not necessarily mean better code. It just means less code. And, in this example, you can see how “well” it turned out. (Sarcasm intended)

But, what if I have another script that wants to call this particular sum function? What is the script going to do? Now I’m left with either making a new module with the same sum function, or I can just copy-paste the same code from Module1 into the other script. Problem not solved. From example 3 we know that this is also bad practice.

The applicable solution to this problem is to make Module1 return the sum, and then have the “main script” call the other functions, rather than making Modul2 do it.

BUT

There are also scenarios where modules should depend on each other. What if you have a module that for example wants to sum all the numbers and calculate the average?

In the module, we can do the following:

-----
-- // Module

local functions = {};

functions.sum = function (...)
    local sum = 0;
    
    for i, val in ipairs(arg) do
        sum = sum + val;
    end
    
    return sum, #arg;
end

functions.average = function (numbers)
   local sum, nums = functions.sum(unpack(numbers));
   
   return (sum / nums);
end

-----
-- // Script 

local functions = require(functions);
local sumNum = functions.sum(1,5);
local avgNum = functions.average({
    1,2,3,4
});

print(avgNum) --> 2.5
print(sumNum); --> 6

For this example, having the functions.average function to call the functions.sum function is legitimate. This is because functions.sum isn’t specifically designed only to be called by functions.average, but any other script can also call functions.sum and it will give them the correct result.

Conclusion

Well, that’s it for now. I hope you enjoyed another one of my posts, and I hope you learned something new, and/or enjoyed the content! Please let me know if you have any questions, suggestions or if there’s anything else you feel like I should add.

80 Likes

Wowza, pretty cool! I learned a bunch of modular related infomation, thanks for these noob (not in a rude manner) resources!

Maybe you could make a book with your knowledge? A noobs studio guide?!

10 Likes

Great to see this! I have avoided modules but will be needing to implement them to some of my new projects!

2 Likes

This is awesome! Also the mistake in the first script is that you need to put in the tuple (…) in place of args or vice versa and second code you made a typo :wink: I hav experimented with modules and I had thought that I only needed modules to do ‘BIG’ chunks of code and not little stuff like calculations but it seems I am contradicted. Thanks :smiley:

1 Like

Happy to see you liked my post!

Even though it might seem like you should have ..., it’s actually not! Instead, you’re using the arg keyword to access the parameters provided. The issue was that I was using pairs instead of ipairs.

Also, note that these were simply examples of what you need to do. The entire point of modules are to split your code into several, independent parts so it’s easier to maintain. Some people also tend to put their entire scripts into modules to reduce “mess” that are in their main scripts. This is unfortunately bad practice. It’s best to keep it simple, and make most of it into modular pieces.

1 Like

Thanks for the info, didn’t know just args accounted for the tuple. Also, I have a question, what about Guis, lets say I have a GUI that does something when clicked on. And I have like 20 text buttons for all of 'em, what is supposed to be done here?

Ik it may seem out of topic but it just came to my mind.

Do i add localscripts to all buttons (obviously a bad practice)? I have thought of putting the buttons in a folder and looping through them to set MouseButton1Click Events which sounds like even worse practice. What would you suggest here?

6 Likes

I agree, I’ve always wondered this. It is something I’ve struggled with.

1 Like