Object-Oriented C# · Lesson 15

Polymorphic behavior without type switches

Let each implementation satisfy one narrow operation contract.

How it works

An interface lets callers request a capability without depending on a concrete class. Several implementations can satisfy the same operation in different ways. The caller can then iterate through interface values and invoke the contract, instead of writing a growing chain of type checks that knows every implementation in advance.

A narrow interface is easier to use correctly than a broad one containing unrelated methods. Define the inputs, outputs, and important failure rules of each operation. Implementations may vary their internal calculation, but they must preserve that observable contract. This makes it possible to add a new implementation without changing a caller that already depends only on the interface.

Walk through the example. ITextRule has one operation that transforms a prepared string. UppercaseRule and PrefixRule implement that operation differently. The caller stores both under the interface type and applies them in sequence, turning rune into RUNE and then into a tagged label. The loop does not inspect implementation types, so its coordination logic stays the same when a new compatible rule is added.

Watch the boundary. An interface name does not guarantee interchangeable behavior. One implementation that unexpectedly returns null or performs an unrelated destructive action would violate a caller's reasonable contract even if its method signature matches. Document the behavior and validate inputs consistently. Also avoid adding members to an interface merely because one implementation happens to need an extra helper internally.

Practice independently. Add a suffix rule and a whitespace-trimming rule to the pipeline. Write expected intermediate and final values before executing it. Decide whether rule order is significant and demonstrate two orders that produce different results. Finally identify which validation rules belong in the common interface contract and which are configuration rules for one concrete implementation only.

A working example

using System;
using System.Collections.Generic;
using System.Linq;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;

ITextRule[] rules = { new UppercaseRule(), new PrefixRule("tag:") };
string text = "rune";
foreach (ITextRule rule in rules) text = rule.Apply(text);
Console.WriteLine(text);
interface ITextRule { string Apply(string value); }
sealed class UppercaseRule : ITextRule
{
    public string Apply(string value) => value.ToUpperInvariant();
}
sealed class PrefixRule : ITextRule
{
    private readonly string prefix;
    public PrefixRule(string prefix) { this.prefix = prefix; }
    public string Apply(string value) => prefix + value;
}

Expected output

tag:RUNE
Try this
Call the shared behavior through the interface instead of branching on concrete implementation types.