Mark As Completed Discussion

In this lesson, we'll be covering Advance Practices in Object-Orientated Programming. After getting through all the fundamentals of OOP, this tutorial is going to be a bit more relaxing. We will discuss some advanced practices with this technique.

Keep in mind that these practices are essential when attempting to understand and use OOP. You can say that today's lesson will be "easy on your brain". Let's start!

Controlling Access to Members of a Class

The first rule of OOP is Abstraction. To make an abstract class, we need a methodology to maintain access to the members of a class. Many OOP-supported languages have special keywords for these methodologies called access level modifiers. Oracle has a very self-explanatory definition for these modifiers:

Access level modifiers determine whether other classes can use a particular field or invoke a particular method. There are two levels of access control:

  • At the top-level—public or package-private (no explicit modifier).
  • At the member level—public, private, protected, or package-private (no explicit modifier).

Controlling Access to Members of a Class

A class is defined by a starting optional modifier. Additionally, each attribute and method within that class will also need its own access modifier.

1class [Name]:
2    [property name]  # No type in Python
3
4    def [method name](self):
5        # Implementation
6        return [Return type R-value]

What are these keywords? Let us go through them one by one.

Public

Public is the most open access modifier. If a variable/attribute has a public modifier, then any other object can easily access it via the . (dot) operator.

Below are major things you need to keep in mind when using the public keyword.

  1. There can only be one public class in a single file in Java.
  2. The file name should be the same as the class name.
  3. It is considered bad practice to make more public attributes and class methods than necessary.

Package-Private

This is the most obscure access modifier and is only applicable to the Java programming language. Java likes to maintain all of its classes inside packages. There is no keyword reserved for this access modifier. Instead, if you do not use any keyword at all, then that class, method, or attribute will be package-private.

Any other class inside a package can access a package-private method or attribute inside the same package, but a class outside that package cannot see the variable without an access modifier.

Protected

This is the most common access modifier of the four. A class can only access a protected class method or attribute if it is a descendant of that class in the inheritance hierarchy. It is similar to private, which we will discuss in the next paragraph. No matter where the descendant class is (perhaps in another package), we can access a class's methods/attributes if we inherit from that class.

If you look at the Shape class hierarchy in the previous lesson, you'll now understand that Square can access all of the properties of the Rectangle and Shape class because it is a descendant of both the Rectangle and Shape class. It cannot, however, access any protected method/attribute of the Circle class because it is not inherited from that class.

Private

Any method, property, or constructor with the private keyword is accessible only from the same class. This is the most restrictive access modifier and is essential to the concept of encapsulation. All data is hidden from the outside world.

You use this access modifier when you don't even want the inherited classes to access this class's property. This modifier can be used to define variables for internal calculations, tracking, and caching that are not needed for any other class to use. For example, we can cache the area of a Rectangle and return it with a getter. No other class needs to know this attribute since they can just use the getter methods.

A general rule of thumb is to keep an attribute as "private" as possible. You'll have fewer weird bugs to deal with later.

Accessing the Object Itself

All OOP-supported programming languages have a special keyword or way to access the "self" object from within a class. For Java and C++, it is this. For Python, it can be named anything but is by convention self.

Suppose you are creating a constructor for the Rectangle class which will take the parameters height and width. You can define the constructor like below:

1class Rectangle(Shape):
2    def __init__(self, h, w):
3        super().__init__()
4        self.height = h
5        self.width = w

But what if you wanted to name the parameters the same thing as the attributes of the class? It would be easier for the user of the class to understand the method signature Rectangle(int height, int width) instead of Rectangle(int h, int w). However, after renaming the parameters, if we use the identifier height or width inside the function, then it will always refer to the parameter (closest scope) and not the attributes. So how do we refer to attributes with the same name?

Here we can use the this keyword to refer to anything of "this" class and not anything outside of the class. See the code below:

1class Rectangle(Shape):
2    def __init__(self, height, width):
3        super().__init__()
4        # self.height => height attribute of Rectangle.
5        # height => height parameter of the constructor method.
6        self.height = height
7        self.width = width

In addition to referencing attributes with the same name as parameters, you also have to access the object itself in order to pass an object to an outside function from within the class. For example, suppose you have a class named GetOne whose job is to return one Rectangle object from two Rectangle objects. Additionally, suppose you have a method in Rectangle that takes a GetOne class and a Rectangle object then updates itself to become a copy of the returned object. Let us first define the GetOne class hierarchy.

1class GetOne:
2    def get(self, rect1, rect2):
3        # Just return the first one for now.
4        return rect1
5
6class GetSmallerArea(GetOne):
7    def get(self, rect1, rect2):
8        return rect1 if rect1.get_area() <= rect2.get_area() else rect2
9
10class GetLargerArea(GetOne):
11    def get(self, rect1, rect2):
12        return rect1 if rect1.get_area() < rect2.get_area() else rect2

