DEV Community

Cover image for How to write an object oriented program that doesn't suck
Naveen
Naveen

Posted on

How to write an object oriented program that doesn't suck

It is fairly obvious that object oriented program has been used as a silver bullet in the modern day programming. Whether you are working as a technical architect or a computer science student, merits of OOP are highly regarded.

I have seen many computer programmers proudly proclaim “Yup, I designed my code in an object oriented way – I have defined my data members as private, they can be only accessed through public methods – I have created base classes and sub-classes that follow a hierarchy – I have created virtual methods in the base class that shares common behavior that can be used in derived classes.” Is that what object orientation is all about? There is definitely more to it.

First of all, we need to understand the purpose behind using object oriented programming. Apart from small throw-away programs that are written to perform a single task and run only once, complex software applications will always require more changes. Your source code needs a regular update either to add new features to the system or to fix bugs. Object oriented programming organizes your code to make the change easier. In addition to that, object oriented programming allows the breaking bigger and complex problems into smaller and reusable modules which provide a consistent method for managing the large code base.

Most of the programmers think that they understand the foundation of the object orientation. When it comes to applying it to real world applications, they struggle.

Most of us suck at object oriented programming – and that's okay. All it takes to become good at anything is to have patience, practice and a little faith in yourself. Object orientation is not only a programming technique but it is a shift in thinking which requires a lot of practice and good design.

How to practice?

I wrote an article about Procrastinating the procrastination in Medium where I explained how to start with something small. Trying to learn object oriented programming initially may look intimidating and overwhelming. Instead of over-complicating and trying to learn everything at once, try taking small steps at a time and try to understand the motive behind applying object orientated design to your code. By practicing how to design a system, you will start to get a natural feel of object relationships. Then, translate it to code. You will start seeing common areas that can be abstracted, modularized and other ways that will improve both your design and code.

How to figure out if my code sucks?

Most of our codes are not perfect in many ways and it will take ages to list all the possible flaws. However, I will list some of the most common and frequent mistakes and give some suggestions to fix them.

  • Adding more functionalities or responsibilities to a class that is not related. Consider the below example
class Account
{    
   public void Withdraw(decimal amount)    
   {        
      try        
      {            
          // logic to withdraw money       
      }        
      catch (Exception ex)        
      {            
          System.IO.File.WriteAllText(@"c:\logs.txt",ex.ToString());   
      }    
   }
}

Can you figure out the problem in the above code? Account class has taken additional responsibility of adding exceptions to the log file. In future, if there's a requirement to change the logging mechanism, the Account class needs to be changed. Well, this doesn't seem to be right.

Fix:

A class has to take only one responsibility and there should be only one reason to change the class. This can be fixed by moving the logging activity to a separate class which takes care of just logging exceptions and writing it to a log file.

public class Logger
{
   public void Handle(string error)
   {
       System.IO.File.WriteAllText(@"c:\logs.txt", error);
   }
 }

Now the Account class has the flexibility to delegate the logging activity to the Logger class and it can focus only on Account related activities.

class Account
{
   private Logger obj = new Logger();    
   public void Withdraw(decimal amount)    
   {        
      try        
      {            
          // logic to withdraw money       
      }        
      catch (Exception ex)        
      {            
          obj.Handle(ex.ToString());
      }    
   }
}
  • Consider there is a new requirement to handle different account types like savings account and current account. To cater to this, I am adding a property to the Account class called “AccountType”. Depending on the type of the account, the interest rate differs too. I am writing a method to calculate interest d on the account type.
class Account
{
   private int _accountType;

   public int AccountType
   {
      get { return _accountType; }
      set { _accountType = value; }
   } 
   public decimal CalculateInterest(decimal amount)    
   {        
      if (_accountType == "SavingsAccount")
      {
         return (amount/100) * 3;
      }
      else if (_accountType == "CurrentAccount")
      {
         return (amount/100) * 2;
      }
   }
}

Problem with the above code is “branching”. If there's a new account type in the future, again Account class has to be altered to cater the requirement which linearly increases the number of if-else condition to the code. Note that we are changing the Account class for every change. This clearly is not an extensible code.

Fix:

It is always a good practice to open the code for extension and close for modification. Can we try extending the code by adding a new class whenever a new account type needs to be added and inherit it from Account class? By doing so, we are not only abstracting the Account class but also allowing it to share common behavior across it's child classes.

