Share via


C#: Usage of Interfaces and Abstract Classes in Real World (and Difference between them)

In job discussions, many interviewers asked the same question regardless of your experience in Software Engineering.  

  • What are Interfaces and Abstract Classes?"
  • When and how to use them?"

Pretty basic question, yet this is one of the fundamental concepts of Object Oriented Programming. This make you reflect that why so many hiring managers were asking the same question, even to an experienced Software Engineer. Now, if we keep our engineering ego aside for the moment, and reflect that why hiring managers are asking this question, we can see there are actually many reasons behind it.

But one of the key reasons being, Interfaces and Abstract Classes are not just simple fundamental concepts in OOP but many software developers regardless of having experience in software development or not, can have a misconception regarding Interfaces and Abstract Classes, and how to use them.

This being said, I didn’t know the truth behind these two concepts till I started developing software as a professional myself. So if you are one of the developers who want to revise your knowledge or learn the truth of these two concepts then follow along. We will try to figure this out using a real-world software development scenario (almost real).

The Real World Implementation Scenario

To discuss the usage of Interfaces and Abstract Classes we will develop a simple console app where Customer Details (Name, Country Region, Account Types) will be printed out on the console. This app can be programmed in several ways and how we program is not the focus of this article. The focus is to see how we can use Interface and Abstract Class using an industrial application development as the medium.

Establish: Differences between Interface and Abstract Class

Before we look at each implementation in details, let’s first establish the differences between the two: Interface and Abstract Class [forums]:

Interface

Abstract Class

A class may inherit several interfaces

A class may inherit only one abstract class

An interface cannot provide any code, just the signature

An abstract class can provide complete, default code and/or just the details that have to be overridden

An interface cannot have access modifiers for the subs, functions, properties, etc. everything is assumed as public

An abstract class can contain access modifiers for the subs, functions, properties

Interfaces are used to define the peripheral abilities of a class

An abstract class defines the core identity of a class and there it is used for objects of the same type

If we add a new method to an Interface then we have to track down all the implementations of the interface and define implementation for the new method

If we add a new method to an abstract class then we have the option of providing default implementation and therefore all the existing code might work properly

So what is the key difference between these two?

Well, the key difference are (not is):

1)  A class can inherit multiple Interfaces, whereas, a class can only inherit one Abstract class.

2)  You have to implement all the member methods in all the implementation of interfaces, whereas, for abstract class you don’t have to.

Now, let’s get our hand dirty and use these two concepts in our console app.

Business Logic For The Console App

The console app needs to print out details (Name, Country Region, Account Types) of Customers but should follow these business rules:

  • Bank Accounts can only be of two types: Current and Savings
  • There can only be two types of Customers: Residential and Non-Residential
  • Residential Customers can open and maintain both Current and Savings Accounts
  • Non-Residential Customers can only have Savings Accounts
  • All the Customers of the Bank are European national only

The Development of Console App Begins

Let's create an interface named BankAccountInterface which reflects the skeleton for how a class Account of Customer should look like. You see, Interface can be treated as a contract, which confirms the minimum functions a class should have. Now, let's see the code for this Interface.

Creating The Skeleton (I mean Interface)

using System;
 
namespace Usage_of_Interfaces_and_Abstract_Classes
{
 public interface  BankAccountInterface
 {
 int GetAccountType();
 string GetAccountName();
 }
}

From now, any class we create, which inherits from this Interface has to implement the methods GetAccountType() & GetAccountName(). If not, then during the build of the project, the compiler will throw an error. We will see what that error looks like later in this section.

Now, in our app we are considering two types of Bank Account: Current Account and Savings Account. So, go ahead and create two Classes with the names CurrentAccount and SavingsAccount respectively. The code is as follows:

// Notice these two classes inherits the Interface
 
public class  CurrentAccount: BankAccountInterface
 {
 // Constructor
 public CurrentAccount()
 { }
 
 // Implementing the method GetAccountType() of BankAccountInterface
 public int  GetAccountType()
 {
 return 1;
 }
 
 // Implementing the method GetAccountName() of BankAccountInterface
 public string  GetAccountName()
 {
 return "Current Account";
 }
 }
 
