Iron Academy Logo
Learn C#

Understanding C# OOP

Tim Corey
49m 41s

Inheritance and interfaces are integral parts of object-oriented programming (OOP). Tim Corey, in his video "Inheritance vs Interfaces in C#: Object Oriented Programming," provides a detailed explanation of when to use inheritance and when to opt for interfaces.

This article serves as a comprehensive guide of Tim Corey's video. It breaks down the key concepts, examples, and code explanations provided in the video, highlighting the differences between inheritance and interfaces and when to use each.

Introduction

Tim at (0:00) begins by highlighting the importance of distinguishing between inheritance and interfaces. He emphasizes the need to understand when to use each concept to achieve the best results. His objective is to demonstrate this through examples, starting with incorrect usage single inheritance and then correcting it.

Creating the Project

At (1:08), Tim creates a simple console application using .NET 5. He names the project "OODemoApp" and explains that the primary goal is to demonstrate the concepts rather than create production-ready code.

Understanding Inheritance

Tim at (1:55) delves into the basics of inheritance. He defines inheritance as a mechanism where a base class's properties and methods are inherited by a derived class. He stresses that inheritance should not be used merely for code reuse and sharing but for establishing a logical "is-a" relationship.

Key points:

  • Is-a Relationship: Ensures that the derived class is a type of the base class.

  • Common Logic: Inherited classes should share common logic, not just properties or method signatures.

Example Class: Rental Car

Tim at (7:52) creates a RentalCar class to illustrate fundamental concept of inheritance. This class represents a rental car in a rental agency in Miami, Florida.

public class RentalCar
{
    public int RentalId { get; set; }
    public string CurrentRenter { get; set; }
    public decimal PricePerDay { get; set; }
    public int NumberOfPassengers { get; set; }

    public void StartEngine()
    {
        Console.WriteLine("Turn key to ignition setting");
        Console.WriteLine("Turn key to on");
    }

    public void StopEngine()
    {
        Console.WriteLine("Turn key to off");
    }
}
public class RentalCar
{
    public int RentalId { get; set; }
    public string CurrentRenter { get; set; }
    public decimal PricePerDay { get; set; }
    public int NumberOfPassengers { get; set; }

    public void StartEngine()
    {
        Console.WriteLine("Turn key to ignition setting");
        Console.WriteLine("Turn key to on");
    }

    public void StopEngine()
    {
        Console.WriteLine("Turn key to off");
    }
}

Demonstrating the Pitfalls of Incorrect Inheritance

Tim at (10:15) explains how improper use of inheritance can lead to issues. He highlights that if inheritance is misused, it can lead to code that is difficult to manage and extend. He advises against using inheritance just to share code.

Adding Enums for Car and Truck Types

Tim at (10:45) adds an enumeration for car types. He creates a new class file named Enums.cs to keep all enums in one place. This enum will help differentiate between different car styles.

// Enums.cs
public enum CarType
{
    Hatchback,
    Sedan,
    Compact
}
// Enums.cs
public enum CarType
{
    Hatchback,
    Sedan,
    Compact
}

He then adds a property to the RentalCar class to specify the car type.

public class RentalCar : RentalVehicle
{
    public CarType Style { get; set; }
    // Other properties and methods
}
public class RentalCar : RentalVehicle
{
    public CarType Style { get; set; }
    // Other properties and methods
}

Expanding to Include Trucks

As Tim at (12:27) explains, the rental agency decides to add trucks to their fleet, which introduces new requirements. He creates a RentalTruck class inheriting from parent class RentalVehicle.

public class RentalTruck : RentalVehicle
{
    public TruckType Style { get; set; }
    // Other properties and methods
}
public class RentalTruck : RentalVehicle
{
    public TruckType Style { get; set; }
    // Other properties and methods
}

He then defines a new enum for truck types.

// Enums.cs
public enum TruckType
{
    ShortBed,
    LongBed
}
// Enums.cs
public enum TruckType
{
    ShortBed,
    LongBed
}

Handling Different Property Types

Tim at (15:28) emphasizes that just because two properties share the same name doesn't mean they are the same. He illustrates this with the Style property, which could mean different enums (CarType for cars and TruckType for trucks).

Introducing Boats to the Fleet

The rental agency expands its fleet to include boats. Tim demonstrates how to handle this by creating a RentalBoat class. Initially, it seems manageable since boats can share some properties with cars and trucks.