public class Account
{
   public virtual decimal CalculateInterest(decimal amount)
   {
       // default 0.5% interest
       return (amount/100) * 0.5;
   }
}
public class SavingsAccount: Account
{
   public override decimal CalculateInterest(decimal amount)
   {
       return (amount/100) * 3;
   }
}
public class CurrentAccount: Account
{
   public override decimal CalculateInterest(decimal amount)
   {
       return (amount/100) * 2;
   }
}
  • Stay with me, this gets more interesting from now – Say the business is coming up with new account type called Online Account. This account is having a good interest rate of 5% and holds all the benefits similar to the savings bank account. However, you cannot withdraw cash from the ATM. You can only wire transfer money. Implementation looks like below
public class Account
{
   public virtual void Withdraw(decimal amount)
   {
       // base logic to withdraw money
   }
   public virtual decimal CalculateInterest(decimal amount)
   {
       // default 0.5% interest
       return (amount/100) * 0.5;
   }
}
public class SavingsAccount: Account
{
   public override void Withdraw(decimal amount)
   {
        // logic to withdraw money
   }
   public override decimal CalculateInterest(decimal amount)
   {
       return (amount/100) * 3;
   }
}
public class OnlineAccount: Account
{
   public override void Withdraw(decimal amount)
   {
        throw new Exception("Not allowed");
   }
   public override decimal CalculateInterest(decimal amount)
   {
       return (amount/100) * 5;
   }
}

Now, consider that I wanted to close all my accounts with this bank. So, all the money from each account has to be taken out before closing the account completely.

public void CloseAllAccounts()
{
    // Retrieves all accounts related to this customer
    List<Account> accounts = customer.GetAllAccounts();
    foreach(Account acc in accounts)
    {
      // Exception occurs here
      acc.Withdraw(acc.TotalBalance);
    }
}

As per inheritance hierarchy, the Account object can point to any one of its child object. No unusual behavior is noticed during compile time. However, during the run-time, it throws an exception “Not allowed”. What did we infer from this? The parent object cannot seamlessly replace the child object.

Fix:

Let's create 2 interfaces – one to process interest (IProcessInterest) and other one to process withdrawal (IWithdrawable)

interface IProcessInterest
{
    decimal CalculateInterest(decimal amount);
}
interface IWithdrawable
{
    void Withdraw(double amount);
}

The OnlineAccount class will only implement IProcessInterest while the Account class will implement both IProcessInterest and IWithdrawable.

public class OnlineAccount: IProcessInterest
{
    public decimal CalculateInterest(decimal amount)
    {
       return (amount/100) * 5;
    }
}
public class Account: IProcessInterest, IWithdrawable
{
   public virtual void Withdraw(decimal amount)
   {
       // base logic to withdraw money
   }
   public virtual decimal CalculateInterest(decimal amount)
   {
       // default 0.5% interest
       return (amount/100) * 0.5;
   }
}

Now, this looks clean. we can create a List of IWithdrawable and add relevant classes to it. In case if a mistake is made by adding OnlineAccount to the list inside the GetAllAccounts method, we will get a compile time error.

public void CloseAllAccounts()
{
    // Retrieves all withdrawable accounts related to this customer
    List<IWithdrawable> accounts = customer.GetAllAccounts();
    foreach(Account acc in accounts)
    {
      acc.Withdraw(acc.TotalBalance);
    }
}
  • Assume that our Account class is in super demand. Business is proposing an idea to expose an API which allows to withdraw money from different third party bank's ATM. We have exposed a web service and every other bank started consuming the web service to withdraw money. Everything sounds good till now. After couple of months, the business is coming up with another requirement saying that some other banks are requesting to set the withdrawal limit to this account from their ATM as well. No problem, raise a change request – it can be done easily.
interface IWithdrawable
{
   void Withdraw(decimal amount);
   void SetLimit(decimal limit);
}

What we just did was bizarre. By changing the existing interface, you are adding a breaking change and disturbing all the satisfied banks those were really happy consuming our web service to just withdraw. Now you are forcing them to use the newly exposed method as well. Not good at all.

Fix:

The best way to fix this is to create a new interface rather than modifying the existing interface. The current interface IWithdrawable can be as it is and a new interface – say, IExtendedWithdrawable is created which implements IWithdrawable.

interface IExtendedWithdrawable: IWithdrawable
{
   void SetLimit(decimal limit);
}

