I wrote an article a while back defining the four primary pillars in OOP. These pillars are, of course, Encapsulation, Inheritance, Polymorphism, and Abstraction; if more information is needed, please read the article I linked. Also, note that it is critical to know the material in the aforementioned post, as this article requires a firm understanding of OOP.
Now that the prerequisites have been defined, let us get into the content of what will be discussed. In OOP, there is more to understand than just the four pillars. Any software developer would know this and understand that programming is more than just writing code; it is a way of thinking. It is also essential to understand that OOP uses only objects, unlike other forms of development. I established this in the post mentioned initially, but it is critical to know this before continuing. In this article, I will be talking specifically about the theoretical usage of access modifiers, static and non-static typing, and finally, abstraction and overloading.
Before I start, I need to establish some vocabulary used. The word instantiate is to initialize an object from a class using a constructor. Also, note the word constructor; as defined, it creates an object using the data defined in the class.
Let us start with a clear understanding of what access modifiers are. Access modifiers are specific access to a method or function in a given class or object. Most of these modifiers are self-explanatory—for example, private means private access: only accessible in the class which defined the method. The modifier public is defined as access accessible from anywhere: inside or outside the class, which has the method definition. Two access modifiers cannot be strictly used in Lua: default and protected. I will include snippets of Java code to elaborate more on their usage in other languages. The information used in Lua will be about public and private access modifiers; these will be the main point of discussion.
To start I will include some code:
local MyClass = {};
MyClass.__index = MyClass;
function MyClass.new()
return setmetatable({}, MyClass);
end
local NewObject = MyClass.new();
In this code snippet, we can see a constructor to the " MyClass " class. We can also see the object’s instantiation defined to the localized variable of “NewObject”. These get our two vocabulary words out of the way; now, we can move on to the first main idea: Access Modifiers.
I will start with public access in the code below:
local Shape = {};
Shape.__index = Shape;
function Shape.new(Color, Shape)
return setmetatable({
Color = Color or "None";
Shape = Shape or "None";
}, MyClass);
end
local MyTriangle = Shape.new("Yellow", "Triangle");
We can see a class defining a custom shape with two public properties: Color and Shape, included in the code. The custom shape I will be instantiating is a Triangle. This triangle object is set to the localized variable of “MyTriangle.” We define Color and Shape as the two public properties. These properties can be viewed anywhere where the object is referenced. So, if I were to print out our two public properties, I can do that like this:
local MyTriangle = Shape.new("Yellow", "Triangle");
print(MyTriangle.Color, MyTriangle.Shape);
This code will print “Yellow” “Triangle” due to their definition.
Now, let us move on to the idea of public methods. Public methods are defined just like public attributes or properties.
For example:
local Shape = {};
Shape.__index = Shape;
function Shape.new(Color, Shape)
return setmetatable({
Color = Color or "None";
Shape = Shape or "None";
}, MyClass);
end
function Shape:GetColor()
print(self.Color)
end
function Shape:GetShape()
print(self.Shape)
end
local MyTriangle = Shape.new("Yellow", "Triangle");
This code defines two methods: GetColor and GetShape, to print their corresponding values. These methods can be invoked anywhere that we reference the object. That also eliminates the need to manually print out the object’s properties, as we just defined two public methods that do exactly that.
Moreover, public access is defined as anything a reference can access. When we instantiate an object, we create a reference to that variable or table which holds it. The public definition allows us to index the variable and index what we intend to invoke or view.
Now that public access has been defined, I will move on to private access modifiers. Unlike the public, private modifiers are only accessible where they are defined.
In this code, we can see I define a private function only accessible inside of the class.
local Shape = {};
Shape.__index = Shape;
function Shape.new(Color, Shape)
local self = setmetatable({
Color = Color or "None";
Shape = Shape or "None";
}, MyClass);
local function DoSomething(ToPrint)
ToPrint = ToPrint or "We did something."
print(ToPrint);
end
function self:PrintSomething()
DoSomething(self.Color..", "..self.Shape);
end
return self;
end
local MyTriangle = Shape.new("Yellow", "Triangle");
Here we have defined a function that can only be accessed by the object that invokes PrintSomething; DoSomething cannot be called anywhere outside where we define its usage. This idea is, by definition, private access to a method.
We can also define private properties the same way:
local Shape = {};
Shape.__index = Shape;
function Shape.new(Color, Shape)
local PrivateNumber = 1;
local self = setmetatable({
Color = Color or "None";
Shape = Shape or "None";
}, MyClass);
function self:PrintPrivateNumber()
print(PrivateNumber);
return PrivateNumber;
end
return self;
end
local MyTriangle = Shape.new("Yellow", "Triangle");
MyTriangle:PrintPrivateNumber();
Here we define a private number inside of our constructor. The only way to change that number is by a public getter or setter method. Currently, I have a getter method. A setter method would simply be defined in the same place as the getter, but instead of retrieving the value, it will change it.
That can be seen like this:
local Shape = {};
Shape.__index = Shape;
function Shape.new(Color, Shape)
local PrivateNumber = 1;
local self = setmetatable({
Color = Color or "None";
Shape = Shape or "None";
}, MyClass);
function self:GetPrivateNumber()
return PrivateNumber;
end
function self:SetPrivateNumber(Value)
PrivateNumber = Value or PrivateNumber;
end
return self;
end
local MyTriangle = Shape.new("Yellow", "Triangle");
MyTriangle:SetPrivateNumber(5);
print(MyTriangle:GetPrivateNumber());
We change our private property from one to five; then, we print it. Note that this is the conclusion of private and public access modifiers. The code in the next section will be Java; I cannot show examples of protected or default using Lua as it is impossible given the language’s conventions.
class Object{
protected String MyString = "Hello World!";
String MyDefaultString = "Hello World 2!";
private String PrivateString = "Yes I am private";
public String PublicString = "Yes, I am Public";
public String getPrivateString() {
return PrivateString;
}
}
class InheritedObject extends Object{
public InheritedObject(){}
public void PrintAccessData(){
System.out.println(String.format("Protected Var: %s \nDefault Var: %s\nPublic Var: %s\nPrivate Var: %s", this.MyString, this.MyDefaultString, this.PublicString, this.getPrivateString()));
}
}
class Driver{
public static void main(String[] args){
InheritedObject MyNewObject = new InheritedObject();
MyNewObject.PrintAccessData();
}
}
This code is an example of what I just defined in Lua. However, I added protected and the Java default. Protected can be accessed everywhere expect for globally. Java default can be accessed everywhere except for subclasses and globally. The other rules for public and private stay the same; this concludes access modifiers.
Now that we have defined what public and private access modifiers work, we can get into more complex OOP concepts. There are two main ideas I want to talk about that are essential to understanding the true power and usability of OOP. These are the idea of static methods as well as abstraction. I will also mention concepts such as inheritance and function overriding. These concepts will help understand what can be done with OOP.
Let us start with static methods. The idea behind static methods is pretty simple. A static method refers to methods that can be invoked without the instantiation of an object.
An example of a static method is as follows:
local MyClass = {}
MyClass.__index = MyClass;
function MyClass.new()
return setmetatable({}, MyClass);
end
function MyClass.StaticMethod(A, B)
return A+B;
end
Notice how StaticMethod is defined with a period rather than a colon. This notation is necessary because, for a method to be static, we cannot have self as a parameter as the self is used to reference an object that a constructor instantiates. With this logic, the constructor method also can be counted as static but strictly for academic purposes. A constructor will only be invoked once, whereas a static method may be invoked many times.
Note, StaticMethod will add two numbers that the user defines. Why would we make this static instead of usable in the object as a method? We do not need to have an object to add numbers together. We save non-static methods for cases that require complex logic and must be explicitly invoked in the context of the object that has been instantiated. That defines the usage of static methods.
Now we can talk about abstraction. This idea is fundamental in OOP as it defines ideas rather than actual code; abstract methods give the user an interface that can be coded and manipulated to achieve specific functionality.
An example in Lua would be as follows:
local MyClass = {}
MyClass.__index = MyClass;
function MyClass.new()
local self = setmetatable({}, MyClass);
function self:AbstractMethod()
error("Method cannot be left abstract");
end
return self;
end
function MyClass:AbstractMethod()
print("My new abstractmethod");
end
In this code, we see an example of a publicly defined method called AbstractMethod. This method, when called, will cause an error that will break the program. This definition and functionality are because the defined method is intended to be an idea rather than an actual method to invoke. For this abstract public method to become functional, we must define what it will do once invoked. This definition was done prior to the constructor method. I wrote a function to print out something to output once the invoked rather than an error.
This abstraction idea also touches on the concept of method overloading. This concept is defined as a method that overrides another method in a class. Also, note that this concept cannot be done unless a method is inherited from another class. This restriction is because if we were to define a class with a method that we want to overload, and then we make another object with the same method, they don’t overload but rather have separate definitions. I will show inheritance followed by method overloading.
An example of method inheritance is included below:
local SuperClass = {}
SuperClass.__index = SuperClass;
function SuperClass.new()
return setmetatable({}, SuperClass);
end
function SuperClass:Test()
print("Magical function?");
end
local MyClass = {}
function MyClass.new()
local Super = SuperClass.new();
MyClass.__index = function(T, Index)
return MyClass[Index] or Super[Index];
end
local self = setmetatable({}, MyClass);
return self;
end
local MyObject = MyClass.new();
MyObject:Test()
We have a defined superclass with a method called Test. This method is then inherited by a subclass called MyClass. When the method test is invoked, it prints to the console.
Now let’s overload the method Test:
local SuperClass = {}
SuperClass.__index = SuperClass;
function SuperClass.new()
return setmetatable({}, SuperClass);
end
function SuperClass:Test()
print("Magical function?");
end
local MyClass = {}
function MyClass.new()
local Super = SuperClass.new();
MyClass.__index = function(T, Index)
return MyClass[Index] or Super[Index];
end
local self = setmetatable({}, MyClass);
return self;
end
function MyClass:Test(a,b)
return a+b;
end
local MyObject = MyClass.new();
MyObject:Test(1,2)
Now instead of printing, it adds two numbers and returns them. The functionality of the inheritance stayed the same; we just overrode the method in the object which inherits it. This concept differs from abstraction because abstraction is defined as a layout or interface for possible functionality, whereas overriding simply changes existing functionality based on the method’s prior definition.
Here are is an example of the abstraction and method overloading in Java:
interface MyAbstractInterface{
public abstract void TestMethod();
}
abstract class MySuperClass implements MyAbstractInterface{
@Override
public void TestMethod() {
System.out.println("Lets go!");
}
protected void InheritedMethod(){
System.out.println("I print stuff");
}
}
class MyClassInherits extends MySuperClass implements MyAbstractInterface{
public MyClassInherits(){} // constructor
@Override
public void TestMethod() {
System.out.println("Lets go overrode!");
}
@Override
public void InheritedMethod(){
System.out.println("I was overrode");
}
}
class Driver{
public static void main(String[] args){
MyClassInherits MyOb = new MyClassInherits();
MyOb.InheritedMethod();
MyOb.TestMethod();
}
}
That concludes the lecture on more advanced OOP concepts; if you have any questions, please let me know. All the code included is free to take and experiment with. I hope you learned something, and thanks for reading.