Lesson 4
Implementing Inheritance Wisely
Introduction

Welcome to another lesson of the Clean Code in Java course! In this course, we have explored foundational concepts like the Single Responsibility Principle, encapsulation, and constructors, all essential for writing clear, maintainable, and efficient code. In this lesson, we'll focus on implementing inheritance wisely. By understanding the role of inheritance, we'll learn how to apply it effectively to enhance code readability and organization while maintaining the principles of clean code.

How Inheritance is Important to Writing Clean Code

Inheritance is a powerful feature in object-oriented programming that allows for code reuse and logical organization. It enables developers to create a new class based on an existing class, inheriting its properties and behaviors. This can lead to more streamlined and easier-to-understand code when used appropriately.

  • Code Reuse and Reduction of Redundancies: By creating subclasses that inherit from a base class, you can avoid duplicating code, making it easier to maintain and extend.
  • Improved Readability: Logical inheritance hierarchies can improve the clarity of your software. For example, if you have a base class Vehicle, with subclasses Car and Motorcycle, the organization makes intuitive sense and clarifies each class's role.
  • Alignment with Previous Concepts: Inheritance should respect the Single Responsibility Principle and encapsulation. Each class should have a clear purpose and keep its data protected, whether it's a base class or a subclass.
Best Practices When Using Inheritance

To leverage inheritance effectively, it's important to follow several best practices:

  • Favor Composition Over Inheritance: Sometimes, inheritance can lead to tightly coupled code. In such cases, composition (where you include instances of other classes) might be a better option.
  • Clear and Stable Base Class Interfaces: Make sure to provide a consistent and limited interface in base classes to prevent subclasses from depending too much on implementation details.
  • Avoid Deep Inheritance Hierarchies: Deep hierarchies can complicate the understanding and maintenance of the code, leading to harder debugging and modification.

Common pitfalls include overusing inheritance to model relationships that might not naturally fit an "is-a" relationship and abusing inheritance for code sharing without considering logical organization.

Bad Example

Let’s explore a bad example to understand the misuse of inheritance:

Java
1class Person { 2 String name; 3 int age; 4 5 void work() { 6 System.out.println("Person working"); 7 } 8} 9 10class Employee extends Person { 11 String employeeId; 12 13 void fileTaxes() { 14 System.out.println("Employee filing taxes"); 15 } 16} 17 18class Manager extends Employee { 19 void holdMeeting() { 20 System.out.println("Manager holding a meeting"); 21 } 22}

In this example:

  • The hierarchy is too deep, with Manager extending Employee, which extends Person.
  • Person having a work() method might be inappropriate because not every person works, making the base class less general.
  • The inheritance might be forced where a Manager "is-a" Person, but the middle class Employee may not be necessary as a separate entity.
Refactored Example

Now let's refactor the previous example to follow best practices:

Java
1class Person { 2 String name; 3 int age; 4 5 Person(String name, int age) { 6 this.name = name; 7 this.age = age; 8 } 9} 10 11class Employee { 12 Person personDetails; 13 String employeeId; 14 15 Employee(Person personDetails, String employeeId) { 16 this.personDetails = personDetails; 17 this.employeeId = employeeId; 18 } 19 20 void fileTaxes() { 21 System.out.println(personDetails.name + " filing taxes"); 22 } 23} 24 25class Manager extends Employee { 26 Manager(Person personDetails, String employeeId) { 27 super(personDetails, employeeId); 28 } 29 30 void holdMeeting() { 31 System.out.println(personDetails.name + " holding a meeting"); 32 } 33}

In the refactored example:

  • Person no longer has a work() method, making it more general.
  • Employee now uses composition to include a Person object instead of inheriting from it. This simplifies the hierarchy.
  • Manager still inherits from Employee, maintaining logical structure but with reduced complexity.
Summary and Next Steps

In this lesson, we explored how to implement inheritance wisely to support clean code practices. By favoring composition over inheritance when appropriate and ensuring clear, stable class designs, you can create more maintainable and understandable code. We've focused on how inheritance can be a powerful tool when used correctly, complementing the previously covered concepts like SRP and encapsulation.

Next, you'll have the opportunity to apply and solidify these principles with practice exercises. Remember, clean code principles continue beyond these lessons, and we encourage you to keep practicing and applying them in your coding endeavors.

Enjoy this lesson? Now it's time to practice with Cosmo!
Practice is how you turn knowledge into actual skills.