So, the old clients will continue using IWithdrawable and the new clients can use IExtendedWithdrawable. Simple, but effective!

  • Let's go back to the first issue we resolved where we added Logger class to delegate the responsibility of logging from Account class. It logs exceptions to a file. Sometimes it's easy to get the log files through email or to integrate it with some third party log viewer for ease of access. Let's implement this.
interface ILogger
{
    void Handle(string error);
}
public class FileLogger : ILogger
{
   public void Handle(string error)
   {
       System.IO.File.WriteAllText(@"c:\logs.txt", error);
   }
}
public class EmailLogger: ILogger
{
   public void Handle(string error)
   {
       // send email
   }
}
public class IntuitiveLogger: ILogger
{
   public void Handle(string error)
   {
       // send to third party interface
   }
}
class Account : IProcessInterest, IWithdrawable
{
   private ILogger obj;    
   public void Withdraw(decimal amount)    
   {        
      try        
      {            
          // logic to withdraw money       
      }        
      catch (Exception ex)        
      {            
          if (ExType == "File")
          {
             obj = new FileLogger();
          }
          else if(ExType == "Email")
          {
             obj = new EmailLogger();
          }
          obj.Handle(ex.Message.ToString());
      }    
   }
}

We wrote an excellent extensible code – ILogger which acts as a common interface to other logging mechanisms. However, we are again violating the Account class by giving more responsibility to decide which instance of the Logging class to be created. It is not the job of Account class to decide on object creation for logging.

Fix:

The solution is to “Invert the dependency” to some other class rather than Account class. Let's inject the dependency through the constructor of Account class. So, we are pushing the responsibility to the client to decide which logging mechanism that has to be applied. The client might in-turn depend on the application configuration settings. We may not worry about configuration level understanding in this article at-least.

class Account : IProcessInterest, IWithdrawable
{
   private ILogger obj;
   public Customer(ILogger logger)
   {
       obj = logger;
   }
}
// Client side code
IWithdrawable account = new SavingsAccount(new FileLogger());

If you have closely paid attention, you might have easily guessed that all these suggested fixes are nothing but the applications of SOLID principles of object oriented design. I will leave it to you to explore and analyze how exactly I applied every aspect of these design principles to the code.

If you believe like I do, that the object oriented programming is a shift in thinking, kindly share this article so that others can learn from it.

This article was originally posted in my Medium blog

Top comments (16)

Collapse
 
thedeemon_lj profile image
Dmitry Popov

In _accountType == "SavingsAccount" you're comparing an int with a string. Just saying. ;)

Collapse
 
mhmd_azeez profile image
Muhammad Azeez

Great article, one thing though: ExType is not defined in the multiple logger types example

Collapse
 
rrconstantin profile image
rrconstantin

Some people (myself included) say that logging also pollutes your class and violates srp. That's why I switched to AOP logging.

Collapse
 
postsrc profile image
PostSrc

This article is really SOLID :D

for the last part Customer constructor should be Account right?

Collapse
 
lluismf profile image
Lluís Josep Martínez

Using obj as a variable name for the Loggers is bad.

FIX : rename it to logger

Collapse
 
aqeelse profile image
Aqeel Ahmad

Nice explanation of SOLID principles with good examples.

Collapse
 
stefandorresteijn profile image
Stefan Dorresteijn • Edited

If you're really trying to learn about OOP, read Sandi Metz's Practical Object-Oriented Design in Ruby and 99 Bottles of OOP. They'll teach you solid OOP.

Collapse
 
theavuthnhel profile image
Theavuth NHEL

Good Idea (Y)

Collapse
 
imthedeveloper profile image
ImTheDeveloper

Really enjoyed reading this as I'm looking into patterns and code structure. The usage of real world examples and requests from the business are excellent.

Collapse
 
elwinbran profile image
ElwinBran

This article is a fine introduction to the ideas of maintainable OOD and OOP.

Collapse
 
djubreel profile image
Jubril Edu

Awesome write-up

Collapse
 
samizan profile image
Samer Azani

Cool solid examples :)

Collapse
 
simplymanas profile image
Manas Ranjan Dash

Good one for every level..

Collapse
 
bmontgomes profile image
Bruno Gomes

That was a pretty solid read! (no pun intended) Your examples were great. :)

Collapse
 
gordeevd profile image
Dmitry Gordeev

neat