On Object Soup
Object-oriented programming has been with us for a long time, as have the languages that support it. Early languages with support for object-oriented programming include Simula (late 1960s) and Smalltalk (late 1970s/early 1980s). I was first exposed to object-oriented programming when I was learning C++ in the early 1990s. After several false starts trying to learn from mediocre C++ books I stumbled upon Stephen Prata’s C++ Primer Plus (now in its fifth edition; it very much deserves the strong Amazon reviews). Prata’s book teaches OO fundamentals through C++, rather than teaching C++ and assuming you’ll figure out OOP somehow along the way.
I spend most of my coding time in Java these days, where object-oriented techniques and concepts are brought to the forefront right away. Even a simple “Hello, World” program in Java requires you to define an object class.
Although I understand the OO philosophy, I have only ever partially embraced it. I think that fully following the OO philosophy can drive systems toward tightly-coupled designs that are extremely difficult to maintain and refactor.
As a first example, consider three-tier systems. The traditional tiers in a three-tier system are the data tier, the business logic tier, and the presentation or user interface tier.
The diagram above shows the basic idea: the data tier is just a data store; it has minimal behavior other than the ability to store and retrieve data. The business logic tier is responsible for using those store and retrieve operations to manipulate the data according to some desired application semantics. The business logic tier then provides one or more programmatic interfaces to the presentation tier, which is responsible for implementing the user interface and translating user actions into invocations on the business logic tier. Although there are many slight variants that can still be called “three-tier” architectures, one constant feature is that the dependencies go one way: the data tier depends on nothing, the business logic tier depends on the data tier, and the presentation tier depends on the business logic tier. You should, for example, be able to remove or replace the presentation tier without making a single change to the other two.
This decoupling of presentation, business logic, and data storage is a well-worn application of the more general principle of separation of concerns. It allows us to do all kinds of neat things like add a second presentation layer (perhaps a Web-based presentation layer) to the system, making few (if any) changes to other parts of the system. It allows us to evolve system semantics and data schema at different rates: I can add new semantics to the business logic layer without changing the data schema at all (if the new semantics don’t need any new data to be stored). Indeed, the three-tier architectural style is one of the least objectionable inventions in the software engineer’s toolbox.
From a pure OO perspective, however, three-tier architectures violate basic OO principles, especially in separating application data from business logic. One of the core tenets of OO is to couple data and the computations that act on that data together in the objects. When implementing a three-tier system in an OO programming language, the objects in the data tier tend to be very simple: they are primarily data structures with minimal get/set methods to access the data members. The application functionality lives primarily in separate business logic objects, which just pass data structures around as parameters.
In a typical three-tier system, you might see this:
ValidationResult r = CredentialValidationService.validate(userCredential);
But in a truly OO system, this would be avoided in favor of something like:
userCredential.setValidationService(validationService); ... ValidationResult r = userCredential.isValid(); //validates credential if necessary
I will admit that the latter seems somewhat more elegant than the former. However, it has fundamental implications. For example, the concept of a credential is now dependent upon the notion of a validation service, and a credential cannot fully be defined without also defining the validation service.
More importantly, this makes implementing a distributed three-tier system (where the tiers may be separated not just by logical, but by physical and network boundaries) a nightmare. Now, the simple credential object has an internal pointer to a large credential service. While the credential itself may be easy to serialize and pass across process boundaries or store in a database, the credential validation service cannot be. This puts a large burden on the programmer to do things like mark the credential validation service as ‘transient’ (i.e., not serialized) and to find it and hook it back up to every credential whenever one is deserialized or passed to the machine that can actually perform credential validation. In the three-tier system, the credential and the validation service are very loosely coupled; in the OO system, they are tightly coupled.
Another example: recently, I was working on an open-source system that simulates a network of several small computers. The simulation was single-process, and I needed it (for scalability reasons) to be multi-process and distributed, so the simulated computers could actually be simulated on separate machines. The real-life counterparts of these simulated computers are truly, physically distributed, so I figured that adapting the simulator to be distributed would be fairly straightforward. At first glance, the simulator looked extremely well-architected, and in many ways it was. It was very easy to understand how the various parts interrelated, because there was a nice dependency hierarchy.
When I looked at the code, I saw something like this:
Distributing the simulation, then, seemed straightforward: I would split the “head” of the tree, putting one application and simulator object/component on each host, having each responsible for one or more simulated computers, and then I would split the communication channel into stubs and skeletons, hiding the networking code inside it like an architectural connector.
As I began to do this, I ran into a mess of trouble, because I had not noticed that the system’s developers had really understood and embraced OO techniques. What the system actually looked like was this:
The two things I had missed were that:
- Each object in the “tree” had a pointer back to its parent object; and
- The message, communication channel, sender, and receiver all had pointers to each other.
This is not so bad in itself, but the application code followed and used these pointers extensively. So, if the receiver’s network device needed some data about the sender’s network device, it would do something like this:
senderParameter = getReceiver().getChannel().getSender().getParentNetworkDevice().getSomeParameter();
Additionally, the message object, which I assumed would be a simple packet of data with a few simple get/set methods, turned out to be a traveling software component itself, dereferencing its own pointers to both the sender and receiver and shepherding itself across the communication channel, to which it also had a pointer!
Everything was coupled to everything else! Separating the two “halves” of the application onto different hosts was like surgery to separate conjoined twins: critical organ systems had to be carefully severed and the dangling connections needed to be sewn up and redirected one-by-one. Parts of the application could not simply reach across half-a-dozen other components to grab data about another part; that data had to be passed explicitly and in advance. Dependencies had to be refactored out. The entire process was very painful.
I am not sure that this was a bad design from an OO perspective, though. Here, data was kept conceptually with the object that needed it, and the operations to manipulate that data were kept alongside it.
Architectures like this are what I call “object soup.” In object soup, everything is mixed together, one object coupled to the other in a tangled web of pointers. Encapsulation exists within objects, but is not reflected within higher-level concepts like layers or tiers. Simple data objects have direct dependencies on large computational services, rather than the other way around. Separating any piece of the system from any other is difficult.
Object-oriented techniques can be powerful tools, applied judiciously. But for large, complex systems, pure OO styles may not elicit the desired properties. Instead, it’s important to leverage OO’s strengths (such as local encapsulation) amidst higher-level styles, patterns, and constraints that provide encapsulation and separation of concerns above the level of individual objects.
September 21st, 2010 at 12:15 pm
Your article is very informative and specifies a very real problem. However, it also seems that you are indicating that this problematic “object soup” has some direct connection to object oriented programming. I don’t believe that it does.
In my opinion, this “object soup” violates a principle called information hiding. In the three-tier architecture you describe, information hiding is explicit in the architecture. In “object soup” information hiding is not part of the overall architecture.
It is true that an OO principle of design is “… to couple data and the computations that act on that data together in the objects …”. Yes, this is true in general. However, you are still suppose to design things. For example in Arthur J. Riel’s “Object-Oriented Design Heuristics” book, states many heuristics which the OO application here violates, such as:
heuristic 4.13) A class must know what it contains, but it should never know who contains it.
Design, in the abstract, is reducible to principles, guidelines, etc. However, a designer is still expected to use these principles, guidelines, and judgement. It is perhaps why designers will probably never be taught entirely from a list of rules.
Thank you for your article, I enjoyed reading it.