The fading of the purely object-oriented view – since 1990

https://bit.ly/2WG39HF   http://avancier.website

 

Surprisingly little is new in the word of software design.

Discussions in Linkedin, January 2019, promoted me to put on record that:

·         agile development principles are older than programming

·         software system theory is older and broader than “object-oriented thinking”

·         design tradeoffs related to decomposition and decoupling are eternal and universal.

 

The aim here is also to propose that balancing tradeoffs is more important than following uni-directional principles

And we ought to look again at system theory in the broad, rather than from a purely “object-oriented” angle.

 

Contents

Background. 1

The emergence of OO programming languages. 2

Early OO thinking – and challenges to it 3

Common OO principles - and tradeoffs. 6

Decoupling and SOA.. 7

Decomposition and microservices. 8

Conclusions. 8

 

Background

The general principles of software system design inform OO thinking, but stand independent of it.

The system of interest here is a software system.

It is a discrete event-driven system.

It responds to input events (messages) by performing operations.

It is deterministic; it commonly determines what to do by comparing input data against persistent state data (memory).

The memory takes the form of a data structure that relates data types of interest to users of the system.

 

A system of this kind can be described from various viewpoints, as shown in this table.

 

 

Behavioral view

Structural view

External view

Operation contracts

System interface

Internal view

Operations

Subsystems

 

The previous paper introduced some general design tradeoffs that are relevant to agile software development

It is presumed that readers of this paper have read the previous one, including the notes on

·         Decomposition and decoupling

·         The simplicity v flexibility trade off

·         Where to specify and code business rules?

This paper adds notes on basic “object-oriented” principles, and their limitations.

The emergence of OO programming languages

COBOL was designed for business applications that process large data structures.

It was supposed to be readable (if verbose) and maintainable.

People wrote un-maintainable code – due to the lack of disciplined modular design.

 

OO programming languages came to prominence at the end of the 1980s.

OOPLs were designed for applications that process small data structures.

They were supposed to be elegant (non-verbose) and efficient - and maintainable.

People still wrote un-maintainable code – partly due to the difficulty of processing large data structures.

 

The first books and papers on OO software design (e.g. by Grady Booch, and by Shlaer and Mellor) included case studies.

In some case studies, the OO program’s objects represented the stateful objects (sensors and motors) of a real-time process control system.

When a sensor object detected an event, it cooperated with other domain objects to process that event.

In other illustrations, the OO program’s objects represented the stateful objects (boxes, buttons, sliders) in a graphical user interface.

In these case studies, only a few small stateful objects were needed to maintain the few state variables the system needed to operate.

 

Around 1990, OOPLs started to gain traction for coding the server-side subsystems of business applications.

A business database schema might contain hundreds of data types (and in operation, millions of data instances).

The CIO of a company where I worked as a consultant at declared “we shall be fully object-oriented by the year 2000”.

Naive talk of this kind led some to rewrite perfectly good database applications in C++ or Java, for no obvious benefit.

 

Aside: C++ is said to be one of the most complex computer languages.

See a famous spoof Stroustrup interview here: http://www-users.cs.york.ac.uk/~susan/joke/cpp.htm

However, the interest is here not OOPLs, it is the “OO thinking” that came with them.

Early OO thinking – and challenges to it

Software systems are usually decomposed or modularised into subsystems (aka modules, components or classes).

OO programming differed from earlier kinds of modular programming in three ways:

·         You can deploy many instances (objects) of a module type (class).

·         Clients use an object identifier to locate the module instance (object) they want

·         Objects of one class can inherit or extend the operations of more generic (perhaps entirely abstract) class.

 

The classes and objects of an OO program can be regarded as the atomic subsystems of a software system.

A class is an abstract subsystem (a description) and an object is a concrete subsystem (a realisation).

 

Bear in mind that a system can be modularised in infinite different ways, event-oriented, entity-oriented or a mix of both.

Object-oriented thinkers tended to favour entity-oriented objects.

In 1972, Parnas had written that a good module should hide any internal data, its internal structure, its creation and update procedures.

Similarly, in 1988, Meyer proposed a class should be an abstract data type, which encapsulates, and defines the operations performable on, a data structure.

 

Atomic subsystem identification?

Early OO thinking: an object is identified by an object identifier.

In a world of increasingly distributed systems, assignment of object identifiers across the web is a non-starter.

Later SOA thinking: even the smallest components can be identified by domain names.

 

Reuse by inheritance?

Early OO thinking: reuse is achieved when an object of one class performs the behaviors of a super type class.

The limitations of inheritance as a tool for reuse soon became evident.

It isn’t just that inheritance between classes distributed across the web is a non-starter.

Or even that deep inheritance trees can make it hard to see/remember what an object of one class will do.

In practice, software designers often use parameterisation rather than inheritance.

And modelling business operations is not like modelling geometrical shapes.

The persistence and fuzziness of real world entities turns subtypes of an object into states or roles of an object, and inheritance relationships into associations.

 

Later SOA thinking: reuse is achieved when two or more kinds of client object call one server object to do something. 

