In this article, we are going to explore how to recognize and refactor a code smell called object-orientation abusers in C#.

To download the source code for this article, you can visit our GitHub repository.

Let’s start!

What Are Object-Orientation Abusers?

Object-orientation abuse is a code smell that describes the misuse or excessive use of object-oriented programming. We may recognize it when we see concepts like inheritance or encapsulation used in a way that produces code difficult to maintain. It may also refer to the incorrect use of object-oriented design paradigms.

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!

In this article, we will discuss four issues that may indicate this code smell, which may apply to:

  • Switch statement
  • Temporary field
  • Refused bequest
  • Alternative classes with different interfaces

Switch Statements

Using a switch statement is just fine, but sometimes it can lead to troubles. Large, complicated code that does different things depending on the condition met is prone to bugs. Not to mention readability. The same applies to complicated if statements, of course.

Fortunately, there are object-oriented programming techniques that can make our code better. 

Replace Conditional With Polymorphism

Let’s assume we have a complex switch statement. What the code will do may depend on the class of the object, an interface that the class implements, or a value of a property:

public class Plane
{
    public const int Cargo = 0;
    public const int Passenger = 1;
    public const int Military = 2;

    public int Type { get; private set; }

    public Plane(int type)
    {
        Type = type;
    }

    public double GetCapacity()
    {
        switch (Type)
        {
            case Cargo:
                return GetCargoPlaneCapacity();
            case Military:
                return GetMiliratyPlaneCapacity();
            case Passenger:
                return GetPassengerPlaneCapacity();
            default:
                throw new ArgumentOutOfRangeException($"Incorrect value: {Type}");
        }
    }
}   

This solution is against the principles of object-oriented programming. The Tell-Don’t-Ask principle says not to ask an object about its current state in order to perform an action. Instead, we should tell the object what should happen.

Let’s do it by creating an abstract Plane class:

public abstract class Plane
{
    public const int Cargo = 0;
    public const int Passenger = 1;
    public const int Military = 2;

    public abstract int Type { get; }
    public abstract double GetCapacity();

    public static Plane Create(int type)
    {
        return type switch
        {
            Cargo => new CargoPlane(),
            Passenger => new PassengerPlane(),
            Military => new MilitaryPlane(),
            _ => throw new ArgumentOutOfRangeException($"Incorrect value: {type}")
        };
    }
}

Now we can create a suitable subclass that inherits the abstract class:

public class CargoPlane : Plane
{
    public override int Type { get { return Cargo; } }

    public override double GetCapacity()
    {
        return 10000;
    }
}

Thanks to this procedure, when we want to know the aircraft’s capacity, we just need to call the GetCapacity() method.

An additional advantage is when adding a new type of aircraft, we don’t have to modify any type of business logic in the Plain class but only add a new subclass.

Temporary Field

The code smell refers to a situation in which a class has a field or a bunch of fields that are only temporarily needed. This field frequently receives a value, is put to use in a computation, and is subsequently deleted, or it is simply used to keep track of interim findings.

Such fields point to a lack of cohesiveness in the class design, which can make the code more difficult to comprehend and maintain while also expanding the application’s memory footprint. This is because, although only being required briefly, the temporary field is likely to remain in memory during the object’s existence.

There are several ways to improve this code smell. If possible, we can just use local variables inside a method that performs some operation. After executing the code, they will be deleted from memory.

Let’s take a look at the BookstoreCustomer class, where we define a method to get the amount of the discount based on the number of books previously purchased:

public class BookstoreCustomer
{
    private int _discountThreshold;
    private int _boughtBooksCount;
    public ICollection<string> BoughtBooks { get; set; }

    public double GetDiscountRate()
    {
        _boughtBooksCount = BoughtBooks.Count;
        _discountThreshold = GetDiscountThreshold();
        var discountBase = 0.05;

        return _discountThreshold * discountBase;
    }

    private int GetDiscountThreshold()
    {
        var firstThreshold = 1;
        var secondThreshold = 2;

        return _boughtBooksCount < 10 ? firstThreshold : secondThreshold;
    }
}

Let’s pay particular attention to the boughtBooksCount and the discountThreshold fields that we added to the class and only used in the GetDiscountRate() method. As soon as we execute this method, the added fields will become useless.

Let’s try to get rid of unnecessary fields with the usage of local variables:

public class BookstoreCustomer
{
    public ICollection<string> BoughtBooks { get; set; }

    public double GetDiscountRate()
    {
        var boughtBooksCount = BoughtBooks.Count;
        var discountThreshold = GetDiscountThreshold(boughtBooksCount);
        var discountBase = 0.05;

        return discountThreshold * discountBase;
    }

    private int GetDiscountThreshold(double boughtBooksCount)
    {
        var firstThreshold = 1;
        var secondThreshold = 2;

        return boughtBooksCount < 10 ? firstThreshold : secondThreshold;
    }
}

Now the BookstoreCustomer class no longer contains redundant fields, and local variables will be purged from memory when they are no longer needed.

However, if for some reason this is not possible, we can always create a new class consisting only of temporary fields. We can also put a method that uses all those variables inside this class. 

Refused Bequest

