Поиск  
Always will be ready notify the world about expectations as easy as possible: job change page
Sep 14, 2023

How C# interfaces can help you structure your codebase

How C# interfaces can help you structure your codebase
Автор:
Mina Pêcheux
Источник:
Просмотров:
14429

Interfaces are at the heart of the “composition-over-inheritance” paradigm — let’s see what that means!

How C# interfaces can help you structure your codebase

As you probably know, C# is a statically typed language. And as such, it is very helpful with type-checking and safe data conversions. Your IDE most likely knows when you’re feeding a variable of the wrong type somewhere and warns you, or even refuses to compile. However, you can actually take this flow checking one step further by using C# interfaces.

C#

Quoting the Microsoft C# docs:

An interface defines a contract. Any class or struct that implements that contract must provide an implementation of the members defined in the interface.

Roughly put, interfaces are about defining the shape of a class or a struct: the fields it should have, the methods it should define, etc. You don’t define the body of your functions or the value of your fields — in an interface, there are no actual definitions, only declarations. Then, you can create classes or structs that “implement this interface”. The idea is that by having the class or struct derive from an interface, you make a deal with your fellow coders (and the C# compiler!): whatever property the interface declares, the class has too; whatever function the interface declares, the class has and implements; whatever event or indexers the interface has, the class has.

Note: beginning with C# 8.0, interface function declarations may also have a body to provide a default implementation, as we’ll see in an example below. However, the most common use case of interfaces only declares the prototype and leaves the actual implementation up to the classes or structs that derive from it.

Interfaces are therefore an extremely powerful tool for codebase sanity and structuring. Since implementing an interface in a class or struct forces you to tick all these boxes, as a developer you can assume things about the class and better predict its behavior. For your IDE, it will probably be helpful too — it will be able to list the available methods and fields that depend on this interface and provide you with lots of interesting info. And the IDE should even be able to tell you when you are missing fields or methods to properly fulfill the “contract” you made when you decided to implement the interface… Visual Studio and VS Code, for example, will highlight the interface dependency in your class or struct declaration and show you what’s missing until you’ve successfully defined and implemented all the required info!

Also, interfaces help with data protection (or encapsulation). When you code your interface, you can choose the type of accessor it uses (public, protected, private). By default, interfaces are public, but you can easily change it as you would for a class or a struct, just by adding the proper keyword in front of it:

// (interfaces are public by default)
interface IMyInterface { ... }
internal interface IMyInternalInterface { ... }
private interface IMyPrivateInterface { ... }

And then, the real trick is with your interface fields getter and setters — as is often the case in object-oriented programming, choosing the right accessibility level for your fields can be the key to success. Usually, when you create a field, you specify both a getter and a setter:

interface IMyInterface
{
    string Name { get; set; }
}

But this means that anyone who has an instance of a class or struct that derives from this interface will be able to modify its Name field. If you only want people to read the value and you want to secure the data, you can define only the getter. This will basically make a read-only field:

interface IMyInterface
{
    string Name { get; }
}

You can take advantage of interfaces to share behavior between classes or structs, too: if you define some static data, or a static method in the interface, then all classes and structs that implement it will have access to it. As the generics or the delegates, this is another example of a C# built-in tool that helps you centralise the code and avoid inconsistencies.

Interfaces are the foundation of the composition over inheritance” OOP paradigm as defined in the very influential 1994 book Design Patterns. The big picture is that this concept relies on combining multiple independent components together on your objects to gradually build a multi-facetted behavior, rather than having your object be the last child in a long chain of inherited classes. This frame of mind is oftentimes considered way more flexible than inheritance because you can more easily add and remove behavior and you are less subjected to side-effects from parent classes.

This is not to say that inheritance is bad in itself: sometimes, you should actually think about mixing up the two or sticking with inheritance for your project. But composition is interesting because it makes each part of your complex behavior autonomous and therefore easier to implement on its own. By fleshing out your codebase into separated features and then merging them with interface implementations, you have more control over the dependencies, the updates and the specifics of each part.

Now — to understand how we can use interfaces in C#, let’s look at two little examples: a fictitious basic RPG game hero system, and Unity’s UI elements callback functions.

Programming with C# interfaces: a basic RPG character class definition

Suppose you’re tasked with designing a small RPG game; and the first feature on the list is the creation of heroes! In this fantasy world (heavily inspired by Tolkien), there are 3 races available: humans, elves and dwarves. Then, for each of these race, the player can pick one class among four: warrior, hunter, mage or thief. This gives a total of 12 combinations — for example, a hero could be a “human mage” or an “elf thief”.

There are plenty of ways of creating such a system; but here, interfaces can come in handy because they will help you separate the concerns, structure your code better and make the various parts of your code more independent which increases modularity. More specifically, in our case, race and class are currently completely unrelated. This means that we can consider each property on its own and work on them separately. Then, thanks to interfaces, we’ll be able to apply both “contracts” to our heroes and make sure they implement both properly — in other words, we are going to follow the composition over inheritance paradigm.

First of, we’ll create some static data about our races and classes. For the races, we have a set of stats and for the classes, we have the name of the base power heroes will have when they start their journey:

public enum HeroRace { HUMAN, ELF, DWARF };
public enum HeroClass { WARRIOR, HUNTER, MAGE, THIEF };

public static Dictionary<HeroRace, Dictionary<string, int>> heroRaceData = new Dictionary<HeroRace, Dictionary<string, int>>()
{
    {
        HeroRace.HUMAN,
        new Dictionary<string, int>()
        { { "attack", 10 }, { "speed", 9 }, { "defence", 11 }, { "charism", 10 }, }
    },
    {
        HeroRace.ELF,
        new Dictionary<string, int>()
        { { "attack", 7 }, { "speed", 12 }, { "defence", 9 }, { "charism", 12 }, }
    },
    {
        HeroRace.DWARF,
        new Dictionary<string, int>()
        { { "attack", 13 }, { "speed", 6 }, { "defence", 13 }, { "charism", 8 }, }
    }
};

public static Dictionary<HeroClass, string> basePowers =
    new Dictionary<HeroClass, string>()
    {
        { HeroClass.WARRIOR, "Strike" },
        { HeroClass.HUNTER, "Shoot" },
        { HeroClass.MAGE, "Fireball" },
        { HeroClass.THIEF, "Disguise" },
    };

This data is fixed and decided once and for all by developers; when the game runs, we simply read it and show it in various ways, we don’t modify it. For now, we’ll simply print some of it in the console.

Now, it’s time to write our interfaces! For the race property, we’ll make an IHeroRace interface that simply defines the HeroRace field itself and an output function that is used to print info on the racial abilities, PrintRaceInformation(). Be careful: this function is not defined in the interface, it must be filled later on when creating our class that derives from this interface.

interface IHeroRace
{
    HeroRace HeroRace { get; }
    void PrintRaceInformation();
}

Similarly, for the class property, we can code up an IHeroClass interface with a field for the hero's class and a direct getter for the base power - here I'm using C# 8.0+, so I can even define a default implementation for the getter:

interface IHeroClass
{
    HeroClass HeroClass { get; }
    string GetBasePowerName()
    {
        return basePowers[HeroClass];
    }
}

Note: since interfaces are by default public, the default implementations in them are too ;)