public class RentalBoat : RentalVehicle
{
    // Properties and methods specific to boats
}
public class RentalBoat : RentalVehicle
{
    // Properties and methods specific to boats
}

Dealing with Sailboats

The introduction of sailboats presents a challenge since sailboats do not have engines. Tim at (19:57) illustrates the limitations of inheritance in this scenario.

public class RentalSailboat : RentalVehicle
{
    public override void StartEngine()
    {
        throw new NotImplementedException("I do not have an engine to start");
    }

    public override void StopEngine()
    {
        throw new NotImplementedException("I do not have an engine to stop");
    }
}
public class RentalSailboat : RentalVehicle
{
    public override void StartEngine()
    {
        throw new NotImplementedException("I do not have an engine to start");
    }

    public override void StopEngine()
    {
        throw new NotImplementedException("I do not have an engine to stop");
    }
}

Solving the Problem with Interfaces

Tim suggests making the StartEngine and StopEngine methods virtual in the base class to allow for overriding in derived classes that do not use these methods.

public abstract class RentalVehicle
{
    // Common properties

    public virtual void StartEngine()
    {
        Console.WriteLine("Engine started");
    }

    public virtual void StopEngine()
    {
        Console.WriteLine("Engine stopped");
    }
}
public abstract class RentalVehicle
{
    // Common properties

    public virtual void StartEngine()
    {
        Console.WriteLine("Engine started");
    }

    public virtual void StopEngine()
    {
        Console.WriteLine("Engine stopped");
    }
}

Handling Methods That Should Not Be Called

Tim at (21:56) explains the pitfalls of having methods in inherited classes that should not be called. For the following example, the RentalSailboat class, which does not have an engine, inherits the StartEngine and StopEngine methods from the RentalVehicle class. This situation can lead to problems if these methods are called unintentionally, as they must throw exceptions to indicate that they are not applicable.

public class RentalSailboat : RentalVehicle
{
    public override void StartEngine()
    {
        throw new NotImplementedException("I do not have an engine to start");
    }

    public override void StopEngine()
    {
        throw new NotImplementedException("I do not have an engine to stop");
    }
}
public class RentalSailboat : RentalVehicle
{
    public override void StartEngine()
    {
        throw new NotImplementedException("I do not have an engine to start");
    }

    public override void StopEngine()
    {
        throw new NotImplementedException("I do not have an engine to stop");
    }
}

Recognizing the Limitations of Inheritance

Tim at (24:06) emphasizes how inheritance can lead to a convoluted and messy codebase when it no longer makes logical sense. For instance, a sailboat should not be treated as a RentalVehicle with an engine. This demonstrates the limitations of inheritance and the necessity for a better design approach hybrid inheritance.

Moving to a Better Design with Interfaces

To address these issues, Tim suggests a better design using interfaces. He starts by creating a new console application project named "BetterOODemo" to demonstrate the improved approach.

Defining the Interface

Tim introduces the IRental interface to encapsulate properties common to all rentals.

public interface IRental
{
    int RentalId { get; set; }
    string CurrentRenter { get; set; }
    decimal PricePerDay { get; set; }
}
public interface IRental
{
    int RentalId { get; set; }
    string CurrentRenter { get; set; }
    decimal PricePerDay { get; set; }
}

Creating the Base Class for Land Vehicles

Tim then creates a base class for land vehicles, separating the concept of public class vehicle rental from the vehicle itself.

public abstract class LandVehicle
{
    public int NumberOfPassengers { get; set; }

    public virtual void StartEngine()
    {
        Console.WriteLine("Engine started");
    }

    public virtual void StopEngine()
    {
        Console.WriteLine("Engine stopped");
    }
}
public abstract class LandVehicle
{
    public int NumberOfPassengers { get; set; }

    public virtual void StartEngine()
    {
        Console.WriteLine("Engine started");
    }

    public virtual void StopEngine()
    {
        Console.WriteLine("Engine stopped");
    }
}

By renaming the base vehicle class to LandVehicle, Tim ensures that only appropriate vehicles inherit the engine-related methods.

Implementing Car and Truck Classes

Tim creates Car and Truck classes that inherit from existing class LandVehicle and implement the IRental interface.

public class Car : LandVehicle, IRental
{
    public int RentalId { get; set; }
    public string CurrentRenter { get; set; }
    public decimal PricePerDay { get; set; }
    public CarType Style { get; set; }
}

