CodeGym /Courses /C# SELF /Using Interfaces in Practice

Using Interfaces in Practice

C# SELF
Level 24 , Lesson 4
Available

1. Interfaces in Business Logic: Buttons and Actions

This is the final lecture on interfaces, so there will be a lot of practical examples to help you really get how interfaces are used in real life.

Interfaces are especially valuable in big apps. So below you'll find some tricky cases, sometimes a bit ahead of our course program. If you're curious — awesome! If not — just skip to the next level :P

The simplest example — buttons and clicks

Let's keep building our learning console app — imagine we have a simple system of user elements (like buttons, text fields, and toggles) in a console menu.

We want some elements to react to clicks (buttons), and others not to (like a static header). Interfaces let us handle these scenarios in a really elegant way.

Step 1: Create the interface


// Component that can be "clicked":
public interface IClickable
{
    void Click();
}

Step 2: Apply the interface to different element types


// Base class for all menu items
public class MenuItem
{
    public string Title { get; set; }

    public MenuItem(string title)
    {
        Title = title;
    }

    public virtual void Display()
    {
        Console.WriteLine(Title);
    }
}

// Button: you can "click" it
public class Button : MenuItem, IClickable
{
    public Button(string title) : base(title) {}

    public void Click()
    {
        Console.WriteLine($"[Button] {Title} was pressed!");
    }
}

// Just a label: can't be clicked
public class Label : MenuItem
{
    public Label(string title) : base(title) {}
}

Step 3: Use interface polymorphism


IClickable[] clickableItems = new IClickable[]
{
    new Button("Save"),
    new Button("Exit")
    // Can't add Label here — it's not IClickable!
};

foreach (var item in clickableItems)
{
    item.Click();
}

See the magic? The list only accepts those who can Click, so you won't get a runtime error if you try to "click" something that's not supposed to be clicked.

2. Interfaces and the “Strategy” Pattern: Picking an Algorithm on the Fly

Let's say you're writing an app where you can save reports in different ways: to a file, to a database, to "the cloud", or maybe even to your boss's Telegram (don't ask, anything can happen).

Step 1: Define the task

We need the ReportGenerator class to work with any saving method, without knowing the implementation details.

Step 2: Describe the strategy interface


public interface IDataSaver
{
    void Save(string reportData);
}

Step 3: Implement different options


public class FileDataSaver : IDataSaver
{
    public void Save(string reportData)
    {
        Console.WriteLine("[FileDataSaver] Saving to file...\n" + reportData);
        // Here you could have code for File.WriteAllText(...)
    }
}

public class DatabaseDataSaver : IDataSaver
{
    public void Save(string reportData)
    {
        Console.WriteLine("[DatabaseDataSaver] Saving to database...\n" + reportData);
        // Here you could have code to write to a database
    }
}

Step 4: Use the interface to swap saving strategies


public class ReportGenerator
{
    private readonly IDataSaver _dataSaver;

    public ReportGenerator(IDataSaver dataSaver)
    {
        _dataSaver = dataSaver;
    }

    public void GenerateReport()
    {
        string report = "This is an important report!";
        Console.WriteLine("Generating report...");
        _dataSaver.Save(report);
    }
}

Showing off the flexibility


// You can swap the saving method without rewriting ReportGenerator!
IDataSaver fileSaver = new FileDataSaver();
ReportGenerator fileReport = new ReportGenerator(fileSaver);
fileReport.GenerateReport();

IDataSaver dbSaver = new DatabaseDataSaver();
ReportGenerator dbReport = new ReportGenerator(dbSaver);
dbReport.GenerateReport();

Practical meaning: If tomorrow you want to write reports to some new super-modern service, you just write a new class implementing the interface and plug the object into your architecture — no need to change a single line of existing code.

3. Interfaces in .NET: IDisposable and using

One of the most common interfaces in .NET is IDisposable. All classes that work with unmanaged resources — files, streams, network connections — implement it.

Why do we need IDisposable?

When you work with a resource that you absolutely have to release (like closing a file), you implement IDisposable and write a Dispose method. This lets you use these objects in a using statement, which guarantees Dispose gets called when you leave the block.

Example: Simulating working with a file

public class FakeFile : IDisposable
{
    public string FileName { get; }

    public FakeFile(string fileName)
    {
        FileName = fileName;
        Console.WriteLine($"Opened file: {fileName}");
    }

    public void Dispose()
    {
        Console.WriteLine($"Closed file: {FileName}");
    }
}

// In the main program:
using (var file = new FakeFile("report.txt"))
{
    Console.WriteLine("Writing to file...");
    // file will be "closed" automatically after using
}

Output:

Opened file: report.txt
Writing to file...
Closed file: report.txt

Really useful: you won't forget to close the file — the interface and the language infrastructure have your back.

4. Interfaces in Collections and LINQ

When you work with lists, arrays, dictionaries, you're already using interfaces — even if you don't think about it.


List<int> list = new List<int> { 1, 2, 3 };
IEnumerable<int> enumerable = list; // all good!

// Now you can loop through the elements like this:
foreach(var x in enumerable)
{
    Console.WriteLine(x);
}

