Can Inheritance Break Encapsulation?
Tushar Singhal
Principal Data Engineer @ Nielsen | Patent 2413120US | [email protected] | Newsletter
Inheritance is a fundamental concept in software architecture that promotes code reusability and modularity. Relating it to a real-life example makes it easier to understand. Simply put, inheritance is like a gift from our ancestors that we naturally inherit, a phenomenon that continues from generation to generation. It serves as a glue that binds generation together. For instance, when we go for a medical checkup and the doctor asks, "Does anyone in your family have heart disease?" that’s a clear reminder of how inherited traits impact our lives!
Let’s delve into inheritance in detail, focusing on its design aspects to enhance architectural modularity, reusability, and maintainability.
Object-oriented programming languages (Java, C++, Python, Scala, Kotlin, PHP, Perl etc) embraces the concept of inheritance, similar to a family tree. The name itself is self-explanatory: parent classes share their attributes with child classes, enabling these children to inherit essential properties simply by being created. This connection allows the characteristics of parent classes to propagate through all child and sub-child classes. Ultimately, this hierarchy enhances the capabilities of sub-child classes, making them more powerful and versatile.
In the example below, the Dog class does not define its own speak() method, yet we can still call it because the Dog class inherits this method from its parent class Animal, allowing for effective code reuse.
scala> class Animal {
def speak(): String = "Animal sound"
}
scala> class Dog extends Animal {
}
scala> val dog = new Dog()
scala> println(dog.speak())
Animal sound
Types of inheritance
Single inheritance : A class extends only one parent class. It establishes a straightforward parent-child relationship.
scala> class Animal {
def speak(): String = "Animal sound"
}
scala> class Dog extends Animal {
}
Multiple inheritance : A class extending more than one parent class. While powerful, multiple inheritance can lead to complexities, such as the diamond problem.
scala> trait Animal {
def speak(): String = "Animal sound"
}
scala> trait Quadruped {
def legs(): String = "Four legs"
}
scala> class Dog extends Animal with Quadruped {
}
Multilevel inheritance : A class extends another class that itself extends a third class. This creates a chain of inheritance, where each level can inherit properties and methods from its parent class.
scala> class Animal {
def speak(): String = "Animal sound"
}
scala> class Quadruped extends Animal {
def legs(): String = "Four legs"
}
scala> class Dog extends Quadruped {
}
Different ways of Implementation
We often see the "extends" keyword in code, which is used to inherit from a parent class, effectively bringing all of the parent class’s properties and methods into the child class.
traits, abstract classes, and regular classes are used to implement inheritance by encapsulating the small part of code logic so that it can we reused.
Traits are incredibly powerful, often reducing the need to use abstract classes in most of the cases. We typically only resort to abstract classes when we need to create a base class that requires constructor arguments.
scala> trait Animal(name: String)
^
error: traits or objects may not have parameters
In this case we need to have an abstract class which can have constructor holding the name variable.
scala> abstract class Animal(name: String)
Consider this scenario:
We need to implement a resource estimator for our data pipelines that calculates the resources required to run the pipeline based on the type of run. For this example, let's consider two types of runs: incremental run and history load. An incremental run will involve less data and, consequently, require fewer resources, while a history run will demand a larger amount of resources.
scala> abstract class Estimator(runType: String) {
def getResources(): Int = {
runType match {
case "incremental" => 5
case "history" => 20
case _ => throw new IllegalArgumentException("Invalid run type")
}
}
def otherMethod(): Unit
}
scala> class Driver(runType: String) extends Estimator(runType) {
def runPipeline(): Unit = {
val resources = getResources()
run(resources)
}
def run(resources: Int): Unit = {
// Logic to run the pipeline goes here
println(s"Pipeline is running with $resources resources")
}
def otherMethod(): Unit = {
// implement parent abstract method
}
}
scala> val incrementalDriver = new Driver("incremental")
scala> incrementalDriver.runPipeline()
Pipeline is running with 5 resources
In this scenario, traits are not useful because they do not support constructors. Since we need to initialize specific attributes related to the resource estimator, using an abstract class is more appropriate.
领英推荐
Design perspective of Inheritance :
Inheritance offers significant advantages, primarily by promoting code reuse and accelerating development. However, it also has its downsides. Let’s explore how to make informed decisions regarding its use during architectural design.
Inheritance helps break down complex systems into modular components. Each class can focus on specific functionalities, making it easier to understand and manage. This feature enhances the modularity of the architecture.
With a clear hierarchy, maintaining and updating code becomes simpler. Changes made in a parent class automatically propagate to child classes, reducing the risk of errors. This help in reducing maintenance cost.
By allowing classes to inherit common behaviours and attributes, inheritance promotes greater code reuse.
But It is often said that "inheritance breaks encapsulation"
One common concern is that "inheritance breaks encapsulation" because it exposes the internal details of the parent class to all child classes. This exposure can lead to unintended dependencies and make it difficult to modify the parent class without affecting its subclasses.
To mitigate this issue, we can effectively use access modifiers. Understanding the visibility of inherited members is crucial for designing effective class hierarchies in object-oriented programming. Properly managing access levels helps maintain encapsulation, allowing for safer and more flexible code.
class Parent {
public def greet(): String = "Hello!"
}
class Child extends Parent {
def sayHello(): String = greet() // Accessible
}
class Parent {
protected def secret(): String = "This is a secret."
}
class Child extends Parent {
def revealSecret(): String = secret() // Accessible
}
class Parent {
private def privateInfo(): String = "Private Information"
}
class Child extends Parent {
def accessInfo(): String = privateInfo() // Not accessible, will cause an error
}
On line 2: error: not found: value privateInfo
The most restricted access modifier is private. When a member is declared as private, it is accessible only within the class itself. Choosing the right access modifier is essential, as it can help mitigate issues related to violating encapsulation.
Consider this scenario:
scala> trait Person {
private var name: String = ""
def getName: String = name
def setName(newName: String): Unit = {
name = newName
}
}
scala>class Employee extends Person {
def displayInfo(): Unit = {
println(s"Name: $getName")
}
}
scala> val emp = new Employee
scala> emp.setName("Think Design First")
scala> emp.displayInfo()
Name: Think Design First
The Employee class must use the getName method because the name variable is restricted from being accessed directly from outside the class.
This practice promotes better design and enhances the overall integrity of the code.
Thanks you so much for reading, Feel free to share your thoughts on this concept....
Follow for more content on your timeline!
Happy Learning :)