Introduction
SOLID principles help developers design object-oriented code that is easier to maintain, test, extend, and modify.
However, identifying a SOLID violation is not always as simple as checking whether a class contains multiple methods. The real question is how responsibilities and reasons for change are distributed across the design.
Consider the following Account class:
public class Account
{
public decimal Balance { get; set; }
public decimal CalculateInterest(string accountType)
{
decimal interest = 0;
if (accountType.Equals("Regular"))
{
interest = Balance * 0.4m;
if (Balance < 1000)
interest -= Balance * 0.2m;
else if (Balance < 50000)
interest += Balance * 0.4m;
}
else if (accountType.Equals("Salary"))
{
interest = Balance * 0.5m;
}
return interest;
}
}
At first glance, the class looks simple. However, as more account types and interest rules are added, the design becomes increasingly difficult to maintain.
This article examines the problems in this code and demonstrates two ways to improve the design:
Polymorphism using inheritance.
The Strategy Pattern using composition.
What Issues Exist in the Code?
The main design problems are:
Account-specific behavior is controlled by string values.
CalculateInterest()contains multiple business rules.Adding another account type requires modifying existing code.
The class mixes account state with account-type-specific interest calculation.
String comparisons are fragile and can lead to errors.
The calculation logic becomes harder to test independently as the number of account types increases.
The two SOLID principles most relevant to this example are:
Single Responsibility Principle (SRP)
Open/Closed Principle (OCP)
The other SOLID principles are not directly demonstrated by this particular example.
Single Responsibility Principle
The Single Responsibility Principle states that a class should have one reason to change.
The important part is the phrase "reason to change."
The original Account class contains:
public decimal Balance { get; set; }
which represents account state, and:
public decimal CalculateInterest(string accountType)
which contains business rules for different account types.
If the account's data model changes, the class may need to change.
If the interest rules for regular accounts change, the same class also needs to change.
If salary-account rules change, the class needs to change again.
This creates multiple potential reasons for modification.
Example
Suppose the business changes the regular-account interest calculation.
The following code must be modified:
if (accountType.Equals("Regular"))
{
interest = Balance * 0.4m;
// ...
}
Now suppose the salary-account calculation also changes.
The same class must be modified again.
As the number of account types grows, more business rules accumulate inside Account.
This is a sign that the responsibilities could be separated.
Open/Closed Principle
The Open/Closed Principle states that software entities should be:
Open for extension.
Closed for modification.
The original implementation violates this principle because adding a new account type requires modifying CalculateInterest().
For example, suppose the application introduces a Premium account.
We would need to add another condition:
else if (accountType.Equals("Premium"))
{
interest = Balance * 0.7m;
}
The existing class has now been modified to support a new business rule.
If another account type is added later, the method must be modified again.
As the number of account types grows, this can result in a large conditional method.
Why String-Based Account Types Are a Problem
The method accepts:
string accountType
This means the behavior depends on specific string values:
Regular
Salary
Premium
String-based type selection has several disadvantages.
For example:
CalculateInterest("regular");
and:
CalculateInterest("Regular");
are different strings.
A typo can also result in unexpected behavior:
CalculateInterest("Regualr");
The compiler cannot detect this mistake.
Polymorphism or a strategy-based design moves the behavior into types instead of relying on string comparisons.
Approach 1: Using Polymorphism
One way to improve the design is to use inheritance and polymorphism.
Create an abstract base class:
public abstract class Account
{
public decimal Balance { get; set; }
public abstract decimal CalculateInterest();
}
The base class contains the common account state while leaving interest calculation to derived classes.
Create a Regular Account
public class RegularAccount : Account
{
public override decimal CalculateInterest()
{
decimal interest = Balance * 0.4m;
if (Balance < 1000)
{
interest -= Balance * 0.2m;
}
else if (Balance < 50000)
{
interest += Balance * 0.4m;
}
return interest;
}
}
The regular-account rules are now isolated from other account types.
Create a Salary Account
public class SalaryAccount : Account
{
public override decimal CalculateInterest()
{
return Balance * 0.5m;
}
}
The salary-account rules are now contained within SalaryAccount.
Adding a New Account Type
Suppose a new premium account is required.
We can add another class:
public class PremiumAccount : Account
{
public override decimal CalculateInterest()
{
return Balance * 0.7m;
}
}
The existing RegularAccount and SalaryAccount classes do not need to be modified.
This demonstrates the Open/Closed Principle more clearly.
Using the Polymorphic Classes
The calling code can work with the base type:
Account account = new RegularAccount
{
Balance = 5000
};
decimal interest = account.CalculateInterest();
Console.WriteLine($"Interest: {interest}");
The correct implementation is selected through polymorphism.
There is no need to pass:
"Regular"
to the method.
Advantages of the Polymorphism Approach
This approach provides several benefits:
Removes account-type string comparisons.
Keeps account-specific rules together.
Makes adding account types straightforward.
Makes individual account implementations easier to test.
Uses standard object-oriented polymorphism.
However, inheritance is not always the best choice.
The important design question is whether the interest calculation is actually a fundamental type of account behavior or whether it is a behavior that should be interchangeable independently of the account.
For the latter case, the Strategy Pattern can be a better fit.
Approach 2: Using the Strategy Pattern
The Strategy Pattern separates an algorithm or behavior from the object that uses it.
In this example, the behavior that varies is interest calculation.
Instead of making every interest rule a subclass of Account, we can create an interface:
public interface IInterestCalculator
{
decimal CalculateInterest(decimal balance);
}
The interface defines what every interest-calculation strategy must implement.
Create the Regular Account Strategy
public class RegularAccountInterestCalculator : IInterestCalculator
{
public decimal CalculateInterest(decimal balance)
{
decimal interest = balance * 0.4m;
if (balance < 1000)
{
interest -= balance * 0.2m;
}
else if (balance < 50000)
{
interest += balance * 0.4m;
}
return interest;
}
}
The regular-account interest rules now have their own class.

Join the conversation! Your thoughts help the community grow.