TL;DR: Defining a good domain model is often a challenging task. Raising the level of abstraction and moving to a domain’s meta-model instead can make your design more flexible.
Limitations of Object-Oriented Programming
One of my favorite programming blogs is Eric Lippert’s series Wizards and Warriors about object-oriented design. In this series, he discusses the limitations of defining complex business rules solely through OOP-based type systems. To illustrate some of these problems, he presents a small OOP model for a typical Role Playing Game system like Dungeons&Dragons. Such a system can have some of the following game rules, e.g.:
- A player character can either be a wizard or a warrior.
- A wizard can only wield a magic staff, but no sword.
- A warrior can only use a sword.
- A werewolf is invulnerable unless a warrior attacks it with a silver blade.
Even though these rules are relatively simple, implementing them with a class-based OOP system soon reaches a limit. Take, for instance, rules 2 and 3: While we could use covariance to override the weapon’s property in derived classes to accept more specific weapon types, this approach is not type-safe. Casting a wizard to a player and giving it a sword would compile but throw an exception during runtime.
Rule number 4 reveils another problem: Determining the correct method to call depends on two factors, the method’s implementor, and the parameter’s type. Most languages only support single-dispatching, so we need a workaround to express this rule with our type system. The Visitor design pattern would be an option. Still, it’s not necessarily an elegant solution as it makes the code harder to read.
As Eric states in his blog, the root cause of our problem is relying too heavily on our language’s type system to implement our business constraints. But a type system is a general tool and not well suited to solve such specific problems. When we use it to encode complex domain rules, we operate on the wrong abstraction level.
He, therefore, suggests a different approach: Instead of using types to define business concepts, our class model should focus on the core concepts of our domain: The main essence of an RPG system is that it defines a game state and a set of rules that validate and alter this state. Our implementation should evolve around these higher-level concepts rather than trying to express the low-level details of the system.
Although he does not state it explicitly, with this approach, he raises the abstraction level of his implementation by moving one step up in the model hierarchy. Stepping up the model abstraction ladder is an interesting strategy: It converts static concepts like classes or inheritance into editable runtime components, making the system much more flexible.
How? Well, to answer this question, we must first dig a bit deeper into models and their hierarchies…
Models and Meta-Models
Our real world is very complex, with many interactions and variables - too complicated to cover all its aspects in a software program. To be still able to write programs that solve our business problems, we have to create a more simplified representation of our world - a model. Which parts of the world we include in our models depends on our concrete use case.
When writing a program, we use object-oriented or domain-driven design1 concepts to create such models. The interesting part is that a program consists of more than one model. There is one expressing the runtime behavior and data and another one for the design - the Object Management Group OMG specifies at least four different model layers2:
For this blog, we only focus on the first three layers, as the fourth one is seldom used by ordinary mortals like us.
M0: The Instance or Object Model
This is the most specific model in our hierarchy. It consists of all objects our program instantiates and all its data during runtime. Following our introductory RPG example, elements in this layer would be concrete character instances like Gandalf the Gray (or White) representing a wizard and a warrior instance called Aragorn (ok, technically, he’s a ranger, but that’s nitpicking…).
All parts on this level are very dynamic: We can create and destroy them at runtime and change most of their attributes, like health points or a warrior’s current weapon.
M1: The Domain Model / User Defined Model
The OMG uses the term User Defined Model for this layer, but calling it Domain Model is also quite common. I prefer the name Static Model as it best describes this layer’s characteristics: It contains the compile-time classes and types we use to describe our domain concepts. It’s safe to consider it the core of our application. Using this abstraction layer for our program makes sense. It is the most natural way how we recognize and describe our environment. We identify entities in our world by their behavior and categorize similar ones into the same groups or types - the main idea behind object-oriented design.
Since all objects of the M0 model are instances of classes defined in this layer, we can also say our whole M0 model is an instance of our M1 model. This relation holds for the entire hierarchy - each model is an instance of its parent model.
However, as we’ve already seen, using this abstraction level for our domain has some drawbacks: Classes are static and cannot be changed during runtime anymore (unless you use some technique like reflection or introspection). The only option to change a mage to a warrior would be to destroy the object and recreate it using a different type - a terrible representation of real-world behavior. In addition, as we’ve seen, to guarantee the constraints of our domain, we can rely only on our type system’s limited capabilities. It might be worth reconsidering whether this is the layer best suited to model our domain.
The world may be more dynamic, as we could express with this model layer…
M2: The Meta-Model
This is the part where it gets interesting: Meta-Models are models that describe other models. While this may sound odd, it just conforms to what we have already done. Our M0 contains the objects our application creates at runtime. The following level, M1, specifies how our domain objects can be constructed and what they look like. Moving another step up the hierarchy means we reach a model describing the parts our domain is composed of.
The models also become less specific with increasing levels: Staying with our RPG example, our M0 instance conforms to specific lore, like Lord of the Rings or Dungeon and Dragons. The M1 model is more general as it describes the typical components each fantasy RPG system could have (warriors, wizards, monsters, etc.). While it’s still bound to a concrete domain (fantasy), we could still use it to create different fantasy systems.
But what would our meta-model then look like?
Our meta-model M2 would be the blueprint for creating different RPG systems. This can be a fantasy system, a science-fiction system like Cyberpunk, or anything you can think of, as long as it follows the general RPG concepts we define on this layer.
Instead of a Warrior or Wizard class, we have a Profession that describes the role of a character. Rules are another core concept of an RPG system, so our meta-model would also have a class for that. Depending on the system, characters can execute different actions (like attacking, equipping a weapon, hacking a robot and so on), so our model should also have a class representing an arbitrary Action.
The OMG defines another model level, M3 (the Meta-Meta-Model). However, I prefer sticking only to the first three levels as they are fine for most use cases. Operating on such high abstraction levels like M3 can cause terrible headaches3.
Okay, but how does this help us overcome the limitations of our OOP type system? As mentioned, with all its rules and regulations, our domain model was too complex to implement with OOP concepts. So why not take a less complicated model and build our application around that? Instead of implementing the domain model, we could try our domain’s meta-model. We would have a single class for professions and two others for actions and rules. Before executing such an action, we validate our rules to ensure our system behaves correctly.
An instance of this model would have concrete objects of the Profession class, like warrior or wizard. Changing a character’s profession becomes simple - we simply have to let it point to a different profession object.
The same works for rules: At runtime, we instantiate an Attack action that has to validate a DoesWarriorAttackWerewolfWithSilversword rule object to determine the exact outcome of the action. Another action, EquipWeapon, would have to check whether a character can use a given item before equipping. Using this approach, we can easily implement all RPG constraints from the beginning of the post.
You could even introduce scripting languages like Python or Lua (or your own DSL) to implement the rule logic. In that way, actions and rules become your application’s data: Changing them can be achieved without recompiling your code base.
Also, this approach is not limited to RPG systems: Moving the hierarchy up means everything that used to be a class becomes an object, and former static relations become dynamic links. Whenever our domain model becomes too static and adding a new behavior or concept requires too many code changes, we could reconsider our design: What are the essential components and conventions upon which our domain is built? Can we base our primary code model on these instead?
UMLBoard, for instance, follows a similar approach: Its internal domain model uses a hybrid model based on the UML model and meta-model. This makes it easy to change the type of a relation at runtime or convert a classifier to an interface and back.
If using meta-models makes a design so flexible, why don’t we use them all time? Well, as always, there are some things to consider:
First, identifying the meta-model concepts you need for your specific use case is not always trivial and requires more profound domain knowledge. In the worst case, you end up with a model so generic that you could implement any possible system with it. While very flexible, working with such a model would be unwieldy as you would have to define everything at runtime.
Also, remember that you’re trading type safety with runtime dynamics. Since your business constraints are now objects, you can no longer rely on the type system to find errors - instead, most of your checks have to happen during runtime.
Sometimes, the type system is not powerful enough to express all your domain rules and restrictions. Raising your level of abstraction and basing your implementation on your domain’s meta-model instead allows for more flexibility in your design. Concepts formerly encoded as classes and methods can become objects you can edit during runtime. Hardcoded logic is converted into application data and can be loaded and modified while your program runs. However, switching the model layer requires deeper domain knowledge, as you have to analyze the main components behind your domain. Also, this strategy might not work for all domains, but it’s definitely worth a try.
What’s your approach when designing your domains? Did you ever had to change or redesign a domain model that was too inflexible?
Please share your thoughts and experiences in the comments or via Twitter (@UMLBoard).
Title image provided courtesy of D20 Collective.
- A good resource for Domain-Driven-Design: https://www.domainlanguage.com/ddd/↩
- Meta-Modeling and the OMG Meta Object Facility (MOF):
- Obviously, I’m not the only one getting confused by too many meta-levels: