Object-oriented programming (OOP) is a fundamental concept in software engineering — even though the wave of functional programming is upon us! It’s a programming paradigm that allows developers to create modular and reusable code by organizing data and behavior into objects. One key aspect of OOP is inheritance, which allows us to create new classes based on existing classes, inheriting their properties and behaviors. Inheritance, modeling an “is a” relationship, is something many of us learn early in our OOP journey so I wanted to show several examples of inheritance in C# to demonstrate it.
This article is intended for beginners who want to learn more about inheritance, and as always when I discuss inheritance, I am much more in favor of people leaning into composition. I want to acknowledge this bias because, after 20 years of programming, I have personally made the mistake of using inheritance for everything and seen many others do it too. Something to keep in mind as you read through!
Understanding Inheritance
In object-oriented programming, inheritance is a powerful concept that allows us to define a new class based on an existing class. It enables the new class, called the derived class or subclass, to inherit the properties and behaviors of the existing class, known as the base class or superclass. This inheritance relationship helps to organize and structure code in a meaningful way.
The purpose of inheritance is to promote code reusability and enhance modularity. By inheriting from a base class, we can reuse the code and functionality already defined in it, without having to rewrite it in the derived class. This not only saves time and effort but also promotes consistency across different classes.
Real World Example to Model with Inheritance
To understand inheritance better, let’s consider a real-world analogy. Think of a car manufacturer that produces different models of cars. Each car model shares common features such as the ability to drive, brake, and accelerate. These common features can be thought of as the base class. The specific car models, such as Sedan, SUV, or Hatchback, can be considered as the derived classes. This is because each of these specific car types have an “is a” relationship to the base — they are all cars!
In the analogy, the base class defines the common features that all car models inherit. The derived classes, on the other hand, can add additional features or modify existing ones to meet their specific requirements. For example, the Sedan class may have additional functionality like a sunroof, while the SUV class could have features like 4-wheel drive.
How It Works In CSharp
Let’s talk about the very basics here. In C#, we can create a base class using the class
keyword and specify the common properties and methods. But how do we represent the “is a” relationship?
We can create derived classes using the :
symbol followed by the name of the base class, which effectively says this class “is a” some other class! The derived classes can then add their unique properties and methods while inheriting the ones defined in the base class. By using inheritance, we can create a hierarchy of classes, with each level inheriting and extending the functionality of the previous level.
We can organize our code in a hierarchical manner, which for many developers allows them to push common re-usable code into base classes. One of the arguments is that this helps with reusability and maintenance — following the Don’t Repeat Yourself (DRY) principle — but my own bias suggests this is often not an ideal way to have reusable code. But we’ll see composition in object-oriented programming later.
Creating Inheritance Relationships in CSharp
In object-oriented programming, inheritance allows us to create relationships between classes. With inheritance, we can define a base class that serves as a blueprint, and then create derived classes that inherit properties and methods from the base class.
To create an inheritance relationship between classes in C#, we use the colon (:) symbol. This was described earlier, so let’s take a look at the syntax for declaring a base class and a derived class in this example:
public class Shape
{
// Base class properties and methods
}
public class Circle : Shape
{
// Derived class properties and methods
}
In the example above, the class Shape
is the base class, and the class Circle
is the derived class. The colon (:) symbol indicates that the Circle
class inherits from the Shape
class.
When a class inherits from a base class, it automatically inherits all the public and protected properties and methods of the base class. This means that the Circle
class will have access to all the properties and methods defined in the Shape
class — in this example so far there are none. So let’s continue!
The Virtual and Override Keywords in CSharp
Sometimes, we may want to modify or extend the behavior of a method that is inherited from the base class. In C#, we can do this by using the override
keyword along with the virtual
keyword. Here’s an example:
public class Shape
{
public virtual void Draw()
{
// Base class implementation
}
}
public class Circle : Shape
{
public override void Draw()
{
// Derived class implementation
}
}
In this example, the Draw
method is declared as virtual
in the Shape
class, which means it can be overridden in derived classes. The Circle
class overrides the Draw
method and provides its own implementation.
Abstract Classes and Abstract Members in CSharp
We can use another keyword in C# called abstract
to offer different behavior from the base class. If we have an abstract
class, the specific type cannot be instantiated, but derived classes inheriting from it can be. Abstract
classes can also offer shared functionality by defining members that are accessible to derived classes.
However, abstract
classes can also have abstract
members! These abstract
members, unlike virtual
members, do not provide default functionality — so derived classes MUST provide an implementation using the override
keyword that we saw previously.
Let’s see that same example with shapes but this time with a twist:
public abstract class Shape
{
public abstract void Draw();
}
public class Circle : Shape
{
public override void Draw()
{
// Derived class implementation that *MUST* be implemented
}
}
In the example above, the derived class looks identical to what we saw in the previous example. However, a fundamental difference is that with virtual
we could optionally override it — with abstract
, we must override and provide an implementation.
Accessing Base Class Members in CSharp
To access the members of the base class from the derived class, we use the base
keyword. This allows us to call the base class constructor, access base class properties, or invoke base class methods. Here’s an example:
public class Shape
{
public string Color { get; set; }
}
public class Circle : Shape
{
public Circle(string color)
{
base.Color = color; // Accessing base class properties
}
}
In the above example, the Circle
class constructor takes a color
parameter and sets the Color
property inherited from the base class. This only works with access modifiers more visible than private, otherwise the derived class cannot see the member.
The above example does not require the base
keyword, even though it is correct. This is because there is not a conflicting current-scope member called Color that would conflict — the only Color is in the base. If you were overriding a virtual member and wanted to call the base implementation still from your override, this is a great use for explicitly putting the base
keyword.
Practical Scenarios of Using Inheritance in CSharp
Let’s check out some practical examples of when and how to use inheritance in software development.
Modeling the Relationship Between Employees
Let’s consider a scenario where we are building a payroll system for a company. The system needs to handle different types of employees, such as full-time employees, part-time employees, and contractors. Instead of creating separate classes for each type, we can use inheritance to model their relationship. That’s because we can say that each type of employee “is an” employee — that’s an inheritance relationship.
We can create a base class called “Employee” which contains common attributes like name, id, and salary. Then, we can create derived classes like “FullTimeEmployee,” “PartTimeEmployee,” and “Contractor” that inherit from the base “Employee” class. By doing this, we can reuse the common attributes and behaviors defined in the base class, while also adding specific attributes or behaviors unique to each type of employee.
Here’s some example code to show this relationship:
public class Employee
{
public Employee(
string name,
int id,
decimal salary)
{
Name = name;
Id = id;
Salary = salary;
}
public string Name { get; set; }
public int Id { get; set; }
public decimal Salary { get; set; }
public virtual void DisplayEmployeeInfo()
{
Console.WriteLine($"Name: {Name}, ID: {Id}, Salary: {Salary}");
}
}
public class FullTimeEmployee : Employee
{
public FullTimeEmployee(
string name,
int id,
decimal salary) :
base(name, id, salary)
{
}
}
public class PartTimeEmployee : Employee
{
public PartTimeEmployee(
string name,
int id,
decimal salary,
int workingHours) :
base(name, id, salary)
{
WorkingHours = workingHours;
}
public int WorkingHours { get; set; }
public override void DisplayEmployeeInfo()
{
base.DisplayEmployeeInfo();
Console.WriteLine($"Working Hours: {WorkingHours}");
}
}
public class Contractor : Employee
{
public Contractor(
string name,
int id,
decimal salary,
DateTime contractEndDate) :
base(name, id, salary)
{
ContractEndDate = contractEndDate;
}
public DateTime ContractEndDate { get; set; }
public override void DisplayEmployeeInfo()
{
base.DisplayEmployeeInfo();
Console.WriteLine($"Contract End Date: {ContractEndDate.ToShortDateString()}");
}
}
Can you identify the custom behavior for the derived classes above? What keywords do you need to be looking for?
Reusability and Maintainability
Inheritance not only allows us to model relationships but also promotes code reuse and maintainability. Consider a scenario where we are developing a game. We can have a base class called “Character” which defines common attributes and methods for all characters in the game, such as health points and attack behavior. Then, we can create derived classes like “Player” and “Enemy” that inherit from the base “Character” class.
By doing this, we can reuse the common attributes and methods defined in the base class, preventing code duplication. Additionally, if we need to make changes to the common attributes or behaviors, we only need to update the code in one place (the base class), which simplifies maintenance and reduces the chances of introducing bugs.
public abstract class Character
{
protected Character(
string name,
int healthPoints)
{
Name = name;
HealthPoints = healthPoints;
}
public int HealthPoints { get; set; }
public string Name { get; set; }
public virtual void Attack(Character target)
{
// Default attack behavior
}
}
public class Player : Character
{
public Player(
string name,
int healthPoints)
: base(name, healthPoints)
{
}
public override void Attack(Character target)
{
// Player-specific attack behavior
}
}
public class Enemy : Character
{
public Enemy(
string name,
int healthPoints)
: base(name, healthPoints)
{
}
public override void Attack(Character target)
{
// Enemy-specific attack behavior
}
}
Design and Planning Considerations
When utilizing inheritance in software engineering, proper design and planning are important. It’s important to identify the entities and their relationships, and carefully decide which attributes and behaviors should be included in the base class versus the derived classes. Inheritance should be used with caution, keeping in mind the principle of “favor composition over inheritance” to avoid excessive class hierarchies and inheritance chains.
While this isn’t a rule, a helpful guideline I find is that if I’m building classes that define logic or algorithms, inheritance is generally a bad fit. I want to compose these systems with other systems. When I’m defining data structures, composition can still be an excellent option, but this is where I find inheritance might be a good fit. As these two statements suggest, composition is more frequently something I find can be used effectively, and it’s actually more rare that I find inheritance a good fit. By following good design principles and leveraging inheritance appropriately, we can create more maintainable and reusable code in our software projects.
Common Pitfalls and Best Practices in Inheritance
When working with inheritance in C#, there are some common pitfalls that developers should be aware of to ensure efficient and effective code. Let’s take a look at some of the key considerations.
Avoid Deep Inheritance Hierarchies
One common mistake in inheritance is creating deep hierarchies with many levels of subclasses. While it may seem like a good idea at first to divide functionality into smaller subclasses, it can quickly become complex and difficult to maintain. Deep hierarchies can lead to code duplication, increased complexity, and decreased readability. It’s advisable to keep the inheritance hierarchy as shallow as possible to maintain simplicity and flexibility in your code.
Understanding the “is-a” Relationship
Inheritance is all about creating an “is-a” relationship between classes. It means that a subclass is a specific type of the base class. For example, in a shape hierarchy, a circle is a specific type of shape. This concept is important to the design and implementation of inheritance. Understanding the “is-a” relationship is important to ensure that subclasses inherit the appropriate attributes and behaviors from the base class.
Best Practices for Using Inheritance Effectively
To use inheritance effectively and efficiently in C# programming, consider the following best practices:
Design for Reusability: Inheritance allows you to reuse code from the base class in the subclass. Make sure that the base class is well-designed and provides a solid foundation for the subclasses to build upon. Remember, you don’t need inheritance to be able to re-use code… the purpose of inheritance is to model an “is a” relationship between types.
Prioritize Composition over Inheritance: In some (probably most) cases, composition may be a better choice than inheritance. Instead of using a subclass, consider using composition to create a class that contains instances of other classes. This approach generally provides more flexibility and reduces the coupling between classes. Using an “is made of” relationship instead of an “is a” relationship is very prominent in systems we build, especially when it comes to logic. “is a” relationships for inheritance may be a better fit in representing data transfer objects instead.
Properly Use Abstract Classes and Interfaces: Abstract classes and interfaces play a significant role in inheritance. Abstract classes provide a blueprint for subclasses and can implement common functionality, while interfaces define a contract that classes must adhere to. Remember, if you are just looking for a spot for common code to give consideration if composition will be a better fit instead of just trying to drive more code into a base class.
Avoid Fragile Base Class Problem: The fragile base class problem occurs when modifications to the base class inadvertently break functionality in its subclasses. To avoid this, ensure that changes to the base class, such as adding or modifying methods, do not have an unexpected impact on the subclasses. Again, this becomes a BIG problem when you have complex inheritance hierarchies with logic scattered between the base class and multiple derived classes — and much of the time this can be avoided with composition.
Remember to keep your inheritance hierarchies shallow, understand the “is-a” relationship, and design for reusability. With an understanding of inheritance concepts and best practices, remember to give strong consideration as to whether composition is a better fit. You might just be using inheritance because you feel more comfortable with it instead of for better OOP modeling reasons.
Wrapping Up Examples of Inheritance in CSharp
I’m hopeful you caught at least a few instances throughout the article of me recommending that you consider composition over inheritance because truly I do see inheritance get overused. I was responsible for a LOT of code early in my career that overused inheritance and we ultimately had to rewrite a significant portion of it because of how much it crippled our ability to extend the code. It broke most of the guidelines in the best practices section of this article.
With that said though, inheritance has its time and its place. I think if you take the time to understand both of these paradigms you can see how to use them effectively.
If you found this useful and you’re looking for more learning opportunities, consider subscribing to my free weekly software engineering newsletter and check out my free videos on YouTube! Meet other like-minded software engineers and join my Discord community!
Want More Dev Leader Content?
- Follow along on this platform if you haven’t already!
- Subscribe to my free weekly software engineering and dotnet-focused newsletter. I include exclusive articles and early access to videos: SUBSCRIBE FOR FREE
- Looking for courses? Check out my offerings: VIEW COURSES
- E-Books & other resources: VIEW RESOURCES
- Watch hundreds of full-length videos on my YouTube channel: VISIT CHANNEL
- Visit my website for hundreds of articles on various software engineering topics (including code snippets): VISIT WEBSITE
- Check out the repository with many code examples from my articles and videos on GitHub: VIEW REPOSITORY
Top comments (0)