Interfaces: because sometimes one implementation just isn't enough

Interfaces: because sometimes one implementation just isn't enough

Interfaces: because sometimes one implementation just isn't enough was initially published on Wednesday January 11 2023 on the Tech Dev Blog. For the latest up-to-date content, fresh out of the oven, visit https://techdevblog.io and subscribe to our newsletter!

What are interfaces?

An interface in programming is a type that defines a set of related behaviors that a class or struct must implement. It's like a contract that specifies what methods a class must implement, without defining how those methods should be implemented.

Interfaces are a way to specify what a class does, rather than how it does it. This allows for greater flexibility in how objects are created and used, since the implementation of an interface can be swapped out at runtime.

Why use interfaces?

There are several advantages to using interfaces in programming:

  • Code decoupling: interfaces allows us to decouple our code more easily and improve its maintainability. They allow us to define a set of related behaviors that can be implemented in different ways, making it easier to change the implementation of a particular behavior without affecting the rest of the code.
  • Simplifying dependency injection: Interfaces allow for more flexibility in how objects are created and used, since the implementation of an interface can be swapped out at runtime. This makes it easier to inject dependencies into an object, which can be useful for things like unit testing.
  • Facilitated testing: this decoupling of code and the use of dependency injection also make it easier to test, as it allows us to isolate specific parts of the code and test them in isolation. Interfaces make it easier to write automated tests, since we can create mock objects that implement the same interface as the objects we are testing. This allows us to test the behavior of an object without worrying about its implementation details.
  • Enabling more complex architectures: Interfaces allow us to build more complex architectures by allowing us to define a set of behaviors that can be implemented by different classes in different ways. Allowing us to create flexible, modular code. This is particularly useful when building larger applications, as it allows us to divide the application into smaller, independent components that can be developed and maintained separately. This can be useful for things like implementing a plugin system or building a modular application.

Best practices

Here are some best practices for using interfaces in your code:

  • Keep interfaces small and focused: A good interface should define a small, focused set of related behaviors. Avoid including unrelated behaviors or methods in your interfaces.
  • Use interface names that describe their purpose: Choose descriptive names for your interfaces that accurately describe their purpose.
  • Avoid using "I" prefixes for interface names: While it's common to prefix interface names with an "I" in some programming languages (e.g. "IMyInterface"), this practice is not followed in all languages and can lead to confusion. It's generally better to use a descriptive name for your interface.

Examples in a few different programming languages

Here are some examples of how to define and implement interfaces in different programming languages:

TypeScript

// Define an interface
interface Shape {
  area(): number;
}

// Implement the interface
class Rectangle implements Shape {
  width: number;
  height: number;

  constructor(width: number, height: number) {
    this.width = width;
    this.height = height;
  }

  area() {
    return this.width * this.height;
  }
}

const rectangle = new Rectangle(10, 20) as Shape;
console.log(rectangle.area()); // Outputs: 200

Swift

// Define an interface
protocol Shape {
  func area() -> Double
}

// Implement the interface
struct Rectangle: Shape {
  var width: Double
  var height: Double

  func area() -> Double {
    return self.width * self.height
  }
}

let rectangle = Rectangle(width: 10, height: 20)
print(rectangle.area()) // Outputs: 200

Kotlin

// Define an interface
interface Shape {
  fun area(): Double
}

// Implement the interface
class Rectangle(val width: Double, val height: Double): Shape {
  override fun area() = width * height
}

val rectangle = Rectangle(10.0, 20.0)
println(rectangle.area()) // Outputs: 200.0

C\

// Define an interface
public interface Shape {
  double Area();
}

// Implement the interface
public class Rectangle : Shape {
  public double Width { get; set; }
  public double Height { get; set; }

  public Rectangle(double width, double height) {
    Width = width;
    Height = height;
  }

  public double Area() {
    return Width * Height;
  }
}

Shape rectangle = new Rectangle(10, 20);
Console.WriteLine(rectangle.Area()); // Outputs: 200

C++

// Define an interface
class Shape {
public:
  virtual double area() = 0;
};

// Implement the interface
class Rectangle: public Shape {
private:
  double width;
  double height;

public:
  Rectangle(double width, double height) {
    this->width = width;
    this->height = height;
  }

  double area() {
    return width * height;
  }
};

Shape* rectangle = new Rectangle(10, 20);
cout << rectangle->area() << endl; // Outputs: 200

Python

# Define an interface
class Shape:
    def area(self) -> float:
        pass

# Implement the interface
class Rectangle(Shape):
    def __init__(self, width: float, height: float) -> None:
        self.width = width
        self.height = height

    def area(self) -> float:
        return self.width * self.height

rectangle = Rectangle(10, 20)
print(rectangle.area()) # Outputs: 200.0

Ocaml

(* Define an interface *)
module type SHAPE = sig
  type t
  val area: t -> float
end

(* Implement the interface *)
module Rectangle: SHAPE = struct
  type t = { width: float; height: float }
  let area { width; height } = width *. height
end

let rectangle = { width = 10.0; height = 20.0 }
print_float (Rectangle.area rectangle); (* Outputs: 200.0 *)

Use cases

Any time we need to decouple our is a great use case for interface. Here are a few examples of use cases for interfaces in different programming languages:

Use case: Modular application in TypeScript