Note: In Go, there's no concept of class inheritance like in Java. Instead, Go uses composition and embedding. The given Go code assumes a struct named Rectangle exists with a method getArea.

Now we define the method inside the Rectangle class. The method will take GetOne and another Rectangle object then update itself using get methods. For this, we need to pass two objects to the get method. One object will be the Rectangle and the other will be itself. See below how we can do that:

1class Rectangle(Shape):
2    def __init__(self, height, width):
3        super().__init__()
4        self.height = height
5        self.width = width
6
7    def compare_and_update(self, comparator, other):
8        # Passing myself to comparator with "self" keyword
9        one = comparator.get(self, other)
10        self.height = one.height
11        self.width = one.width
12
13    def __str__(self):
14        return f"Rectangle({self.height},{self.width})"

In the main method, we can use this like below:

1# Assuming necessary class definitions from previous conversions
2def main():
3    # Using concept of polymorphism
4    smaller = GetSmallerArea()
5    larger = GetLargerArea()
6
7    rect1 = Rectangle(3, 4)
8    rect2 = Rectangle(5, 6)
9
10    rect1.compare_and_update(smaller, rect2)
11    print(rect1)  # Rectangle(3,4)
12
13    rect1.compare_and_update(larger, rect2)
14    print(rect1)  # Rectangle(5,6)
15
16main()

Let's test your knowledge. Fill in the missing part by typing it in.

Suppose you have a code snippet like below:

TEXT/X-JAVA
1private class A {
2    private String text;
3}

How do you access the text property from within the class?

Write the missing line below.

Accessing Super Class

To recap what you've seen before, classes can be derived from other classes. With this inheritance-derivation relationship, the whole system can be viewed as a class hierarchy. Inside the hierarchy, there are some definitions to keep in mind which are similar to the tree data structure.

Accessing Super Class

  1. Subclass: A class that is derived from another class is called a subclass.
  2. Superclass: The class from which a subclass is derived is called the superclass.
  3. Siblingclass: If two classes are derived from the same parent, then they are siblingclasses. Keep in mind that this is different from sibling nodes in a tree where all the nodes at the same level are sibling nodes. In the context of OOP, sibling classes must have the same parent.

Inheriting from a class means that you are extending the class's capabilities (or changing them). What if you don't want to totally replace the functionality of a method you are overriding? In that case, you need to somehow call the previous method first, then add other instructions. The initial method before inheritance can be accessed using the super keyword.

For example, when you create a constructor for the Square class, you only need 1 value (the side). Since it is derived from the Rectangle class, you should just reuse the Rectangle constructor. You can do it like so:

Accessing Super Class

TEXT/X-JAVA
1public class Square extends Shape {
2    public Square(int side) {
3        // This calls the super constructor
4        super(side, side);
5        // If you want, you can also do other things here
6    }
7
8    // This method is unnecessary
9    // But for demo we are showing it to you.
10    @Overriding
11    public double getArea() {
12        return super.getArea();
13    }
14}

Build your intuition. Fill in the missing part by typing it in.

You have code like below:

TEXT/X-JAVA
1public class A {
2    public String a() {
3        // impl
4    }
5}
6public class B extends A {
7    public String a() {
8        // impl
9    }
10}
11public class C extends B {
12    public String a() {
13        // impl
14    }
15}

How can you call the method a of class A from class C? Let the instance of C be called c.

Write the missing line below.

Method Chaining

In many libraries, you see a large number of . (dot) notations (like in chains) calling several functions sequentially. This is a programming style named method chaining. In method chaining, most of the methods in a class return the object itself so another function can use the object later on.

Method Chaining

For example, suppose you want to create a group of shapes with a new class ShapeGroup that will contain several shapes inside its internal data structure. We will implement several methods to add different types of shapes to our class. Each of these methods in our ShapeGroup class will return itself in order to use method chaining.

TEXT/X-JAVA
1public class ShapeGroup {
2    private List<Shape> arrayList;
3    private List<Integer> labels; // 1 = Rectangle, 2 = Circle, 3 = Square
4
5    public ShapeGroup() {
6        arrayList = new ArrayList<>();
7        labels = new ArrayList<>();
8    }
9
10    // These methods will return itself, ShapeGroup
11    public ShapeGroup addRect(Rectangle rect) {
12        arrayList.add(rect);
13        labels.add(1);
14        return this;
15    }
16
17    public ShapeGroup addRect(int height, int width) {
18        return this.addRect(new Rectangle(height, width));
19    }
20
21    public ShapeGroup addCircle(Circle circle) {
22        arrayList.add(circle);
23        labels.add(2);
24        return this;
25    }
26
27    public ShapeGroup addCircle(double radius) {
28        return this.addCircle(new Circle(radius));
29    }
30
31    public ShapeGroup addSquare(Square sq) {
32        arrayList.add(sq);
33        labels.add(3);
34        return this;
35    }
36
37    public ShapeGroup addSquare(int side) {
38        return this.addSquare(new Square(side));
39    }
40}

