Understanding Interfaces in C#
Interfaces allow us to define a contract for a class to implement. Think of it like a lightweight abstract class.
A common example is a database connection. There are dozens of kinds of databases, and they all have slightly different behaviors and capabilities. However, the way you interact with them is often the same. You have to be able to open/close connections:
public interface IDatabase
{
void Connect();
void Disconnect();
}
And the actual way you would do that between, say, Postgres and SQL Server is different, but the code behavior is (roughly) the same. (This is a good example of abstraction, one of the four object-oriented programming principles.)
Inheritance is Great (kind of), But Why Use Interfaces?
Multiple Inheritance is Not a Thing
C# does not support multiple inheritance for classes, meaning a class cannot inherit from more than one base class. However, C# allows a class to implement multiple interfaces. This helps achieve similar functionality as multiple inheritance without the complexities and issues it can introduce.
Interfaces Emphasize Composability
Interfaces are a way to achieve composability in C#. They allow different classes to share common behavior without being tied to a single inheritance hierarchy. This promotes flexible and modular design, making it easier to manage and extend code.
Understanding interfaces is core to understanding how C# code is written in the real world.
Why Are Interfaces Important?
They Provide No Behaviors by Default
Interfaces in C# define a contract but do not provide any implementation. It is up to the implementing classes to define how the contract is fulfilled. This means interfaces are purely about defining what methods and properties a class should have, not how they should work.
In other words:
- Classes define the thing's behaviors and properties
- Interfaces define the thing's shape without opinions about the behavior
Declaring an Interface
An interface is declared using the interface
keyword. It can contain method signatures, properties, events, and indexers but no implementation.
public interface IDatabase
{
void Connect();
void Disconnect();
bool IsConnected { get; }
}
Example
Classes implement interfaces by providing the required method and property implementations.
public class SqlDatabase : IDatabase
{
public bool IsConnected { get; private set; }
public void Connect()
{
// Implementation code
IsConnected = true;
}
public void Disconnect()
{
// Implementation code
IsConnected = false;
}
}
When a class inherits from an abstract class, it's said that the class is inheriting from the abstract class.
When a class implements an interface, it's said that the class is implementing the interface.
Advanced Stuff
Default Implementations
C# 8.0 introduced default implementations in interfaces, allowing you to provide a default implementation for methods in an interface.
Example
public interface ILogger
{
void Log(string message);
void LogError(string error) // Default implementation
{
Console.WriteLine($"Error: {error}");
}
}
🌶️🌶️🌶️ Spencer's hot take: I don't use default implementations in interfaces pretty much ever. Not that they can't be useful, but I typically just use interfaces as they were intended. Default implementations are useful in a very *narrow set of circumstances.
Static Abstract Methods
Static abstract methods in interfaces were introduced in C# 11.0, allowing you to define static methods in interfaces that must be implemented by the implementing types.
Example
public interface ICalculator<T>
{
static abstract T Add(T a, T b);
}
There are definitely reasons to do this, but in the real world, it's used pretty sparingly. See more here: Static Abstract Methods
Use Cases
Service-Based Architecture
Interfaces are commonly used in .NET for defining service contracts. For example, interfaces are used to define services that perform operations or provide functionalities, such as data access services or logging services.
The database example above is pretty good.
Marker Interfaces
Interfaces can also be used as markers to provide metadata about a class. For instance, IAuditable can be used to mark classes that should be audited.
public interface IAuditable { }
Doesn't look useful, until you have other code that audits objects based on their type! I'll show an example of this when we get to the next course on ASP.NET.
Avoiding Inheritance
Interfaces are often used to avoid deep inheritance hierarchies. Instead of extending classes, you can use interfaces to define shared behaviors and compose functionalities from multiple sources.
That doesn't mean that you should declare one class that implements 10 interfaces. I like code to be broken up as necessary, and often will have a single-use interface for a single class.