[OOP] Characteristics of Object Orientation - Inheritance
[OOP] Characteristics of Object Orientation - Inheritance
It's a lazy weekend, and Mr. A is casually watching a movie. In it, the protagonist, a girl who'd lived her whole life mistreated, one day receives a massive inheritance from someone. It turns out she was the long-lost granddaughter of a wealthy conglomerate family! From there, she uses the inheritance to repay those who'd been kind to her, and deliver a satisfying revenge on those who'd looked down on her.
Mr. A, who found it fairly satisfying, soon remembers that this kind of thing could never happen in real life. We call this inheritance. As a concept, inheritance like this used to only be something you'd see in movies or dramas. It was practically a fictional concept, one you'd only find in a dictionary.
Me, living an ordinary life, but in a computer isekai I'm suddenly an heir???!!?!?!
But in object-oriented languages, anyone can easily inherit whenever they need to!
Object orientation has this exact same concept. Inheritance in object orientation means an object inheriting from another object and using the elements of the object it inherited from.
Here, the object that inherits from another is called the child, and the object being inherited from is called the parent.
A child object can access the parent object's variables and methods according to the parent's encapsulation setup. Also, when the parent object is an abstract object, the child can implement or work with the parent object's methods through abstract methods and overriding.
An abstract object is an object that includes one or more abstract methods.
JAVA
abstract public class Main { // 메소드 }
An abstract class expressed in Java looks like this, with the abstract keyword placed at the front of the class to indicate that the object is abstract.
An abstract method is a method that must be implemented by the child object.
JAVA
abstract public class Main { public void normalMethod() { System.out.println("일반 메소드"); } abstract public void abstractMethod(); }
The example above is an abstract object expressed in Java. normalMethod() is a regular method, and abstractMethod() is an abstract method. Abstract methods differ significantly from regular methods in that the method's behavior isn't described at all.
Implementing an abstract method is the child object's responsibility, and this happens at one of the following stages.
- When an instance of the abstract object is created
- When the abstract object is inherited
A regular method is declared within its own object. But for an abstract method, the declaration happens in the object that's going to be assigned the abstract object. What advantage does this offer?
For example, suppose there's a parent object Main and a child object Sub that inherits from it. What if, structurally, abstractMethod() absolutely needs to use the child object's variables or methods?
If the behavior is already declared in the parent object, like normalMethod(), it becomes very difficult to reflect the child object's elements. You could try creating an instance, but if you don't know in advance which object will inherit from it, this is nearly impossible unless you pre-allocate instances of every conceivable object. And that approach wastes a tremendous amount of memory.
On the other hand, an abstract method like abstractMethod() is implemented in the child object, so it can directly access the child object's variables and methods. Because of this, when you need to implement behavior that relies on the child object's elements, defining that method as abstract makes it easy to implement in a way tailored to the child object's characteristics.
Let's create an instance of Main inside Sub in Java.
JAVA
public class Sub { public void run() { Main main = new Main() { @Override public void abstractMethod() { System.out.println(text()); } } } private String text() { return "Sub 객체의 요소"; } }
Normally, the abstractMethod() method wouldn't be able to access Sub's text(), because text() has the private access modifier.
But since the abstract method is implemented within Sub, it can directly access all of Sub's elements. In other words, it can access even internal methods marked private.
Let's have Sub inherit from Main in Java.
JAVA
public class Sub extends Main { @Override public void abstractMethod() { System.out.println(text()); } private String text() { return "자식 객체 Sub의 요소"; } }
If the parent object has an abstract method, the child object must override it. Otherwise, a compile error occurs.
Likewise, since the method is implemented in the child object, it can access all of the child object's elements.
In this way, abstract methods delegate the responsibility of implementation to the child object, granting unrestricted access to the child object's elements. Even though these would normally need to be opened up with something like public, since the implementation happens inside the child object itself, there's no need to change the access modifier at all.
Let's use Java to see exactly how object inheritance works and how it's used.
JAVA
import java.util.Date; /** * 컴퓨터 추상 클래스 * * @author RWB * @since 2021.08.06 Fri 21:19:19 */ abstract public class Computer { private final String OS; /** * Computer 생성자 함수 * * @param os: [String] OS 이름 */ public Computer(String os) { this.OS = os; } /** * 시작 함수 */ public void startup() { System.out.println(new StringBuilder().append(OS).append(" - started at ").append(new Date().toString())); } /** * 종료 함수 */ public void shutdown() { System.out.println(new StringBuilder().append(OS).append(" - shutdown at ").append(new Date().toString())); } /** * 동작 추상 함수 */ abstract public void run(); }
Here we have an abstract object called Computer. This object has a state called OS, and behaviors called startup, shutdown, and run.
Of these, run is a bit special—it's written as behavior, but no specification of exactly how it behaves is provided.
This is one of the characteristics of an abstract object: an abstract object can include one or more abstract methods. An abstract method is an unimplemented method—think of it as roughly representing the concept of a behavior. Implementing an abstract method happens in the child object that inherits from that object. In other words, the abstract method run performs behavior that's implemented differently by each child.
The two classes below, Asus and Dell, are child classes that inherit from the abstract class Computer.
JAVA
/** * ASUS 컴퓨터 클래스 * * @author RWB * @since 2021.08.06 Fri 21:24:50 */ public class Asus extends Computer { /** * Asus 생성자 함수 * * @param os: [String] OS 이름 */ public Asus(String os) { super(os); } /** * 동작 함수 */ @Override public void run() { System.out.println("ASUS 작업 수행"); } }
JAVA
/** * DELL 컴퓨터 클래스 * * @author RWB * @since 2021.08.06 Fri 21:26:46 */ public class Dell extends Computer { /** * Dell 생성자 함수 * * @param os: [String] OS 이름 */ public Dell(String os) { super(os); } /** * 시작 함수 */ @Override public void startup() { super.startup(); System.out.println("시스템 안정화 수행"); } /** * 종료 함수 */ @Override public void shutdown() { System.out.println("시스템 프로세스 정리 수행"); super.shutdown(); } /** * 동작 함수 */ @Override public void run() { System.out.println("DELL 작업 수행"); } }
You can see that both Asus and Dell inherit from Computer. You can also see that both implement the run function differently.
However, unlike Asus, Dell adds pre- and post-processing steps for system stability during startup and shutdown, respectively.
To implement these pre/post steps, startup and shutdown are overridden. Through this process, the desired behavior is added to each of the startup and shutdown functions.
super?
When a child class needs to call the parent class, it does so using the super keyword. You can see it used in Dell's overridden method behavior. super.shutdown() calls the shutdown() method of the parent class, Computer.
JAVA
/** * 메인 클래스 * * @author RWB * @since 2021.06.14 Mon 00:06:32 */ public class Main { /** * 메인 함수 * * @param args: [String[]] 매개변수 */ public static void main(String[] args) { Dell dell = new Dell("Windows 10 Pro"); Asus asus = new Asus("Ubuntu 21.04"); dell.startup(); dell.run(); dell.shutdown(); System.out.println(); asus.startup(); asus.run(); asus.shutdown(); } }
OUTPUT
Windows 10 Pro - started at Fri Aug 06 22:54:39 KST 2021 시스템 안정화 수행 DELL 작업 수행 시스템 프로세스 정리 수행 Windows 10 Pro - shutdown at Fri Aug 06 22:54:39 KST 2021 Ubuntu 21.04 - started at Fri Aug 06 22:54:39 KST 2021 ASUS 작업 수행 Ubuntu 21.04 - shutdown at Fri Aug 06 22:54:39 KST 2021
Running Asus and Dell's methods in sequence produces the result shown above. You can see that Dell's extra system operations run during both startup and shutdown.
Object orientation strives to modularize every object. Good modularization aims for encapsulation and information hiding to be properly implemented and maintained.
But just as sturdy packaging is hard to open, tight modularization tends to make a module rigid. Not only would the scope for reuse be limited, but extending it would also become difficult. If object orientation only had these two concepts, developers would constantly have to compromise between reusability and modularity when implementing objects.
But thanks to the existence of the concept of inheritance, you can ensure reusability and extensibility without compromising an object's designated modularity at all. It's an ingenious concept that offsets the dilemma created by object orientation's modularization. Personally, I think it's the single most important characteristic among object orientation's features. Of course, it's also one of the trickier concepts within object orientation, but understanding it well will let you write code that's a bit more true to the spirit of object orientation.