(Here, a client object is dependent on one or more server objects, to which it delegates work.

And a server object is a dependency of one or more client objects, to which it provides services.)

 

In “Design Patterns”, Gamma etc. promoted composition over inheritance.

A composition contains a client object and (in its state) the server objects it depends on.

Via successive composition, a composition can contain a complex graph of client and server objects.

To quote from a post in Linkedin "large OO programs struggle with increasing complexity as you build this large object graph of mutable objects.”

Aside: on dependency injection

Designing a flexible client implies making minimal assumptions about its dependencies/server objects.

And designing those server objects (when acting as clients) implies making minimal assumptions about their dependencies... and so on.

 

How to increase the pluggability/configurability of the software configuration?

How to shield a client from changes to the internal implementations of server objects?

How to replace server objects without changing client objects?

You need to prevent a client from locating or building its own server objects.

 

Dependency injection is a pattern or technique that plugs server objects into client objects.

First, the client needs interfaces that define how to use server objects.

These interfaces must be defined independently of both client and server, referenceable by both.

(Of course, if an interface changes, things tend to break – at compile-time or run-time or both.)

 

Then, you need an injector object that constructs server objects and injects them into a client.

The injector can give the client a new version of a server object, without the client knowing.

The injector may also construct the client.

It may build a complex object graph by treating an object first like a client and later as a server.

 

Bear in mind that designing for flexibility (in this and other ways) tends to increase complexity.

 

Domain-oriented design?

Early OO thinking: entity-oriented domain objects interact to complete a process, with no need for a process object.

But given that a software system is a discrete event-driven system, the event-oriented view is as important as the entity-oriented view.

And by the end of the decade, more value was placed on understanding a whole event procedure or transaction.

In Domain-Driven Design (2003), Evans gave the responsibility of managing transactions to aggregate entities in the domain model.

Others use some kind of controller object to orchestrate the domain objects involved in a transaction or other process.

 

Martin Fowler observed that Domain-Driven Design is difficult to learn and best reserved for complex systems with “rich domain models”.

(Where rich probably implies substantial use of inheritance.)

In the 1980s. the UK government’s structured design method (SSADM) had transformed an object-based domain model into event-oriented procedures.

In 2002, Fowler wrote that event-oriented “Transaction Scripts” are fine for many enterprise applications.

Both sources presumed reuse would be achieved by delegation to subroutines from event procedures or transaction scripts.

 

Synchronicity?

Early OO thinking: clients and servers interact synchronously.

A client object makes a request to a server object and waits for a reply.

A server object accepts only one invocation at once (it “blocks” parallel invocations).

 

If speed and simplicity is the goal, client objects can make synchronous invocations to server objects.

But it is difficult to distribute a system built this way, and it may not scale up if a high throughput is needed.

Later SOA thinking: the message passing mechanism will be asynchronous rather than synchronous request-reply.

For some, asynchronous messaging implies the need for middleware; for others (e.g. Martin Fowler) this is questionable.

 

One name space?

Early OO thinking: client and server objects work in the same name space.

Some envisioned that the world’s software objects could, even would, be joined up in a global distributed OO program.

Around 1999, I reviewed an application whose Java developers had distributed c80 domain object classes to different application servers.

To complete any non-trivial update process involved using an object request broker to get from one application server to the next.

 

Eventually, OO thinkers recognised that systems cannot be designed on the presumption that distributed objects occupy one name space.

As Martin Fowler wrote in 2002, the first rule of distributed objects is – Don’t distribute your objects!

Later SOA thinking: Microsoft proposed messages should be conveyed in self-describing data formats.

And distributed subsystems should be invoked via WSDL-defined interfaces (or later, using REST principles).

 

(Much like OO thinkers before them, Microsoft envisioned a global software system.

The world’s applications could, even would, locate reusable server systems using their UDDI server in California.

Eventually, they abandoned this unrealistic idea.)

 

Statefulness?

Early OO thinking: objects are stateful domain objects, as in real time process control systems, and graphical user interfaces.

The presumption that objects should be small, should persist and maintain “mutable state” proved problematic.

 

Early on, some OO purists deprecated business database management systems and query languages.

In 1995, I sat on the speaker’s panel at a conference.

The first question: “Gartner predict ODMS will soon replace RDBMS; when will that happen?”

When I answered “Not before I retire” some in the audience booed!

However, most appreciated that a database is more than infrastructure for persisting the state of domain objects.

Inevitably, the structure of a database is a model of terms, concepts and rules of a business domain. (See the paper on persistent data for discussion of this.)

 

Later SOA thinking: remote server objects will be stateless rather than stateful.

However, many business applications depend on the existence of a data server and a large persistent data store.

So, transient server objects may act as a facade to the internal state of the system of interest.

And often, some persistent data is cached on the application server (or client device) for the purposes of one use case, session or transaction.

Common OO principles - and tradeoffs

SOLID is an acronym for five principles often presented as basic OO principles.

They help to illustrate the theme here, that balancing tradeoffs is more important that uni-directional principles.

 

