Operator Overloading with Interface-Based Programming in C
Master System Design with Codemia
Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.
Introduction
Operator overloading and interface-based design solve different problems in C#. Interfaces define contracts and substitution points, while overloaded operators are implemented only on concrete types. The usual pattern is to design around interfaces for abstraction and testing, then place operators on immutable value-like implementations where the semantics are obvious.
Operators Belong to Concrete Types
C# does not let an interface define operator implementation in the same practical way that a class or struct does for normal domain models. That means an operator such as + is attached to a concrete type, even if the rest of the system depends on an interface.
A common example is a money type.
The interface tells the rest of the program what an amount looks like. The concrete Money type provides the arithmetic behavior.
Why Immutability Matters
Operator overloading is easiest to reason about when the operands behave like values instead of mutable objects. If a + b mutates a, the code becomes surprising immediately.
That is why overloaded operators are usually best on:
- immutable classes
- immutable structs used carefully
- domain types with clear mathematical meaning
For domain objects with identity, lifecycle state, or side effects, operator overloading usually makes the code less clear rather than more expressive.
Keep Method and Operator Semantics Aligned
If you provide both a normal method and an operator, they should mean the same thing.
If Add and + behave differently, the type becomes harder to trust. The operator should not be a shortcut for a different rule.
Use Interfaces in the Service Layer
The service layer can still depend on interfaces even when operators live on concrete types.
In other words, interface-based design and operator overloading are not opposites. They just operate at different levels of the model.
Interfaces help with composition, dependency inversion, and testing. Operators help with local expressiveness when the concrete type really behaves like a value.
Equality Must Stay Consistent
If you overload operators such as == and !=, they must agree with Equals and GetHashCode.
If equality is inconsistent, collections, caching, and comparisons start behaving unpredictably.
Common Pitfalls
The most common mistake is expecting an interface to carry the full operator behavior. In practice, the operator still lives on the concrete implementation, so callers either need that concrete type or a method-based abstraction.
Another issue is overloading operators for types that do not have obvious mathematical semantics. + on money is natural. + on a complex mutable service object is usually not.
Teams also often skip invariant checks. A money addition operator that ignores currency mismatch is concise but wrong.
Finally, do not add operator overloads only because the language allows them. Use them when they make the code more readable for everyone, not just for the type author.
Summary
- Interfaces define contracts, but operators are implemented on concrete types.
- Operator overloading works best on immutable, value-like domain types.
- Keep method-based and operator-based APIs semantically identical.
- Enforce domain invariants inside every overloaded operator.
- Use operators only when the meaning is obvious and stable to readers.