The term bequest denotes the action of transferring personal property through a will. It means that someone can give some of their belongings to another person. The other person has the option to reject that bequest if they do not wish to receive it.

In the domain of object-oriented programming, a subclass can reject a bequest when it inherits fields and methods that it doesn’t require. Those rejected fields and methods are object-orientation abusers.

Let’s look at a code example showing inheritance for some animals:

public class Animal
{
    public void Eat()
    {
        Console.WriteLine("Eating...");
    }

    public void Sleep()
    {
        Console.WriteLine("Sleeping...");
    }
}

public class Bird : Animal
{
    public void Fly()
    {
        Console.WriteLine("Flying...");
    }
}

The Animal class contains two methods, Eat() and Sleep(). Then we have a Bird class, that inherits from Animal and has one additional method – Fly()

Now we would like to add a specific species of bird. Let’s add a penguin. It’s a bird, so this class should inherit from Bird, right?

public class Penguin : Bird 
{ 
    public void Swim() 
    { 
        Console.WriteLine("Swimming..."); 
    } 
}

It sounds quite logical, but there is a problem – penguins cannot fly. Therefore, a Penguin class inheriting from a Bird class would contain a redundant method that shouldn’t be used. This is the perfect example of a refused bequest code smell.

To improve the code, the Penguin class should not inherit from the Bird class:

public class Animal
{
    public void Eat()
    {
        Console.WriteLine("Eating...");
    }

    public void Sleep()
    {
        Console.WriteLine("Sleeping...");
    }
}

public class Bird : Animal
{
    public void Fly()
    {
        Console.WriteLine("Flying...");
    }
}

public class Penguin : Animal
{
    public void Swim()
    {
        Console.WriteLine("Swimming...");
    }
}

In some cases, a good way to deal with this problem may also be to isolate a class in between that all flying birds will inherit from.

Replace Inheritance With Delegation

There are also cases where inheritance doesn’t make much sense. In some places, programmers use it more for convenience than for logical reasons and that can make it one of the object-orientation abusers.

Let’s look at an example of an incorrectly used inheritance. First class we need for that purpose is Paycheck, which holds information about employee’s salary: 

public class Paycheck
{
    private readonly decimal _hourlyRate;
    private readonly int _hoursWorked; 
    private readonly decimal _overtimeRate;

    public Paycheck(decimal hourlyRate, int hoursWorked)
    {
        _hourlyRate = hourlyRate;
        _hoursWorked = hoursWorked;
        _overtimeRate = 1.5M * hourlyRate;
    }

    public decimal CalculatePay()
    {
        return _hourlyRate * _hoursWorked;
    }

    public decimal GetOvertimeRate() 
    {
        return _overtimeRate;
    }
}

We also have an Employee class, which inherits from the Paycheck:

public class Employee : Paycheck
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Email { get; set; }

    public Employee(decimal hourlyRate, int hoursWorked)
        : base(hourlyRate, hoursWorked)
    {
    }
}

As a result of this inheritance it is easy to get the amount of the employee’s pay by calling the CalculatePay() method on the Employee object. However, this is logically incorrect, and what’s more? The Employee class, unfortunately, implements all the fields and methods defined in the Paycheck class.

In such cases, it is best to use the replace inheritance with delegation pattern. In this method we want to put the superclass object in a newly created field and delegate the method to the superclass object:

public class Employee
{
    private readonly Paycheck _paycheck;
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Email { get; set; }

    public Employee(Paycheck paycheck)
    {
        _paycheck = paycheck;
    }

    public decimal CalculatePay()
    {
        return _paycheck.CalculatePay();
    }
}

Thanks to the use of delegation, the Employee class does not contain all unnecessary methods and fields of the Paycheck class. This code structure is more logical and leads to fewer bugs and it’s clean from any object-orientation abusers.

Alternative Classes With Different Interfaces

This code smell arises when two classes are similar on the inside but distinct on the exterior. In other words, this implies that the code is similar or nearly the same but the method name or method signature is somewhat different.

Using the Dog and Cat classes as an example, we can see that both classes implement animal sounds:

public class Dog
{
    public string Name { get; set; }

    public void Bark()
    {
        Console.WriteLine($"{Name} makes a sound.");
    }
}

public class Cat
{
    public string Name { get; set; }

    public void Meow()
    {
        Console.WriteLine($"{Name} makes a sound.");
    }
}

According to the principle of Don’t Repeat Yourself, we would like to have only one method. If they do the same, then one may be redundant. We can achieve this by creating an abstract class or an interface, and letting these classes extend or implement it:

public abstract class Animal
{
    public string Name { get; set; }
    public abstract void MakeSound();
}

public class Dog : Animal
{
    public override void MakeSound()
    {
        Console.WriteLine($"{Name} barks.");
    }
}

public class Cat : Animal
{
    public override void MakeSound()
    {
        Console.WriteLine($"{Name} meows.");
    }
}

The Animal class method might be abstract, requiring full implementation by Dog and Cat classes, or it can be a regular method with a default implementation. The goal is to arrange and connect relevant classes so that they are not completely isolated.

Conclusion

In this article, we examined numerous strategies for refactoring object-oriented abusers in C#. Understanding these approaches will help to prevent the misuse or excessive use of object-oriented programming in the future.

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!