public class  SavingsAccount: BankAccountInterface
 {
 //Constructor
 public SavingsAccount()
 { }
 
 // Implementing the method GetAccountType() of BankAccountInterface
 public int  GetAccountType()
 {
 return 2;
 }
 
 // Implementing the method GetAccountName() of BankAccountInterface
 public string  GetAccountName()
 {
 return "Savings Account";
 }
 }

In each of these classes if you do not implement both the methods specified in the Interface then you would end up with an error such as:

[Class Name] does not implement interface member [MethodName()]

Creating The Idea To Be Turned Into Reality Later (I mean Abstract Class)

 Many times, the structure of Bank Account types doesn't really change and remains almost the same over the decades. But Customer type can change especially given the Global Expansion and increasing accessibility of technology we can see various types of Customer.

Hold on a minute! We have a good news. Although Customer Types may change over time but at the end of the day, every customer is a human being (so far) with some basic common traits. So let's create an Abstract Class of Customer, named CustomerAbstractClass, with some of those common traits. 

The code for this as follows:

public abstract  class CustomerAbstractClass
 {
 // Declare the GetCustomerType() default method here
 public abstract  int GetCustomerType();
 
 // Implement the GetCustomerTypeName() default method here
 public abstract  string GetCustomerTypeName();
 
 // Implement the GetCustomerTypeName() default method here
 public virtual  string GetCountryRegion()
 {
 return "UK/EU";
 }
 }

Now, I am not going into details how to define an Abstract Class, which method should be Abstract and what it means. I assume you are already aware of the terms such as abstract, virtual, etc. For more details please refer to the References section to get more details on Abstract Class.

In our implementation of CustomerAbstractClass, the reason GetCustomerType() and GetCustomerTypeName() methods are defined as abstract because I want to implement them in the derived class so that I can reflect the Customer type and type name based on the class they belong to. The reason GetCountryRegion() is not abstract is because this can remain the same for any type of Customers since all our Customers are European nationals. But this method is still marked as virtual because in case I have to modify the method in a derived Class, then I can do that easily by overriding the method.

Let's now create the two types of basic Customer Type at the moment which inherits from the Customer Abstract Class. For European nationals we simply call them Customer and for non-residentials we simply call them NonResidentialCustomer. The code is as follows:

// Customer derived from CustomerAbstractClass
public class  Customer: CustomerAbstractClass
 {
 private string  CustomerName;
 
 
 // Constructor
 public Customer(string CustomerName) 
 {
 this.CustomerName = CustomerName;
 }
 
 // Get Customer Name
 public string  GetName()
 {
 return CustomerName;
 }
 
 // Using override to return the already 
 //implemented method, GetCustomerType() of Base class, which is CustomerAbstractClass
 public override  int GetCustomerType()
 {
 return 1;
 }
 
 // Using override to return the already 
 //implemented method, GetCustomerTypeName() of Base class, which is CustomerAbstractClass
 public override  string GetCustomerTypeName()
 {
 return "Citizen";
 }
 
 // Returns the list of account types available
 public List<BankAccountInterface> GetAccounts()
 {
 List<BankAccountInterface> accounts = new  List<BankAccountInterface>();
 
 var currentAccount = new  CurrentAccount();
 var savingsAccount = new  SavingsAccount();
 
 accounts.Add(currentAccount);
 accounts.Add(savingsAccount);
 
 return accounts;
 }
 }
 