Inside the main method, we can add a bunch of shapes with only 1 line of code.

TEXT/X-JAVA
1public class Main {
2    public static void main(String[] args) {
3        ShapeGroup sg = new ShapeGroup();
4        // Adding 5 shapes
5        sg.addSquare(5).addRect(5,4).addCircle(new Circle(3.4)).addCircle(3.4).addRect(new Rectangle(8,2));
6    }
7}

Overriding vs Overloading

When it comes to inheritance, there are two ways to add methods to your subclasses. Overriding a class means altering a method from its superclass. Overloading a method means creating a separate method with the same name but different parameter types.

Overriding must occur between the superclass and subclass. On the other hand, function overloading can occur in the same class.

When comparing overloading and overriding, the function signature is the primary difference. A function signature contains both the name of the function and the parameters of the function. If the function signature is identical for two classes in an inheritance hierarchy, then you are overriding a method. If they are different (even in the same class), then you are overloading the method.

1class A:
2    def a_method(self, a, b):
3        if isinstance(a, int) and isinstance(b, int):
4            return str(a) + str(b)
5        elif isinstance(a, int) and isinstance(b, str):
6            return str(a) + b
7        elif isinstance(a, str) and isinstance(b, int):
8            return a + str(b)
9
10class B(A):
11    def a_method(self, a, b):
12        if isinstance(a, int) and isinstance(b, int):
13            return str(a + b)
14        elif isinstance(a, float) and isinstance(b, float):
15            return str(a + b)
16        else:
17            return super().a_method(a, b)

Note: Go doesn't support method overloading. To demonstrate a similar concept, we use different method names based on the parameter types.

Class Typecasting Rules

In an inheritance hierarchy, you can cast an object to any of its parent or child classes in the hierarchy tree. This can be done with the same syntax used for primitives ((newType) oldObject). Keep in mind, however, that you will get an Exception at runtime if you try to do illegal casting (for example, cast a Square to a Circle).

If you try to typecast an object to a subtype, it is called downcasting, or narrowcasting. Generally, it is considered bad practice to downcast unless it is absolutely necessary in your code. If you try to typecast an object to a superclass type, then it is called upcasting, or wide casting. Most polymorphism concepts are applied with the help of upcasting in a program.

1# Python is dynamically typed, so casting isn't strictly necessary, but to illustrate the concept:
2class Main:
3    rect = Rectangle(5, 6)
4
5    # Upcasting
6    shape = rect
7
8    # Downcasting (Python doesn't require explicit casting)
9    rect2 = shape
10
11    # This is also valid
12    shape2 = Rectangle()
13
14    # But this is invalid (and will throw an error if tried)
15    # rect3 = Shape()

It is not allowed in Java to typecast to other classes outside of the superclass and subclasses set. In those cases, you will need a ModelMapper which will give you class A given B or class B given A regardless of any inheritance relationship between A and B.

It's too complicated to go through ModelMapper now, so we are leaving it out of this series. However, feel free to go and check the documentation yourself if you are curious.

Conclusion

That's all for today! In the next lesson, we will learn about abstract classes, multiple inheritance, and interfaces. We will also learn how to make OOP design decisions and create UML diagrams, so be prepared to become a master of object-oriented programming.

One Pager Cheat Sheet

  • We will be learning about Advanced Practices in Object-Orientated Programming today, which are essential for understanding and using OOP.
  • Access level modifiers are used to determine which classes can use defined fields or invoke specific methods in order to maintain access control for members of a class.
  • All public attributes and class methods should be used as sparingly as possible, and there should only be one public class per file in Java, where the file name must be the same as its class name.
  • Classes, methods, or attributes without any access keyword defined are package-private, allowing any class inside the same package to access them, but no other class outside the package can.
  • Square can access protected methods and attributes of its ancestor classes in the inheritance hierarchy.
  • The most restrictive access modifier private should be used to hide data from the outside world, leading to fewer bugs later on.
  • In all OOP-supported programming languages, this or another special keyword must be used to access the "self" object from within a class in order to access attributes or pass an object to an outside function.
  • The current object is referenced using the this keyword, which in this context refers to an instance of the A class, and should be used to access the text property of the instance via this.text.
  • Classes can access methods of their superclass using the super keyword.
  • The use of super enables access to methods of a class' superclass and allows to access any superclass' method with the use of super.super.method().
  • Using Method Chaining, multiple functions can be called on an object in a single statement, returning the object to allow function calls to be chained together.
  • Overriding occurs between the superclass and subclass, while overloading is done within the same class and differs in the function signature.
  • You can upcast and/or downcast an object within its inheritance hierarchy, but it is bad practice to downcast unless necessary, and casting to classes outside of the hierarchy requires a ModelMapper.
  • Becoming a master of Object-Oriented Programming (OOP) involves learning about abstract classes, multiple inheritance, and interfaces, as well as making OOP design decisions and creating UML diagrams.