Interfaces and Abstract Classes in C#
Learn interfaces and abstract classes in C#, their differences, and when to use each approach to design clean and extensible code.
In C#, abstract classes and interfaces are used to build flexible and extensible object-oriented architectures.
Both help define common behaviors, but there are important differences.
An abstract class can provide partial implementation (method bodies), whereas an interface only declares
signatures and leaves the implementation entirely to derived classes.
Abstract Class
An abstract class is defined with the abstract keyword and cannot be instantiated directly.
It defines common properties and methods, but some methods can be marked as abstract to force subclasses to implement them.
Abstract classes may also include regular methods—i.e., partially implemented behavior.
public abstract class Shape
{
public string Color { get; set; } = "Black";
// Must be implemented by subclasses
public abstract double GetArea();
// A common method
public void PrintColor()
{
Console.WriteLine($"Color: {Color}");
}
}
public class Circle : Shape
{
public double Radius { get; set; }
public override double GetArea()
{
return Math.PI * Radius * Radius;
}
}
Here, the Shape abstract class provides a common Color property,
but leaves GetArea() to subclasses.
The Circle class implements it in its own way.
Interface
An interface is defined with the interface keyword.
It contains only method and property signatures and no bodies (prior to C# 8).
A class can implement multiple interfaces (enabling a form of multiple inheritance).
This establishes a “common contract” among different classes.
public interface ILogger
{
void Log(string message);
}
public class FileLogger : ILogger
{
public void Log(string message)
{
Console.WriteLine($"Logged to file: {message}");
}
}
public class DbLogger : ILogger
{
public void Log(string message)
{
Console.WriteLine($"Logged to database: {message}");
}
}
Classes implementing ILogger perform logging in different ways,
but they must all expose the same method signature: Log(string).
Abstract Class vs Interface
Key differences:
- Abstract Class: Can define common behavior and required behavior. Can contain property and method bodies.
- Interface: Declares signatures only. Supports multiple implementation.
- Abstract Class: A class can inherit from only one abstract (base) class.
- Interface: A class can implement multiple interfaces.
- Abstract Class: Can contain shared state, fields, and a constructor.
- Interface: Does not contain fields or a constructor.
Real-World Scenario: Production Line (Interface + Abstract Class)
In the example below, we define an IProduct interface to model a product manufacturing flow and an
abstract class (ProductBase) that partially implements it.
IProduct declares the common contract (properties + method signatures), while ProductBase
provides the shared flow (template method: Production()) and partial implementation.
MetalProduct, WoodProduct, and PlasticProduct customize this structure.
We use an enum (SizeOption) for consistent size values like “S/M/L,”
and a simple quality-check interface IQualityCheck that products implement.
using System;
using System.Collections.Generic;
// Enum for standardizing sizes
public enum SizeOption
{
Small,
Medium,
Large
}
// Common contract: properties and behaviors all products must have
public interface IProduct
{
string Name { get; set; }
string SKU { get; set; }
SizeOption Size { get; set; }
decimal UnitCost { get; set; } // Unit cost
int Stock { get; set; } // Units in stock
// Production flow and cost calculation
void Production();
decimal CalculateCost(int quantity);
string GetSpecs();
}
// Simple quality-check contract
public interface IQualityCheck
{
bool QualityCheck(out string reason);
}
// ABSTRACT CLASS: Partial implementation + shared flow (Template Method Pattern)
public abstract class ProductBase : IProduct, IQualityCheck
{
public string Name { get; set; } = string.Empty;
public string SKU { get; set; } = string.Empty;
public SizeOption Size { get; set; }
public decimal UnitCost { get; set; }
public int Stock { get; set; }
// Material-based multiplier (subclasses can override)
protected virtual decimal MaterialFactor => 1.00m;
// Template method: runs production steps in a fixed flow
public void Production()
{
LogStep($"Production started: {Name} ({SKU})");
PrepareMaterials();
Assemble();
Finish();
LogStep("Production completed.\n");
}
// Base cost calculation (subclasses can override if needed)
public virtual decimal CalculateCost(int quantity)
{
if (quantity <= 0) return 0m;
// Simple model: unit cost * quantity * material factor * overhead (10%)
decimal baseCost = UnitCost * quantity * MaterialFactor;
decimal overhead = baseCost * 0.10m;
return baseCost + overhead;
}
// Common technical specification output
public virtual string GetSpecs()
=> $"SKU: {SKU}, Size: {Size}, Unit Cost: {UnitCost:0.00} USD, Stock: {Stock}";
// Default quality check: is cost and stock sensible?
public virtual bool QualityCheck(out string reason)
{
if (UnitCost <= 0) { reason = "Unit cost cannot be zero/negative."; return false; }
if (Stock < 0) { reason = "Stock cannot be negative."; return false; }
reason = "OK";
return true;
}
// Steps that subclasses must implement
protected abstract void PrepareMaterials();
protected abstract void Assemble();
protected virtual void Finish()
{
LogStep("General quality check and packaging");
}
protected void LogStep(string message) => Console.WriteLine($" - {message}");
}
// METAL PRODUCT
public class MetalProduct : ProductBase
{
public string Alloy { get; set; } = "Steel 304L";
public bool RequiresHeatTreatment { get; set; } = true;
protected override decimal MaterialFactor => 1.45m; // metal processing is costlier
protected override void PrepareMaterials()
{
LogStep($"Preparing metal sheet/billet (Alloy: {Alloy})");
if (RequiresHeatTreatment) LogStep("Preparing furnace for heat treatment");
}
protected override void Assemble()
{
LogStep("Cutting, bending, welding, and surface grinding");
if (RequiresHeatTreatment) LogStep("Applying heat treatment (hardening)");
}
protected override void Finish()
{
LogStep("Corrosion protection and surface coating");
base.Finish();
}
public override string GetSpecs()
=> $"[Metal] {Name} | {base.GetSpecs()} | Alloy: {Alloy} | Heat treatment: {(RequiresHeatTreatment ? "Yes" : "No")}";
public override bool QualityCheck(out string reason)
{
if (!base.QualityCheck(out reason)) return false;
// Extra rule for metal: alloy name must not be empty
if (string.IsNullOrWhiteSpace(Alloy)) { reason = "Alloy information cannot be empty."; return false; }
return true;
}
}
// WOOD PRODUCT
public class WoodProduct : ProductBase
{
public string WoodType { get; set; } = "Oak";
public bool IsVarnished { get; set; } = true;
protected override decimal MaterialFactor => 1.20m;
protected override void PrepareMaterials()
{
LogStep($"Selecting and drying lumber (Type: {WoodType})");
LogStep("Cutting and sanding");
}
protected override void Assemble()
{
LogStep("Joinery: mortise/dowel and glue");
}
protected override void Finish()
{
LogStep(IsVarnished ? "Applying varnish" : "Applying oil/protective finish");
base.Finish();
}
public override string GetSpecs()
=> $"[Wood] {Name} | {base.GetSpecs()} | Wood type: {WoodType} | Varnish: {(IsVarnished ? "Yes" : "No")}";
}
// PLASTIC PRODUCT
public class PlasticProduct : ProductBase
{
public string Polymer { get; set; } = "PP";
public bool Recyclable { get; set; } = true;
protected override decimal MaterialFactor => 0.95m; // plastics often cost less
protected override void PrepareMaterials()
{
LogStep($"Preparing granule feedstock (Polymer: {Polymer})");
LogStep("Mixing color masterbatch");
}
protected override void Assemble()
{
LogStep("Injection molding and trimming");
}
protected override void Finish()
{
LogStep("Surface inspection and labeling");
base.Finish();
}
public override string GetSpecs()
=> $"[Plastic] {Name} | {base.GetSpecs()} | Polymer: {Polymer} | Recyclable: {(Recyclable ? "Yes" : "No")}";
}
// DEMO
class Program
{
static void Main()
{
var items = new List<IProduct>
{
new MetalProduct
{
Name = "Steel Shelf",
SKU = "M-SH-001",
Size = SizeOption.Large,
UnitCost = 500m,
Stock = 40,
Alloy = "304L",
RequiresHeatTreatment = true
},
new WoodProduct
{
Name = "Wooden Table",
SKU = "W-TB-002",
Size = SizeOption.Medium,
UnitCost = 350m,
Stock = 12,
WoodType = "Oak",
IsVarnished = true
},
new PlasticProduct
{
Name = "Plastic Crate",
SKU = "P-CR-003",
Size = SizeOption.Small,
UnitCost = 80m,
Stock = 200,
Polymer = "PP",
Recyclable = true
}
};
const int qty = 10;
foreach (var p in items)
{
Console.WriteLine($"\n=== {p.Name} ===");
Console.WriteLine(p.GetSpecs());
p.Production();
Console.WriteLine($"Total cost ({qty} units): {p.CalculateCost(qty):0.00} USD");
if (p is IQualityCheck qc)
{
Console.WriteLine(qc.QualityCheck(out var reason)
? "Quality Check: OK"
: $"Quality Check: FAIL - {reason}");
}
}
}
}
Summary: The IProduct interface provides a common contract for different product types.
The ProductBase abstract class partially implements this contract and standardizes the production flow (template method).
The subclasses (Metal/Wood/Plastic) customize material-specific steps and cost assumptions.
By combining interface + abstract class, you get both flexibility and reusability.
Advantages
- Interface: Flexibility thanks to multiple implementation.
- Abstract Class: Reduces code duplication with shared state and partial implementation.
- Both: Reduce coupling and increase testability.
TL;DR
abstract class: Defines common + required behavior; can include method bodies.interface: Declares signatures only; supports multiple implementation.- A class can inherit from only one abstract/base class but can implement multiple interfaces.
Related Articles
Class, Object, Property and Methods in C#
Learn how classes, objects, properties, and methods work in C# and form the core building blocks of object-oriented programming.
Delegates and Events in C#
Learn delegates and events in C# to build event-driven applications using callbacks, subscriptions, and real-world examples.
Dependency Injection Basics in C#
Learn the basics of Dependency Injection in C#, including managing dependencies, loose coupling, and improving testability.
Encapsulation, Inheritance, and Polymorphism in C#
Learn encapsulation, inheritance, and polymorphism in C# with clear examples to understand core OOP principles and real use cases.
Extension Methods in C#
Learn extension methods in C# to add new functionality to existing types without modifying their source code.
Namespaces and Assemblies in C#
Learn how namespaces and assemblies work in C# to organize code, manage dependencies, and structure projects effectively.
Sealed, Static, and Partial Classes in C#
Learn the purpose, differences, and use cases of sealed, static, and partial classes in C# with practical examples.
SOLID Principles with C#
Applying SOLID principles in C# with examples: building flexible, maintainable, and testable code.