// NonResidentialCustomer derived from CustomerAbstractClass
public class  NonResidentialCustomer: CustomerAbstractClass
 {
 private string  CustomerName;
 
 
 // Constructor
 public NonResidentialCustomer(string CustomerName) 
 {
 this.CustomerName = CustomerName;
 }
 
 // Get Customer Name
 public string  GetName()
 {
 return CustomerName;
 }
 
 // Using override to return the
 // method, GetCustomerType() of Base class, which is CustomerAbstractClass
 public override  int GetCustomerType()
 {
 return 2;
 }
 
 // Using override to return the 
 // method, GetCustomerTypeName() of Base class, which is CustomerAbstractClass
 public override  string GetCustomerTypeName()
 {
 return "Non Residential";
 }
 
 // Using override to return the already 
 // implemented method, GetCountryRegion() of Base class, which is CustomerAbstractClass
 public override  string GetCountryRegion()
 {
 return "Non Residential in "  + base.GetCountryRegion();
 }
 
 // Returns the list of account types available
 public List<BankAccountInterface> GetAccounts()
 {
 List<BankAccountInterface> accounts = new  List<BankAccountInterface>();
 
 var savingsAccount = new  SavingsAccount();
 
 accounts.Add(savingsAccount);
 
 return accounts;
 }
 }

As you can see from the aforementioned code that the derived class Customer implemented the GetCustomerType() and GetCustomerTypeName() but not the GetCountryRegion() method of the base class CustomerAbstractClass, whereas, NonResidentialCustomer Class have implemented all the methods of the base class. If it was interface then we would have to implement all three methods in all the derived Classes as mentioned earlier.

We are ready! We have successfully created our Abstract Class and Interface as expected. Now, all we have to do is run some method to populate the details of different Customers. But first we have to create and populate some records of different Customers of the Bank to test.

Use the code below in the Main method of your Program.cs as follows:

01.static void  Main(string[] args)
02. {
03. Customer customer1 = new  Customer("Jon Skeet");
04. NonResidentialCustomer customer2 = new  NonResidentialCustomer("Somdip Dey");
05. 
06. // Priting our details of Customer No. 1
07. Console.WriteLine("Details of Customer No. 1::");
08. Console.WriteLine("Customer Name: " + customer1.GetName());
09. Console.WriteLine("Type: " + customer1.GetCustomerType());
10. Console.WriteLine(Environment.NewLine + "Available Accounts:");
11.  
12. foreach(BankAccountInterface account in  customer1.GetAccounts())
13. {
14. StringBuilder stringBuilder = new  StringBuilder();
15. stringBuilder.AppendLine("Account Type: " + account.GetAccountType());
16. stringBuilder.AppendLine("Account Name: " + account.GetAccountName());
17. stringBuilder.AppendLine();
18. Console.WriteLine(stringBuilder);
19. }
20. 
21. Console.WriteLine("---------------------------");
22. 
23. // Priting our details of Customer No. 2
24. Console.WriteLine("Details of Customer No. 2::");
25. Console.WriteLine("Customer Name: " + customer2.GetName());
26. Console.WriteLine("Type: " + customer2.GetCustomerType());
27. Console.WriteLine("Region: " + customer2.GetCountryRegion());
28. Console.WriteLine(Environment.NewLine + "Available Accounts:");
29. 
30. foreach (BankAccountInterface account in customer2.GetAccounts())
31. {
32. StringBuilder stringBuilder = new  StringBuilder();
33. stringBuilder.AppendLine("Account Type: " + account.GetAccountType());
34. stringBuilder.AppendLine("Account Name: " + account.GetAccountName());
35. stringBuilder.AppendLine();
36. Console.WriteLine(stringBuilder);
37. }
38. 
39. Console.WriteLine(Environment.NewLine + "Press any key to exit!");
40. Console.ReadLine();
41. }

In the aforementioned code you can see I am creating a new Customer object, who's name is "Jon Skeet" (hope Jon doesn't discover this article on the internet) and a new NonResidentialCustomer object, who's name is "Somdip Dey" (it feels awkward to type my own name, Somdip, here). And then fetching details of each Customer object so that I can print them in the console.

After you compile and build the whole program you should see an output like this on your console:

Windows Console Output:

Mac Console Output:

Conclusion

Hopefully, with this example,, you got a better understanding of Why, When and How you should use Interface and Abstract Class in your real-world application.

The full code is available from Code Gallery: https://gallery.technet.microsoft.com/Code-Usage-of-Interfaces-7d05dac2

GitHub Project: https://github.com/somdipdey/Usage-of_Interfaces_and_Abstract_Classes_In_Real_World_Application_CSharp/tree/master

References