And finally, we simply need to create our Hero class that derives from both these interfaces. It's a perfectly usual C# class meaning that we can also have additional fields (here, the hero's name) and methods (like PrintBaseInformation()) that don't come from any interface. But we of course need to implement the function required by the IHeroRace interface that has no default implementation, and we use the constructor to fill in our various fields:

class Hero : IHeroRace, IHeroClass
{
    public Hero(string name, HeroRace heroRace, HeroClass heroClass)
    {
        Name = name;
        HeroRace = heroRace;
        HeroClass = heroClass;
    }
    public string Name { get; }
    public HeroRace HeroRace { get; }
    public HeroClass HeroClass { get; }

    public void PrintBaseInformation()
    {
        Console.WriteLine("{0} is a {1} {2}", Name, HeroRace, HeroClass);
    }

    public void PrintRaceInformation()
    {
        Console.WriteLine("{0}'s racial abilities:", Name);
        Dictionary<string, int> data = heroRaceData[HeroRace];
        PrintTable<int>(
            new string[] { "Attack", "Speed", "Defence", "Charism" },
            new int[][] { new int[] {
                data["attack"], data["speed"], data["defence"], data["charism"]
            } }
        );
    }
}

Note: to display the racial abilities of the hero in a nice way, I’ve used one of my util C# snippets, PrintTable<>(); it is a generic method that tries to pretty print an enumerable of enumerables of same-type objects (in order to get rows of columns).