In this example, we'll use interfaces to build a modular application in TypeScript. We'll define an interface called Widget that defines a set of methods that all widgets must implement, including render() and update(). Then, we'll create a class called WidgetManager that can discover and load widgets that implement the IWidget interface at runtime.

// Define the IWidget interface
interface Widget {
  render(): void;
  update(): void;
}

// Implement the IWidget interface
class MyWidget implements Widget {
  render() {
    console.log("Rendering MyWidget");
  }

  update() {
    console.log("Updating MyWidget");
  }
}

// WidgetManager class that can discover and load widgets at runtime
class WidgetManager {
  widgets: Widget[];

  constructor() {
    this.widgets = [];
  }

  loadWidgets() {
    // Load widget modules
    const widgetModules = [MyWidget];

    // Create instances of all widgets
    for (const widgetModule of widgetModules) {
      this.widgets.push(new widgetModule());
    }
  }

  renderWidgets() {
    for (const widget of this.widgets) {
      widget.render();
    }
  }

  updateWidgets() {
    for (const widget of this.widgets) {
        widget.update();
    }
  }
}

To use the WidgetManager, you can do the following:

const widgetManager = new WidgetManager();
widgetManager.loadWidgets();
widgetManager.renderWidgets();
widgetManager.updateWidgets();

Use case: Protocol-oriented programming in Swift

In this example, we'll use interfaces (called protocols in Swift) to implement protocol-oriented programming in Swift. We'll define a protocol called Shape that defines a single method called area(), which returns the area of a shape. Then, we'll create two structs that implement the Shape protocol: Rectangle and Circle.

// Define the Shape protocol
protocol Shape {
  func area() -> Double
}

// Implement the Shape protocol
struct Rectangle: Shape {
  var width: Double
  var height: Double

  func area() -> Double {
    return self.width * self.height
  }
}

struct Circle: Shape {
  var radius: Double

  func area() -> Double {
    return Double.pi * self.radius * self.radius
  }
}

// Create instances of the Rectangle and Circle structs
let rectangle = Rectangle(width: 10, height: 20)
let circle = Circle(radius: 5)

// Print the area of the shapes
print(rectangle.area()) // Outputs: 200
print(circle.area()) // Outputs: 78.53981633974483

Using protocols in this way allows you to define a set of behaviors that can be implemented by different types in different ways, and to treat those types as a single type when it makes sense to do so. This can be useful for things like creating collections of shapes, or writing functions that work with any type that implements the Shape protocol.

Use case: Plugin system in C\

In this example, we'll use interfaces to build a simple plugin system in C#. We'll define an interface called Plugin that defines a single method called Execute(), which will be used to execute the plugin. Then, we'll create a class called PluginManager that can discover and load plugins that implement the Plugin interface at runtime.

// Define the IPlugin interface
public interface Plugin {
  void Execute();
}

// Implement the IPlugin interface
public class MyPlugin: Plugin {
  public void Execute() {
    Console.WriteLine("MyPlugin is executing!");
  }
}

// PluginManager class that can discover and load plugins at runtime
public class PluginManager {
  public IEnumerable<Plugin> LoadPlugins() {
    // Load assemblies in the current directory
    foreach (var assembly in Directory.GetFiles(Environment.CurrentDirectory, "*.dll")) {
      // Load the assembly
      var pluginAssembly = Assembly.LoadFrom(assembly);

      // Find all types that implement the IPlugin interface
      foreach (var type in pluginAssembly.GetTypes().Where(t => typeof(Plugin).IsAssignableFrom(t))) {
        // Create an instance of the plugin
        yield return (Plugin)Activator.CreateInstance(type);
      }
    }
  }
}

// Use the PluginManager to load and execute plugins
var pluginManager = new PluginManager();
foreach (var plugin in pluginManager.LoadPlugins()) {
  plugin.Execute();
}

Use case: Dependency injection in Python

In this example, we'll use interfaces to implement dependency injection in Python. We'll define an interface called Logger that defines a single method called log(), which can be used to log data and events, either to the console, to a logfile, or to a remote service:

#Define an interface
class Logger:
    def log(self, message: str) -> None:
        pass

# Implement the interface
class ConsoleLogger(Logger):
    def log(self, message: str) -> None:
        print(message)

# Use the interface for dependency injection
class UserService:
    def __init__(self, logger: Logger):
        self.logger = logger

    def create_user(self, name: str) -> None:
        self.logger.log(f"Creating user {name}")
        # ...

# Inject the ConsoleLogger into the UserService
user_service = UserService(console_logger)
user_service.create_user("Alice")

Conclusion

In conclusion, interfaces are the bomb! They make it easier to inject dependencies, swap out implementations at runtime, build modular application or use complex architectures, and write automated tests. Whether you're using protocols in Swift, interfaces in Java, ABCs (Abstract Base Classes) in Python, or something else, there are endless possibilities for using interfaces to improve the structure and modularity of your code.

And let's be real, who doesn't love a good interface? Go out there and interface-ate your code like it's 1999 (but with better technology)

Interfaces: because sometimes one implementation just isn't enough was initially published on Wednesday January 11 2023 on the Tech Dev Blog. For the latest up-to-date content, fresh out of the oven, visit https://techdevblog.io and subscribe to our newsletter!

Did you find this article valuable?

Support Tech Dev Blog by becoming a sponsor. Any amount is appreciated!