Single responsibility principle

The idea that a class should have only a single responsibility is vague.

Not only systems but also goals and responsibilities are recursively decomposable.

It could be argued an entire CRM application has one responsibility – helping people to manage relationships with customers.

Or that a separate class is needed to maintain each attribute of each entity.

 

At the fine-grained level of OO classes, Bob Martin defines a responsibility to be “a reason for change”.

He defines the principle thus: there should never be more than one reason for a class to change.

The principle is normally read as leading us to define small classes/objects.

And it has encouraged developers to spread logic spread over way too many subsystems.

 

Business applications have to maintain large data structures.

A business database schema might contain hundreds of data types (and in operation, millions of data instances).

Chopping a large data structure up to suit OO thinking creates complexity in messaging and other difficulties.

Difficulties include maintaining data integrity and querying/analysing a whole data set.

 

This OO application development project in 2008 yielded some astonishing figures.

It contained more than 50,000 classes and 6 million lines of C++ based code.

Did this pay off? The time between crashes ranged from 30 seconds to 30 minutes!

As early as 1995, system architects were complaining objects are too small; we need to work at the level of coarser-grained components.

 

Open/closed principle

The principle states that well-designed code can be extended (is open to change) without modification to existing code (which is closed).

New features can be added by adding new code, rather than by changing old, already working, code.

This principle is usually explained in terms of what OO programmers do when attaching subclasses to a base class.

Statically typed languages (like C++) allow programmers to use inheritance to this end.

One defines virtual operations in a virtual base class, then attaches subclasses that conform to and implement the interface of the base class, without changing it.

 

The idea that a software system should be open for extension, but closed for modification is OK up to a point.

But it presumes the base entity is sound.

And it can lead to endless expansion and complexification of a system that would better be refactored or more radically rationalised.

 

Liskov substitution principle

This is the idea that objects (e.g. rectangles) should be replaceable with instances of their subtypes (e.g. squares) without altering the correctness of operations.

In other words, the properties of super type should be shared by all its subtypes (see previous principle).

 

Interface segregation principle

The idea that designing client-specific interfaces are better than designing one general-purpose interface is OK up to a point.

But having many similar interfaces (with overlapping sets of operations) can hinder agility.

 

Dependency inversion principle

The idea that a client should "depend upon abstractions, not concretions" is OK up to a point.

E.g. Clients should depend on the logical data structure of a data store, rather than its physical database schema.

If the two structures clash (rather than correspond), the practice is to decouple clients from the database schema using a data abstraction layer. (See the paper on persistent data structures.)

Still, as Craig Larman pointed out, decoupling for the sake of it is not time well spent.

At the extreme it produces code that is “interfaced to hell”.

Decoupling and SOA

Again, decomposition and decoupling – to facilitate the management and change of subsystems - are very general system design ideas.

In the 1970s non-OO software design gurus defined principles for decomposition and decoupling (e.g. Constantine and Parnas.)

Perhaps the most important principles in Jackson’s “Principles of Program Design” (1975) were:

·         every data structure a program reads or writes can be described in the form of a hierarchical regular expression

·         corresponding data structures can be merged into one program structure

·         clashing data structures should be processed by different subsystems (or modules) that communicate via simple messaging.

 

The last is the fundamental principle for modularising a complex system, which reappears in (e.g.) the MVC design pattern.

In the 1990s, many OO design patterns were devised to decouple clients/senders from servers/receivers – by introducing a “level of indirection” between them.

But in some ways, OOPL-level thinking during this period held back more coarse-grained architectural design thinking.

 

Around 2000, in reaction to distributing objects and using object request brokers, Microsoft introduced SOA.

The term SOA has since been used so widely and loosely as to become almost meaningless, but its original principles stand.

First the world wide web, and then REST, have made those basic SOA principles easier to implement.

 

The universal tradeoffs still apply, e.g. decoupling subsystems can still lead to dis-integrity, duplications and delays in a wider system.

For more on coupling and related matters, read this SOA paper.

Decomposition and microservices

The optimal granularity of a subsystem (or a persistent data structure) is a case-by-case judgement call.

And designers must trade off complexity in subsystems against complexity in inter-subsystem dependencies and messaging.

 

Agile developers’ dream

Enterprise architects’ nightmare

Smaller, simpler subsystems

Can lead to more complex subsystem dependencies & integration

Loosely-coupled autonomous subsystems

Can lead to duplication between subsystems and/or difficulties integrating them

Data store isolation

Can lead to data disintegrity and difficulties analysing/reporting data

 

For more on component granularity, read this Microservices paper.

Conclusions

The general principles of software system design inform OO thinking, but stand independent of it.

The previous paper introduced some general design tradeoffs that are relevant to agile software development

This paper adds notes on basic “object-oriented” principles, and their limitations.

 

Two proposals emerge from this discussion above.

First: balancing tradeoffs is more important than following uni-directional principles.

Second: we ought to look again at system theory in the broad, rather than from a purely “object-oriented” angle.

For more on general system theory come to our next System Theory for Architects tutorial in London