Most LINQ methods work with collections through the IEnumerable<T> interface. This lets you write code that doesn't care about the specific collection type.

Why does this matter?
You can swap List<T> for T[], HashSet<T>, or even your own custom collection — and your code will keep working!

5. Interfaces for Testing (Mock Objects)

In tests, it's super important to isolate your code from external dependencies: don't touch the real database, don't mess with real files. Interfaces let you use stubs (mock objects) and test your code without touching real databases or files.


public class FakeDataSaver : IDataSaver
{
    public bool WasCalled { get; private set; } = false;
    public void Save(string reportData)
    {
        WasCalled = true;
        Console.WriteLine("Saved data in mock object!");
    }
}

// In the test
FakeDataSaver saver = new FakeDataSaver();
ReportGenerator generator = new ReportGenerator(saver);
generator.GenerateReport();

Console.WriteLine($"Was Save called? {saver.WasCalled}");

Result: the test doesn't depend on the real world, but checks that the right method was called!

6. Explicit Interface Implementation for Resolving Conflicts

Sometimes a class has to implement two interfaces with the same methods, but the logic for those methods is different.


public interface IFlyable
{
    void Move();
}

public interface ISwimmable
{
    void Move();
}

public class Duck : IFlyable, ISwimmable
{
    // Explicit implementation
    void IFlyable.Move()
    {
        Console.WriteLine("The duck is flying!");
    }

    void ISwimmable.Move()
    {
        Console.WriteLine("The duck is swimming!");
    }
}

Duck duck = new Duck();

// duck.Move(); // Error: no such method!
((IFlyable)duck).Move(); // "The duck is flying!"
((ISwimmable)duck).Move(); // "The duck is swimming!"

This might look harsh, but sometimes that's exactly what the specs require!

7. Interfaces in the Event Model: INotifyPropertyChanged

.NET has standard interfaces for supporting events — for example, when something changes in your data models and the UI needs to know about it (super common in WPF, MAUI, and other GUIs).


using System.ComponentModel;

public class Person : INotifyPropertyChanged
{
    private string name;
    public string Name
    {
        get => name;
        set
        {
            if (name != value)
            {
                name = value;
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Name)));
            }
        }
    }

    public event PropertyChangedEventHandler? PropertyChanged;
}

The point: any framework that supports data binding expects your class to implement this interface — and then the UI will automatically update when properties change.

8. Interfaces in the “Factory” Pattern

Interfaces are perfect for building factories — classes that create different but interchangeable objects.


public interface ITransport
{
    void Move();
}

public class Bicycle : ITransport
{
    public void Move() => Console.WriteLine("Riding the bike!");
}

public class Car : ITransport
{
    public void Move() => Console.WriteLine("Driving the car!");
}

public class TransportFactory
{
    public static ITransport Create(string type)
    {
        return type switch
        {
            "bike" => new Bicycle(),
            "car" => new Car(),
            _ => throw new ArgumentException("Unknown transport type")
        };
    }
}

ITransport transport = TransportFactory.Create("bike");
transport.Move(); // "Riding the bike!"

9. Interfaces for Events: Example of a Custom Event

Let's describe an interface for an "event listener":


public interface ILoginListener
{
    void OnLogin(string userName);
}

// Class that fires the event
public class LoginManager
{
    private List<ILoginListener> listeners = new();

    public void Subscribe(ILoginListener listener) => listeners.Add(listener);

    public void Login(string userName)
    {
        Console.WriteLine($"User {userName} logged in.");
        foreach (var listener in listeners)
            listener.OnLogin(userName);
    }
}

// Class implementing the interface
public class WelcomeMessage : ILoginListener
{
    public void OnLogin(string userName)
    {
        Console.WriteLine($"Welcome, {userName}!");
    }
}

LoginManager manager = new();
manager.Subscribe(new WelcomeMessage());
manager.Login("Vasya");
// User Vasya logged in.
// Welcome, Vasya!

At an interview: if they ask — “how would you implement your own event system?”, just talk about interface listeners!

10. Interfaces for Plugins (App Extensibility)

A lot of big apps support plugins. Thanks to interfaces, your app can "load" new modules on the fly without knowing their internals.


public interface IPlugin
{
    string Name { get; }
    void Run();
}

// Your app:
public class PluginLoader
{
    public void LoadAndRun(IEnumerable<IPlugin> plugins)
    {
        foreach (var plugin in plugins)
        {
            Console.WriteLine($"Launching plugin: {plugin.Name}");
            plugin.Run();
        }
    }
}

Plugins can be developed by third parties — the main thing is that they implement the interface. Your app becomes extensible!

2
Task
C# SELF, level 24, lesson 4
Locked
Implementing an Interface for Geometric Shapes
Implementing an Interface for Geometric Shapes
2
Task
C# SELF, level 24, lesson 4
Locked
Simple Factory Using Interfaces
Simple Factory Using Interfaces
1
Survey/quiz
Using Interfaces, level 24, lesson 4
Unavailable
Using Interfaces
Advanced Interfaces
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION