✨ This article is part of a broader discussion in my book Safe by Design: Explorations in Software Architecture and Expressiveness. If you like it, you might enjoy the full read for free on GitHub.
Introduction
In many programming systems, there is a recurring need to instantiate objects from raw binary data. This is especially common in protocols or serialization layers where object reconstruction must follow a predictable contract. While object-oriented languages typically offer powerful polymorphic behavior through interfaces and virtual methods, constructors are notably not polymorphic. This limitation becomes apparent when you want to enforce that all subclasses of a given protocol or interface must implement a constructor with a specific signature — such as accepting a byte[]
and an index
.
The issue stems from the fact that virtual dispatch mechanisms rely on the existence of an already constructed object. Without an instance, the runtime has no way of determining which method implementation to invoke. As a result, constructors cannot be declared in interfaces or abstract base classes, and the compiler cannot enforce constructor conformance across all subclasses.
However, this limitation does not invalidate the desire to express such constraints. In fact, it signals a deeper need: developers often wish to encode architectural contracts into their type system. They want to guarantee, at compile time, that all types conforming to a protocol can be constructed from raw data. But without a dedicated language feature, this kind of contract enforcement must be emulated through design patterns and discipline.
This article explores how to achieve such compile-time guarantees using static constructs — specifically enums, fixed-size function arrays, and interface patterns. We’ll build a technique that offers the flexibility of polymorphic construction while maintaining type safety and performance. Along the way, we’ll uncover how this approach aligns with the Open-Closed Principle and can scale across large, extensible systems.
Protocols as Constraints and the Single Responsibility Principle
In statically typed languages, interfaces and abstract classes (collectively referred to here as protocols) serve as tools for defining polymorphic behavior. They describe what an object can do, enabling inversion of control and decoupling between components. However, developers often push these protocols beyond their original purpose — not just to describe behavior, but also to constrain the structure of the codebase.
For example, one might wish to ensure that all implementations of a protocol include a constructor with a specific signature, or that they are registered in a central factory. Since protocols cannot express such meta-level requirements directly, developers embed these constraints informally in documentation or enforce them via code reviews. But this approach is fragile and prone to errors, especially as a system grows.
This leads to a violation of the Single Responsibility Principle (SRP). A protocol intended to define polymorphic behavior ends up serving a second role: enforcing architectural discipline. This dual responsibility increases the cognitive load on developers and introduces risks when adding new subclasses.
What this suggests is that modern languages might benefit from a new, declarative construct: one explicitly designed for constraining code structure rather than just behavior. Until such features exist, developers are left to simulate them through alternative mechanisms. In the next section, we’ll explore one such mechanism — building a safe and scalable factory system that approximates “virtual constructors” through static dispatch.
Static Dispatch and Virtual Constructors
To simulate polymorphic construction in a type-safe and performant way, we can use a combination of static data structures and reflection. The idea is simple: for each type that conforms to a protocol, we register a factory function that knows how to construct it from raw bytes.
Let’s take a closer look at a real-world implementation in C#. The system defines a factory that reads a skillIndex
from a byte array, looks up a constructor for the corresponding type, and invokes it using reflection. To avoid repeated reflection overhead, constructors are compiled once and cached in a static array:
static class SkillFactory {
delegate ISkill SkillConstructorSignature(byte[] bytes, ref int index);
static readonly SkillConstructorSignature[] cachedConstructors =
new SkillConstructorSignature[(int) SkillType.SkillsCount];
public static ISkill fromBytes(byte[] bytes, ref int index) {
var skillIndex = (int)(SkillType) BufferUtils.readI2(bytes, ref index);
if (cachedConstructors[skillIndex] != null) {
return cachedConstructors[skillIndex].Invoke(bytes, ref index);
}
var type = SkillData.classes[skillIndex];
var constructor = type.GetConstructor(new[] {
typeof(byte[]).MakeArrayType(),
typeof(int).MakeByRefType()
});
if (constructor == null) {
throw new Exception($"No constructor from raw bytes for type {type}");
}
var newExpression = Expression.New(constructor);
var compiledExpression = Expression.Lambda<SkillConstructorSignature>(newExpression).Compile();
cachedConstructors[skillIndex] = compiledExpression;
return compiledExpression.Invoke(bytes, ref index);
}
public static void toBytes(ISkill skill, byte[] bytes, ref int index) {
BufferUtils.writeI2((short) skill.type, bytes, ref index);
skill.serialize(bytes, ref index);
}
}
This approach emulates a virtual constructor table, where cachedConstructors
acts as a dispatch vector. Each function in the array knows how to build a specific subtype from raw data. When a new skill type is added:
- It gets assigned a unique enum value in
SkillType
. - A corresponding class is added to
SkillData.classes
. - Its constructor with signature
(byte[], ref int)
is implemented.
If all three are done correctly, the system “just works.” If something is missing — like a constructor with the wrong signature — the code will fail fast and clearly at runtime. And with some compile-time scaffolding, we can catch these issues even earlier, as we’ll explore in the next section.
Compile-Time Control via Static Structures
One of the most common pitfalls in large, extensible systems is forgetting to update some central logic when adding a new type. For example, when a new subtype is introduced, a developer might forget to update a factory method or register the new class — leading to runtime bugs that are hard to trace. Ideally, the compiler would help us catch such mistakes.
Some languages, like Swift or Rust, enforce exhaustiveness in switch
statements. If you forget to handle a new enum case, the compiler will flag it. Unfortunately, mainstream languages like C#, C++, and TypeScript do not enforce such checks by default.
To work around this, we can encode the exhaustiveness requirement into the type system using a simple trick: by reserving a final enum case — commonly named Count
, TypesCount
, or Last
—and using it to size a static array. Here's how this works:
enum SkillType {
Fireball,
IceBlast,
Heal,
// ...
SkillsCount
}
Now we can create an array with a fixed size based on SkillsCount
:
static readonly SkillConstructorSignature[] cachedConstructors = new SkillConstructorSignature[(int) SkillType.SkillsCount];
This enforces an implicit contract: if you forget to add a factory function for a new SkillType
, the missing index in the array becomes a null slot, leading to a controlled failure at runtime. But more importantly, the structure of the code naturally forces the developer to “touch all the places” whenever a new type is added. You cannot forget to extend the array without breaking the program.
This is not just a runtime safety mechanism — it’s a static assertion embedded in the shape of the code. And since enum values are typically assigned incrementally from 0, array indexing becomes safe and efficient.
While it’s true that arrays don’t enforce ordering, and mistakes like index mismatches are possible, in practice such bugs are rare, easy to localize, and trivial to fix. Surprisingly, despite their simplicity, constructs like these are underutilized or outright unavailable in many languages that favor reflection or runtime registration.
In the next section, we’ll compare this approach with interfaces and explore when to use static dispatch, enums, or full-blown polymorphism.
📘 If you’re enjoying this so far, there’s a lot more in the book — same tone, just deeper. It’s right here if you want to peek.
Interfaces vs Enums: When to Use What
At first glance, enums, arrays, and interfaces may appear to serve different purposes. But under the hood, they often enable similar patterns: dispatch, constraint enforcement, and structural safety. Choosing between them depends not only on performance or readability, but on the type of control you want the compiler to enforce.
Let’s break this down:
Enums and Static Arrays
Best suited for data-centric dispatch. For example:
- You have a closed set of cases.
- You want the compiler to help ensure completeness (e.g., via array size or exhaustive
switch
). - You’re controlling external behavior, like parsing byte streams or serializing formats.
They shine when you want to associate behavior or data with a finite set of symbolic cases — especially when that set changes rarely and predictably.
Interfaces and Polymorphism
Best suited for instructional logic, encapsulation, and extension. For example:
- You want behavior defined per type, co-located with its data.
- You want to respect the Open-Closed Principle: new behavior through new classes, not edits to central logic.
- You’re building systems where responsibilities are distributed across modules or plugins.
They shine when types evolve independently, and when grouping logic with data improves clarity.
Gray Area: Polymorphic Static Data
Sometimes, you want each type to expose a static property, such as a type tag or category label. These values are shared across instances and differ between types. For example:
interface ISkill {
type: SkillType
}
Here, the type
acts like a "polymorphic static" value: it behaves like static data, but it’s accessed through instance-level polymorphism. This is often the only viable approach in languages that don’t support true type-level functions or compile-time constants per subclass.
Composability
Interestingly, both approaches can and often should coexist. For example:
- The enum
SkillType
can be used to drive deserialization. - The interface
ISkill
defines shared behavior and serialization logic. - A static array links
SkillType
to constructor functions. - The
type
getter onISkill
ensures round-trip consistency between the deserialized type and the serialized form.
This hybrid approach results in code that is extensible, type-safe, and compilable under constraints — a rare trifecta in dynamic factory systems.
Reflection and Performance
One of the main concerns with reflection-based solutions is performance. Traditional reflection — calling GetConstructor()
and Invoke()
—is notoriously slow, especially in tight loops or real-time systems. However, modern runtimes offer efficient alternatives by leveraging expression trees or dynamic code generation.
In our implementation, we use System.Linq.Expressions
to create and compile constructor delegates at runtime. This means the reflection step only happens once per type. The resulting compiled delegate is cached in a static array and reused for all future instantiations. This brings us close to raw constructor performance, with the flexibility of dynamic dispatch.
Here’s what makes this approach production-grade:
- First-use compilation: Constructors are compiled only once per type.
- Zero-cost dispatch after caching: After caching, constructor calls are as fast as manually written lambdas.
- Memory-safe: The array index (backed by an enum) ensures no accidental out-of-bounds access.
- Controlled failure: If a constructor with the expected signature is missing, the system fails early and visibly.
This technique gives you the best of both worlds: dynamic extensibility with static-like performance. More importantly, it respects the Open-Closed Principle: new subclasses can be added without modifying the factory, provided they follow the expected constructor contract.
From the consumer’s point of view, constructing a new ISkill
from a byte stream is a one-liner. But behind that one line lies a rigorous, testable, and efficient system of type-safe dispatch—one that avoids runtime conditionals, switch statements, or unchecked reflection.
Conclusion
By combining enums, static arrays, interface patterns, and compile-time invariants, we’ve shown that it’s possible to emulate polymorphic constructors in statically typed languages — without sacrificing performance or type safety. This approach offers:
- Scalability: Easily accommodates dozens or hundreds of subtypes without increasing complexity.
- Maintainability: Changes in one subtype do not require modifications to the core logic.
- Safety: Design violations are caught at compile time or on first use, not in production.
- Performance: Cached expression trees deliver constructor calls as fast as native methods.
More importantly, this technique aligns closely with core software architecture principles:
- It enforces Open-Closed Principle (OCP): systems can be extended with new types without altering existing code.
- It encourages Separation of Concerns: type creation, serialization, and logic are cleanly modularized.
- It allows developers to encode structural contracts — even when the language lacks native support for things like constructor constraints or static assertions.
In many ways, this pattern elevates the type system from a passive safety net to an active design enforcer. It nudges the compiler toward the role of a co-architect: not just catching errors, but shaping the way systems grow.
If you enjoyed this article, you might like my book Safe by Design: Explorations in Software Architecture and Expressiveness. It dives deeper into topics like this one — contracts, type safety, architectural clarity, and the philosophy behind better code.
👉 Check it out on GitHub