Bertrand Meyer: Object-Oriented Software Construction
Table of Contents
Model Principle (MP)
Variants and Alternative Names
Context
Principle Statement
The object structure of the software should model and mirror those concepts and actions of the real world, that the software supports.
Description
The software should model and mirror the “real world”. This first of all means, that the structure of the software—to some extent—models the structure of the problem. When the “real world action” that the software should support comprises certain entities like e.g. customers, products, and orders, then there should be one object for each customer, product and order. Furthermore there should be one class for each concept. And if there is a certain relationship between customers, orders, and products, there should also be an association between the corresponding classes and references between the objects. So the object structure models the structure of the real world concepts.
Real world actions are then mirrored in the software system. This means that each action in the real world triggers a corresponding action in the model world which ensures that the model stays consistent with the real world. So the software is a kind of a simulation of what actually happens. If customer orders some product, the software reacts by creating an order object, which is connected to the customer and the product objects corresponding to the customer and the product involved in the real world action.
Be precise with semantics. An operation cancelOrder()
should get an Order
or OrderId
as a parameter. In some cases you might be inclined to supply a Product
instead, maybe because you already have a variable with that object at hand. This might work totally fine in the concrete situation but in fact it only works “by accident”. Similarly you might be inclined to invoke cancelOrder()
in a situation when you want to delete the order from the repository. This might work as expected but actually cancelOrder
is semantically slightly different from deleteOrder
. So even if cancelOrder
currently does nothing but to call deleteOrder
you should call the method on the correct abstraction level that has precisely the right semantics.
Keep sure that your software models the reality by invoking the method that has the correct semantics and supplying it with the parameters that are needed from a requirements perspective.
Rationale
When the structures in the software roughly correspond to the structures of the problem domain, a developer doesn't have to learn both of them. Knowing the problem domain is inevitably necessary. Any further structure of the software has to be learned and understood in addition. So creating a direct mapping between them, makes understanding the software easier, which improves maintainability. In such a system for most functionality there is a “natural”, i.e. an intuitively clear place to implement it. This makes structuring the software easier and helps finding the implementation for a given functionality.
Moreover if something works accidentally, it breaks accidentally. Many many bugs are created because you are not precise with semantics.
In the example above supplying a Product
to the cancelOrder
method only works because by some circumstance it is made sure that when cancelOrder
is called, there is only one order for that particular product. It's a hidden precondition. As the software is changed, this hidden precondition may not be guaranteed anymore. This results in a non-obvious bug in a part of the system you haven't directly touched. Similarly in the deleteOrder
example: The cancelOrder
operation might get enhanced by creating a reverse invoice, a credit note, or a message to the customer. But an order might have to be deleted for purely technical reasons (migration to a new order system, etc.). So if you call cancelOrder
instead of deleteOrder
this will produce nasty bugs if cancelOrder
gets enhanced as described.
Strategies
- Create a class for each relevant real-world concept (“natural classes”)
- Create methods corresponding to real-world actions
- Map additionally necessary behavior to natural classes instead of creating artificial classes
- For artificial behavior that cannot be mapped to a natural class at least create a metaphor or an artificial model (like a state machine)
- Be precise with semantics. If you have an operation that currently does what you need but for slightly different reasons because it's an operation on the wrong abstraction level, create a new operation with the correct semantics. Have that new operation call the existing one as an implementation detail (e.g. have a
cancelOrder
method call thedeleteOrder
method).
Caveats
This principle may lead to the problem of modeling the real world in too great detail. This complicates the design without giving any further benefits. Especially a wrong understanding of inheritance may lead to taxomania, where an inheritance relation and the respective classes are only introduced because there seems to be such a taxonomy in the “real world”3). But inheritance should only be used on purpose, not just because it is possible. A special form of this problem is called vapor classes, which are useless abstractions which are never used4).
See also section contrary principles.
Origin
The root of this principle is the very beginning of object-orientation itself. The idea behind Simula, the first object-oriented programming language, was to view program executions as simulations. Kristen Nygaard, one of the creators of Simula defines object orientation as follows:
“Object-oriented programming. A program execution is regarded as a physical model, simulating the behavior of either a real or imaginary part of the world.”5).
Although this view is disputed as a definition for object-oriented programming, it became the key idea of object-oriented analysis. In 6) Grady Booch clearly states that objects “directly reflect our model of reality”.
Evidence
- Accepted: Virtually every introduction to object-oriented analysis roughly explains this but mostly without stating it as a principle. Bertrand Meyer explains this principle in his book Object-Oriented Software Construction
- Questioned: The value of this principle is disputed. It is questioned whether objects in the OOP sense nicely map to real-world objects7). Furthermore there is the typical object-relational impedance mismatch8) and the observation that business rules are sometimes cross-cutting9). There also is not one single obvious model for the “real world”. A model is subjective to the one creating the model. So it is not enough to model the “real world” but it is important to think about how to model it10).
Relations to Other Principles
Generalizations
Specializations
Contrary Principles
- Encapsulate the Concept that Varies (ECV): Sometimes there are “concepts that vary” which are not directly related to a real-world concept. So ECV demands having an artificial class.
- Keep It Simple Stupid (KISS): There are often simpler ways to build a software system than to model and mirror the real world behavior, which frequently means having more objects and more complicated structures.
- Single Responsibility Principle (SRP): Following the Model Principle sometimes results in classes having more than just one responsibility.
- High Cohesion (HC): MP sometimes creates classes with suboptimal cohesion. See also SRP.
Complementary Principles
- Tell, don't Ask/Information Expert (TdA/IE): TdA/IE tells how to distribute functionality among the natural classes which are created according to the Model Principle.
- Low Coupling (LC): When designing a model for a software, it has to be borne in mind that structures with low coupling are desirable.
- Law of Leaky Abstractions (LLA): When building abstractions according to MP, keep in mind that there will most likely be abstraction leaks. A good abstraction minimizes those leaks.
Principle Collections
Examples
Example 1: Object Structure (Library)
In a software system for a library, there will be a classes like Book
, Reader
, and Lending
. A reader has a name, a book has a title and the reader must return the book after some date of expiry. So the corresponding classes will have attributes describing these properties. The reader may borrow and return a book, so the Reader
class will have methods borrow()
and return()
. Classes, attributes, and methods are directly inferred from the problem domain.
Example 2: Swing
GUI frameworks like Java Swing typically have classes corresponding to the types of controls that can be used to build graphical user interfaces. So Swing for example has classes like JButton
, JCheckBox
, and JTextField
.
Furthermore buttons, check boxes, text fields, and the like are also models of concepts in the real world. Buttons are typical controls of machines and check boxes and text fields are parts of a typical (paper-based) form. So the class JCheckBox
is a model for a check box on the screen which itself is a model for a check box on a paper-based form.
Example 3: Dependencies
MP also tells which modules may depend on which others. Suppose we have a software comprising a parser for mathematical functions. Obviously there will be classes Parser
and Function
. MP tells that dependencies between these classes shall be according to the model. Logically a parser parses a string and creates Function
objects. It is impossible to think about a Parser
without Function
s. So Parser
may naturally depend on Function
.
On the other hand our intuitive model of parsers and functions tells us that a Function
does not need a Parser
to be a meaningful entity. One can easily think of Function
s created by using builder functions instead of a parser. And even if that wasn't true and there would only be the possibility to create functions by using parsers, a Function
object logically can work without knowing that there are parsers which have created it. In an imaginary hierarchy of modules Parser would be a module on a higher scale than Function
. So MP forbids that Function
depends on Parser
.
Example 4: Brake and Air Conditioning
Suppose a car has an air conditioning and a hill start assistant. The air conditioning needs to make sure that the engine provides enough power on sunny days. So it measures its power-consumption and pushes down the gas pedal just enough to the engine isn't stalled. The hill start assistant automatically releases the hand brake if you start driving. Now the following situation can happen: A car waits in front of a boom barrier of an underground garage. It's a hot day and the driver opens the window to get the ticket. hot air flows into the car and the A/C powers up. The revolution speed is low because the car stands still so the A/C hits the gas pedal in order not to stall the engine. Now the hill start assistant realizes that the gas pedal was pressed and releases the hand brake because pressing the gas pedal is the trigger that the diver want to drive away. As a result the car crashes into the boom barrier.
The problem here is with the A/C. Semantically it wanted to increase the motor power but actually it called an operation that hit the gas pedal. This is almost the same but not exactly the same. It's an operation on the wrong level of abstraction. If the A/C had called an operation increaseMotorPower
instead of an operation hitGasPedal
the problem would have been prevented.
Example 5: Inferring Information
Suppose there is a smartphone app and its backend. The app lets you buy several products. One day the app developers demand that the backend adds a field describing the product size (small, medium, large). The backend developers add the field and everyone is happy. The app developers use this field so they can decide whether gift wrapping is available (large products cannot be gift-wrapped). Half a year later the company decides to enhance their product portfolio by reselling products of company B. The backend will redirect orders of B-products directly to company B so they are in charge of delivery.
Unfortunately B is not able to provide gift-wrappings at all. So the smartphone app has to be changed such that it not only removes the gift-wrapping option for large products but also for B-products. But smartphone apps need to be updated by the customer (and some of them never do). So the only way to ensure that gift-wrapping is handled correctly even if the customer hasn't updated yet, is that the backend returns size:large
for every B product (even for small ones).
The size
field gets deprecated and a new field giftWrappingAvailable
is added. In fact that's what they should have done in the first place. The app developers needed to know if gift wrapping is available. But instead they inferred this information based on the size. This worked but it worked accidentally—and it broke accidentally.
Description Status
Further Reading
- ProgramsRepresentMentalModels (general discussion on mental models)
- SystemMetaphor (metaphors in extreme programming are a specific kind of model as discussed here)
- TheValueOfDomainModels (general discussion on domain models)
- Martin Fowler: AnemicDomainModel (the result of not properly adhering to the principle)
- Martin Fowler: Domain Logic and SQL (other possible to implement a system and their difference to domain models)
Discussion
Discuss this wiki article and the principle on the corresponding talk page.
Bertrand Meyer: Object-Oriented Software Construction