On the boundaries of the software system
Each software system has its boundaries: the places where input comes from and output is written to. If you follow a model like Hexagonal architecture (also known as Ports and Adapters) or Clean architecture, then you know that you need to introduce adaptation layers in the software. These layers serve a number of purposes. For example, using adaptation layers decouples the system from external dependencies (like database or UI), so that they can be replaced. It also becomes easy to test the system itself without setting up all external dependencies. For almost all of the systems, those are desirable properties since they reduce work and increase robustness.
However, there is also a rather profound difference between developing objects in core modules and in adaptation layers. This has to do with the fact that those adaptation layers face the outer world.
Read more: On the boundaries of the software systemInformation and behavior
To understand this distinction, we need to go back to the basics of information science. Logically, there are two big parts: information and behavior. Or, to tailor it to programming terms: data and functions that operate on data. From that definition, it might look like “data” is the most important; without data, no function can work. But in an object oriented world, the functions define the behavior of the class and the data parts is about the details. Data is usually encapsulated. So while it is true that data is necessary, OO design is done in terms of behavior. In a well designed program, objects call each other’s functions and methods, but they only use their own local data.
A question might arise though: how does an object get its data? The data is provided to the object, either by passing it to its constructor (so it can be stored as class member for later use), or as function parameters. If, for simplicity, we regard constructors as “functions that return an object”, then the data is passed as a detail of the function. Likewise, the function may return data, but it is the act of calling the function that is important.
Basic architecture pattern
Reading and writing to the outer world is basically done using an adaptation layer:
The adaptation layer shields the outer world from the software core. So it has to sanitize the data coming from the outer world, and also converting it to the abstractions used by the software core. This also works the other way around: when data needs to be output, the adaptation layer takes the software core’s objects and writes the resulting data to the outer world: a file, a network stream or something else.
Moving to the boundary
In an object oriented design, objects realize their behavior by calling functions of other objects. This is a fine concept as it allows modeling the program according to the real-world domain. There are also a whole bunch of design patterns that solve commonly encountered problems in a reusable manner.
However, when getting near the border of the system, things change. Sure, reading data can be done fine using the OO approach. For example, when you want to read an XML file from disk, then you can open a File
, then obtain the file’s input Stream
and feed it to an XmlParser
, which returns a nice XmlDocument
for you.
But for more complex scenarios, this is just not possible. Examples are:
- Serializing and deserializing objects to make their contents persistent
- Reading objects from a database using an ORM, which usually requires that the fields to be stored are marked with annotations.
- How UI frameworks (like WPF) tap into ViewModels to get the information to be drawn to the screen: WPF expect all dynamic data to be exposed as properties on the ViewModels. So if you want to draw a progress message, you need to expose it as a property on the ViewModel class.
The reason of this is simple: it is not the functions, but the data that is leaving the system. So any framework that helps you to do so requires the data itself and the objects are going to provide it one way or another.