We can use this class like this, to print some info and check that everything works fine:

static void Main(string[] args)
{
    Hero[] heroes = new Hero[]
    {
        new Hero("Billy", HeroRace.DWARF, HeroClass.WARRIOR),
        new Hero("Johnny", HeroRace.HUMAN, HeroClass.THIEF),
        new Hero("Betty", HeroRace.ELF, HeroClass.HUNTER),
        new Hero("Jackie", HeroRace.ELF, HeroClass.MAGE),
    };
    foreach (Hero hero in heroes) {
        hero.PrintBaseInformation();
        hero.PrintRaceInformation();
        Console.WriteLine("...");
    }
}

Another example of C# interfaces: Unity’s UI callbacks

When you program games in Unity and C#, there is a very common application of interfaces: the UI elements callback functions. Oftentimes, you’ll have some button or frame that you’d like to be able to click on to perform an action. Or you could want to add a hover/unhover logic that changes the element’s color while the mouse is over it. All of this is doable via script using some of Unity’s built-in UI interfaces: IPointerEventHandler, IPointerExitHandler, IPointerClickHandler...

For example, the following snippet of code can be applied on any object in a Unity scene that has an UnityEngine.UI.Image component on to tint it red when hovered:

using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;

[RequireComponent(typeof(Image))]
public class HoverableUIElement : MonoBehaviour, IPointerEnterHandler, IPointerExitHandler
{
    private Image _image;
    private Color _defaultColor;
    private Color _hoverColor;
    
    private void Awake()
    {
      _image = GetComponent<Image>();
      _defaultColor = _image.color;
      _hoverColor = Color.red;
    }

    public void OnPointerEnter(PointerEventData eventData)
    {
        _image.color = _hoverColor;
    }

    public void OnPointerExit(PointerEventData eventData)
    {
        _image.color = _defaultColor;
    }
}

Like before, once we’ve chosen an interface to derive from for our C# class, we simply need to implement the matching methods to fulfill the contract, and Unity’s engine will automatically call them at the proper time.

Conclusion

C# interfaces are extremely useful for codebase structure — they allow you and the devs in your team (plus your IDE!) to know what to expect and what to assume about the custom objects you’re handling. Rather than hoping for the best and/or copying the same function hundreds of times, you can have multiple classes derive from the same interface to share behavior and fields. Using interfaces usually pushes to think in the composition-over-inheritance way, meaning that your classes are assemblies of small independent systems that all coexist, instead of being yet another derivative of a base abstract class. This paradigm has grown on people and has been put in the spotlight many times because it nicely decouples components and better separates concerns.

What about you: do you use C# interfaces in your projects? Do you have other use cases and nice examples of where they can be useful? If you do, feel free to react in the comments! :)

Похожее
Dec 23, 2023
Author: Matt Bentley
A guide to implementing value objects — Domain-Driven Design’s most powerful, yet least understood and utilized building block Generated using DALL-E 3 Value Objects are one of the most powerful building blocks in Domain-Driven Design for creating rich models, however...
May 8, 2023
Author: Waqas Ahmed
Dapper is a lightweight ORM (Object-Relational Mapping) framework for .NET Core and is commonly used to query databases in .NET Core applications. Here are some of the advanced features of Dapper in .NET Core: Multi-Mapping: Dapper allows you to map...
Oct 26, 2023
Author: Genny Allcroft
Key strategic and tactical considerations to take when building a new product with the domain-driven design concepts in mind I have just finished reading Learning Domain-Driven Design by Vlad Khononov. It’s quite a short book (c. 300 pages) aiming to...
Jun 28
Author: ByteHide
Are you preparing for an interview that will involve C# asynchronous programming? You’ve come to the right place! This comprehensive article covers a wide range of C# async-await interview questions that will test your understanding of the async-await pattern, along...
Написать сообщение
Тип
Почта
Имя
*Сообщение
RSS