public class Truck : LandVehicle, IRental
{
    public int RentalId { get; set; }
    public string CurrentRenter { get; set; }
    public decimal PricePerDay { get; set; }
    public TruckType Style { get; set; }
}
public class Car : LandVehicle, IRental
{
    public int RentalId { get; set; }
    public string CurrentRenter { get; set; }
    public decimal PricePerDay { get; set; }
    public CarType Style { get; set; }
}

public class Truck : LandVehicle, IRental
{
    public int RentalId { get; set; }
    public string CurrentRenter { get; set; }
    public decimal PricePerDay { get; set; }
    public TruckType Style { get; set; }
}

This design maintains a clear separation of concerns child class name, ensuring that each class only has properties and methods relevant to its type.

Avoiding Code Duplication

Tim at (31:41) discusses the importance of avoiding unnecessary code duplication. He explains that while the IRental interface requires the same properties in multiple classes, this is not considered a violation of the DRY (Don't Repeat Yourself) principle because no logic is duplicated—only property declarations.

Implementing the Rental Sailboat Class

Tim at (35:09) explains how to handle the RentalSailboat class separately by implementing the IRental interface, instead of inheriting from LandVehicle. This approach helps to avoid the pitfalls associated with inappropriate name inheritance in c#.

public class Sailboat : IRental
{
    public int RentalId { get; set; }
    public string CurrentRenter { get; set; }
    public decimal PricePerDay { get; set; }

    // Additional properties and methods specific to sailboats
}
public class Sailboat : IRental
{
    public int RentalId { get; set; }
    public string CurrentRenter { get; set; }
    public decimal PricePerDay { get; set; }

    // Additional properties and methods specific to sailboats
}

Managing Different Rentals

Tim sets up a list to manage different types of rentals, utilizing the IRental interface to store various rental types, including trucks, sailboats, and cars.

List<IRental> rentals = new List<IRental>
{
    new Truck { CurrentRenter = "Truck Renter" },
    new Sailboat { CurrentRenter = "Sailboat Renter" },
    new Car { CurrentRenter = "Car Renter" }
};
List<IRental> rentals = new List<IRental>
{
    new Truck { CurrentRenter = "Truck Renter" },
    new Sailboat { CurrentRenter = "Sailboat Renter" },
    new Car { CurrentRenter = "Car Renter" }
};

This design allows for looping through the rentals and accessing common properties like CurrentRenter, PricePerDay, and RentalId.

foreach (var rental in rentals)
{
    Console.WriteLine($"Renter: {rental.CurrentRenter}, Price Per Day: {rental.PricePerDay}");
}
foreach (var rental in rentals)
{
    Console.WriteLine($"Renter: {rental.CurrentRenter}, Price Per Day: {rental.PricePerDay}");
}

Casting to Specific Types

To access specific properties and methods of different rental types, Tim demonstrates how to use the is keyword to check and cast objects to their respective types of inheritance.

foreach (var rental in rentals)
{
    if (rental is Truck truck)
    {
        Console.WriteLine($"Truck Style: {truck.Style}, Passengers: {truck.NumberOfPassengers}");
    }
    else if (rental is Sailboat sailboat)
    {
        // Access sailboat-specific properties
    }
    else if (rental is Car car)
    {
        // Access car-specific properties
    }
}
foreach (var rental in rentals)
{
    if (rental is Truck truck)
    {
        Console.WriteLine($"Truck Style: {truck.Style}, Passengers: {truck.NumberOfPassengers}");
    }
    else if (rental is Sailboat sailboat)
    {
        // Access sailboat-specific properties
    }
    else if (rental is Car car)
    {
        // Access car-specific properties
    }
}

Flexibility with Interfaces

Tim emphasizes that using interfaces provides flexibility for future changes. For instance, adding new types of rentals, like tanks or TVs, would not disrupt the existing structure.

Avoiding Overuse of Inheritance

Tim advises against overusing inheritance for code sharing, as it can lead to a convoluted and inflexible codebase. Instead support multiple inheritance, he recommends leveraging interfaces and composition to achieve the desired outcomes without stretching the "is-a" relationship beyond its logical bounds.

Conclusion

Tim Corey’s explanation of inheritance and interfaces in OOP offers a clear pathway to creating maintainable and flexible code. By showcasing common pitfalls and providing a refined design with interfaces, he ensures developers can make informed decisions about structuring their applications effectively.

For a deeper dive into these concepts and to see them in action, watch Tim's complete video. His channel is a goldmine of programming tutorials. Don't miss out!