In this article, we are going to explore how to recognize and refactor a code smell called object-orientation abusers in C#.
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.
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.