General software system
theory and design trade offs
Copyright 2019 Graham Berrisford. One of several hundred papers at 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:
· software system theory is older and broader than “object-oriented thinking”
· design tradeoffs related to decomposition and decoupling are eternal and universal.
The general principles of software system design inform OO
thinking, but stand independent of it.
This paper introduces some general design tradeoffs that are
relevant to agile architecture (whatever you mean by that).
Contents
General
system theory – basic terms and concepts
System
design tradeoffs (REPEATED)
Where
to specify and code business rules?
In classical cybernetics, systems exhibit
regular or repeatable behaviors.
The behaviors are performed by some actor(s) and modify some structure(s) or state variables.
That is the kind of system most system theorists are interested in.
The more particular
system of interest here is a software system.
It is a discrete
event-driven system.
It responds to input
events (messages) by performing processes.
It is deterministic;
it determines what to do by comparing input data against persistent state data
(memory).
The memory takes the
form of a data structure.
The data structure relates data types (entities and their attributes) of interest that system users want to monitor or direct.
A system can be described from the different viewpoints shown in this table.
|
Behavioral
perspective |
Structural
perspective |
External perspective |
Service contracts |
System interface |
Internal perspective |
Processes |
System / subsystems |
The whole
quadrant defines aspects of one system; but all is recursive.
Each
subsystem (bottom right) is itself a system and recursively describable using
the whole quadrant.
Synonyms
used by software designers include
·
Service contract = method or operation
·
Process = method body or procedure
·
System interface = API, WSDL-defined interface
·
Subsystem =
module, component or class
(Unfortunately,
people also use the term service for an interface or a component.)
Structural
perspective
A system is a structure that can be triggered by events to perform one or more processes.
External perspective - interface
A system can be defined from the outside by its interface, which lists the services it can perform.
The interface encapsulates whatever internal processes and resources the system needs.
The encapsulation boundary is chosen by the system describer or designer, it is logical rather than physical.
(Which means that systems can overlap.)
Internal perspective- subsystems
A system may be composed of interacting subsystems
A system may be described or designed as composed of a few large subsystems or many small subsystems.
A subsystem may be further decomposed, meaning that coarser-grained subsystems contain finer-grained subsystems.
At the bottom level are atomic subsystems - not further decomposed.
Behavioral
perspective
A process that runs over time from start to end.
One system-level process may span several subsystems, which each performs a part of the higher/wider process.
A system can act as either or both:
· a client, which requests another (server) system to perform a process
·
a server, which performs a process at the
request of another (client) system.
Here, client and server are logical terms (nothing to with hardware)
External perspective- service contracts
An invocation is an event or message that triggers a process.
The full contract for a process may be presented in the form of service
contract:
· Signature (process name, input parameters and output/response message).
· Business rules (preconditions and post conditions that apply to system state variables).
To invoke a process, a client needs only its signature.
However (see the section on business rules below) a client may also test preconditions and/or test post conditions.
Internal perspective- processes
A process is a sequence of activities.
Both structured and OO methods divide them into two kinds.
An enquiry process returns/outputs a message to inform or direct the
behavior of external entities (it has no effect on the system’s state).
An update process has an effect on the system’s state; it changes the
value of one or more internal state variables (or else returns/outputs a
failure message).
Gurus in agile development and architecture frameworks promote general principles.
It is important to recognise where principles can be in conflict.
A theme here is that
balancing tradeoffs is more important than following uni-directional
principles.
For
many decades, general system design principles have included:
·
Decomposition:
modularise a large monolithic system into subsystems.
·
Decoupling: strive for tight
cohesion within a subsystem and loose coupling between subsystems (Constantine,
1968).
The
aim of these principles is to facilitate the design, management and change of
subsystems.
Taken too far however, both principles have negative effects, and designers
must strike a balance between extremes.
Unfortunately, Constantine didn't explain how we must flex his principle according to the granularity of the subsystems.
There are many ways to decouple subsystems; and there are degrees of coupling.
Fine-grained subsystems are usually better more tightly
coupled than coarse-grained subsystems.
Simple subsystems or simple messaging?
This table contrasts the qualities of dividing a system into larger or smaller subsystems
Subsystem |
Inter-subsystem |
|||
Size |
Processes |
Messaging |
Coupling |
|
Macro
subsystems |
Larger |
Longer |
Less |
Looser |
Micro
subsystems |
Smaller |
Shorter |
More |
Tighter |
Decomposing a system into small, simple subsystems creates complexity in the structure of the system.
It increases the volume and complexity of inter-subsystem messaging.
How much complexity in messaging or event flows is a self-inflicted wound? Caused by
· Distributing subsystems that would better be co-located?
· Dividing an application into microservices where it should not be?
· Dividing an application into microservices that are too small?
· Dividing what should be an ACID transaction into several transactions that then need to be coordinated one way or another?
· Programmers directed by the CIO to use the middleware he has spent too much money on, even where it is not needed?
· Programmers eager to teach themselves Domain-Driven Design where they would better use Transaction Scripts or other pattern?
There are many different ways to implement messaging, ranging from tightly to loosely coupled.
Size matters in the sense that smaller, subsystems typically merit tighter coupling than larger subsystems.
Local agility or
global complexities, delays,
disintegrities?
Note first that decoupling is not a single concept.
Our architect classes cover a dozen or more ways to decouple subsystems.
In “Applying UML and patterns”, OO thinker Craig Larman said to pick your battles.
“It is not high coupling per se that is the problem; it is high coupling to elements that are unstable in some dimension, such as their
· interface [the list of processes/services that are provided or required]
· implementation [internal procedures, physical features or technologies]
· mere presence [availability when needed]”
“If
we put effort into “future proofing” or lowering
the coupling when we have no realistic motivation, this is not time well spent.”
Skillful decoupling of subsystems should help people manage and change each subsystem on its own.
But recognise that decoupling small subsystems can make it harder to meet higher/wider goals.
And if two subsystems are almost always used together, then co-locate them!
A principle of
“microservices” is to “decentralise data management” by dividing one coherent
database structure into smaller ones.
The aim is to divide
the work of application development into smaller applications, to be developed
by relatively autonomous development teams
In the right
circumstances this is a good approach, but it can lead to downsides - complexities, delays,
disintegrities.
Agile
system or simple system?
To make a system more agile usually requires a redesign that
makes the system more complex.
Setting out to build an agile system adds time and cost to
development.
Agile system principle |
|
Agile development principle |
Decouple
subsystems |
Can
be contrary to |
Keep
the system simple |
Design
for future flexibility |
Can
be contrary to |
You
ain’t gonna need it |
Agile system or fast system?
An agile system is usually slower than a rigid one.
E.g. shifting business rules from procedural instructions
into data variables (that end users can change) tends to make a system slower.
Agile system or data integrity?
Continual improvement at a microscale risks disintegrity at a macro scale.
E.g. we change our digital course materials during a course.
Inevitably, our printed course material, digital course material and web site get out of step.
The best we can promise customers is “eventual consistency” – probably.
In other businesses (e.g. money handling, safety-critical) data integrity can be mission critical.
So, decoupling
subsystems and allowing inconsistency between them can be dangerous.
A software system
(being deterministic) applies business rules that compare input events/messages
to internal state/memory.
Business rules can
be expressed in the form of pre and post conditions.
Preconditions define the state a system should be in before an event can be processed successfully
Post conditions define the state a system should be in after the event (some call them “side effects”).
Specifying business rules in service
contracts
Computer scientist
Charles Anthony Hoare was interested in the formal specification of a system.
His logic can be
expressed in the form of what is called the Hoare triple.
· If the preconditions for a process are met.
· And the process completes successfully
· Then the system’s state will meet the post conditions of the process.
For example:
· If a customer’s debt meets the precondition that Debt + Sale Value < Limit.
· And the sale process completes successfully
· Then the customer’s debt will meet the post condition that Debt (after) = Debt (before) + Sale Value.
OO thinker Bertrand
Meyer wanted to define a system’s
interface in a formal, precise and verifiable way.
He proposed it
should specify business rules
related to system state, in way that can be tested.
So he extended
interface definitions to capture system invariants and more.
A process may be
presented in the form of a service contract thus:
· Signature (process name, input parameters and output/response message).
· Business rules (preconditions and post conditions that apply to system state variables).
Notice
that one way or another, a client often gets to know something of server
system’s internal state.
Internal
state variables (or synonyms for them) may appear in the signature and in
business rules.
Implementing (coding) business rules in
process procedures
At run time, who tests
the preconditions of a process? Client or server?
Bertrand Meyer first promoted Design by Contract in Object
Oriented Software Construction, Prentice Hall, 1988.
But again there is a
tradeoff to be made between design patterns.
Design by contract
Premise: a client must guarantee the preconditions of a
process before invoking that process.
The server is then expected to create the post condition
that the Service contract promises.
This might help to keep the system
simple.
But it is impractical and/or risky in multi-user
client-server database systems.
Servers cannot rely on as-yet-unknown clients
guaranteeing servers’ preconditions.
And in distributed systems, clients may not be able
guarantee the preconditions of a process before invoking that process.
So in the design of distributed systems, people apply
defensive design instead.
Defensive design
Premise: a client is not expected to ensure preconditions hold true.
This means designing such that:
· a server handles any failure of any client to send valid data or ensure preconditions
· a client can either ignore or cope with any failure of any server to produce the desired effects/results.
The server must test the preconditions of a process are met, and if not, it likely return a failure message to explain that.
This might add some complexity to the system.
But it is the normal choice when client and server systems are designed by different teams (see the Bezos mandate in this SOA paper).
Test-driven design
There is an agile
development method called “test-driven design”.
This requires
developers to code test cases that test post conditions - using assertions.
The general principles of software system design inform OO
thinking, but stand independent of it.
This paper introduces some general design tradeoffs that are relevant to agile architecture (whatever you mean by that).