1. Abstraction and Encapsulation¶
Learning Objectives
- know the definitions of abstraction and encapsulation
- understand the concept of information hiding to enforce the abstraction barrier
- understand the drawback of breaking the abstraction barrier
- be familiar with using ModuleScripts for abstraction and encapsulation
Abstraction¶
Abstraction is the idea that we can separate the concerns between the implementer and user (or client) of the code by hiding implementation details from the users.
Take for instance the abstraction being played by your browser: a user can just throw in a URL in the searchbar and go along their merry way to their favourite website without having to worry about how it's actually achieved (i.e. HTTP requests, DNS requests, networking protocols, etc).
Similarly, when we work with code in large projects, it's generally a good idea to break code into different "modules" if you will, where the client calling the code does not need to know how the module works, just how to use it. We already do this all the time when we call functions like print()
. Most of us know nothing about how these functions work but - here's the important thing - we don't need to; we just need to know how to call the code that we trust.
Information Hiding¶
Information hiding is the principle that we must restrict access to a "module"'s internal implementation. In other words, there must be an abstraction barrier between the implementation and the client.
We call the invisible barrier between the client and implementer the abstraction barrier: something that helps us separate our concerns. The implementer can write their module code without having to worry about the client randomly calling a function they aren't supposed to and breaking everything. Isn't that a great thing not to have to worry about?
On the other hand, if we break the abstraction barrier then we might inadvertently allow the client to mess with code in unintended ways. Implementing this abstraction barrier properly is similar to minimizing the attack surface of our code; we can reduce logic errors by only letting our code being accessed in a certain way.
Encapsulation¶
Encapsulation is a different but related idea which says that we should bundle together data with the associated functions (also called methods) which act on it on the same side of the abstraction barrier.
ModuleScripts¶
ModuleScripts in Roblox are a type of script used to organize and reuse code. They are excellent tools for implementing abstraction and encapsulation in your projects. Unlike regular scripts, ModuleScripts return a table that acts as their public interface, which allows you to define functions, variables, or constants that can be accessed from other scripts while keeping internal logic hidden. After writing a ModuleScript, we can use the require()
function to load it from an external script, i.e. the client.
Local Scope and Public Interface¶
Within a ModuleScript, you can define local variables and functions that are only accessible within the script itself. These act as private members, supporting the principle of information hiding. The table returned by the ModuleScript defines its public interface, listing the functions or variables that other scripts can access.
For example:
-- UtilityModule.lua
local UtilityModule = {}
-- Private function (local)
local function sayHello(name)
print("Hello, " .. name .. "!")
end
-- Public function (attached to table)
function UtilityModule.sayHelloTimes10(name)
for i = 1, 10 do
sayHello(name)
end
end
return UtilityModule
Here, sayHelloTimes10
is a public function which can be used by the requiring script but sayHello
is private because it is local to the ModuleScript. See how through this abstraction barrier, we force the client, the requiring script, to only be able to print out the string in multiples of 10, because they have no way of accessing the private local functions.
Singletons¶
When we introduce state into the equation through private local variables, we create something called a Singleton,which has the same shared state across all requires. That is, it is a kind of global abstraction over state.
For instance:
-- GameManager.lua
local GameManager = {}
-- Private member (local)
local score = 0
-- Public functions (attached to table)
function GameManager.addScore(points)
score += points
end
function GameManager.getScore()
return score
end
return GameManager
Here, addScore
and getScore
are part of the public interface, while score
remains hidden within the module and cannot be directly accessed by classes which require
this module. If a script with this module adds to the score, other scripts which also have this module will see the change in the score. This is a very useful behaviour in certain cases in game development, such as for global game state managers or when we know there will only ever be a single instance.
So far, we don't have a way to initialise unique instances which each store their own state. In the next unit, we will cover how to use ModuleScripts to implement classes, which are able to represent more powerful types of abstractions.