1. Design your plugins/modules around Inputs and Outputs
  2. The functions receiving Inputs (and returning Outputs) are your Interface
  3. Obey the Separation of Concerns
  4. Use Dependency Injection of other modules with well defined Interfaces
  5. Never do more than you have to
  6. Never settle for 1 plugin when 2 will be cleaner

Basics of Module/Plugin Design

A module is a black box that has an input and an output (or many) and it produces a result. The user of the box should not really care how it does what it does. Designing a good module is about being very precise on what inputs you’ll get, and what outputs are required.

A module requires some set of functions for receiving inputs (called the interface) and at least one function for installation or attaching. Think of the attach function as a kind of constructor.

local BlackBox = {}
local function greet(name)
    return "Hello " .. name
end 
BlackBox.Install = function(custom_greet)
    if not custom_greet then
        BlackBox.Greet = greet
    else
        BlackBox.Greet = custom_greet
    end
end
return BlackBox

And here is how you would use the plugin:

local blackBox = require(game.ReplicatedStorage.Modules:WaitForChild("BlackBox"))
blackBox.Install()
-- OR
blackBox.Install(function(name)
    return "Hola " .. name
end)
local greeting = blackBox.Greet(player.Name)

This illustrates the basic design of a black box, or a Module. Notice how we don’t print inside of the module. Never do anything you don’t need to do inside the black box. A Module should exist for outputting messages, like logging, errors, and so on. If a Module needs to print anything, it should always accept a Printer. A Printer or Logger should have a standard interface like Log, Info, Warn, Error, Print etc. We call this Dependency Injection. Whenever your module depends on certain functionality that it really shouldn’t provide, then we need to inject that functionality into the plugin.

Separating Concerns

What should your module know about and do? The answer is: AS LITTLE AS POSSIBLE. Programmers on the go, especially new ones, often make the mistake of combining two or three modules into one big monolith that knows too much and goes too far. One example I’ve seen is an in-game currency manager that knows about and delivers assets to players.

This should really be two plugins, if you think about it, your wallet doesn’t care what you buy, or where you buy it, only that you have enough money or not.

This is a continuation of the idea that Plugins or Modules really shouldn’t be printing things, a Printer or Logger should do that, and the facility should be provided to the Module. A wallet doesn’t decide whether the money you spend is for beer or condoms or pet care magazines.

Here is some untested pseudo-code for a wallet module:

-- WalletModule
local Wallet = {}
local balance = 0
local last_error = nil
Logger = {}
Logger.Log = function (msg) return nil end
local function setBalance(amnt)
    balance = amnt
    return true
end
local function getBalance(amnt)
    return balance
end
function canDebit(amnt)
    if balance >= amnt then
        return true
    else
        return false
    end
end
local function credit(amnt)
    balance = balance + amnt
    return true
end
local function debit(amnt)
    if canDebit(amnt) then
        balance = balance - amnt
        return true
    else
        last_error = "Insufficient Balance"
        return false
    end
end
local function getLastError()
    return last_error
end
Wallet.Install = function(logger,save_func)
    Logger = logger
    Wallet.GetLastError = getLastError
    Wallet.CanDebit = canDebit
    Wallet.Debit = debit
    Wallet.Credit = credit
    Wallet.SetBalance = setBalance
    Wallet.GetBalance = getBalance
    if save_func then
        Wallet.Save = save_func
    else
        Wallet.Save = function()
            Logger.Log("Save function not implemented")
        end
    end
end
return Wallet

-- LoggerModule
local Logger = {}

Logger.Log = function(msg)
    print(msg)
end
return Logger

-- Main Server Script, or a Player section of
-- the script
local wallet = require(...)
local logger = require(...)
wallet.Install(logger,function() 
    -- Do something with DataStore here.
end)

I don’t want to get too caught up in the implementation details, suffice it to say, you only want your Modules to do one or two things, and do them well and bug free.

One thing to remember about Lua is that it is NOT Object Oriented, it is a functional programming language. So you should be thinking in terms of inputs and outputs. Now there is a kind of semi-global state to the Module in the form of balance, and the functions have side effects, but only within the module. Try as best as you can to never let a black box have side effects to the global space. If you must have some kind of side effect, make sure it is explicitly set by a function that obviously has side effects, or is marked as having them. So when tits become upwardly inclined, which they will, you can easily find the site of the side effect and make sure it’s not the cause of your sorrows (it usually will be).

Naming and Interfaces

There are many flame wars about function and variable naming, spacing, and interface design. Screw that. Just do it this way, and move on.

Local Variables/Internal Module State

Use snake_case, but spell the words out fully. No abbreviations like msg, or _t etc.

local shekel_balance = 0
local floren_balance = 0

Local/Internal Functions

Use camelCase as well.

local function canDebit()
    return true
end

Function Arguments

Use snake_case. But never use the whole word, make them short but meaningful. _t is a throwback from C, where custom types were often appended with an _t, like int_t or player_t.

local function canDebit(curr_t,amnt)
    if curr_t == "Shekel" then
        if shekel_balance >= amnt then return true end
    elseif curr_t == "Floren" then
        if floren_balance >= amnt then return true end
    else
        last_error_message = "Unknown Currency " .. curr_t
    end
end

Public Interface Functions

Use a CamelCase, with each word capitalized.

Module.CanDebit = canDebit

Getters and Setters

Make sure that you declare all “private” variables as local, and all private functions as local, assign them later to public facing functions.

Use Getters and Setters to allow outside users to manage or affect data. Don’t be stingy either. C++ programmers can really be a headache because they take the Public/Private thing way too far and hamstring their modules. There should be a valid way to change EVERYTHING inside a module. You have to grant an interface for more experienced programmers, otherwise they’ll just hack your plugin up. What’s the point?

I can’t tell you how many times I’ve just re-written or forked and modified a library because the author got cute with public/private for no good reason.