What is (still) hard about software architecture

Dec 20, 2008 | George Fairbanks

A while back I posted a list of things that are hard about software
architecture
, and this
posting is an elaboration on that list that will make its way into the
software architecture book.

Comments on this list are welcome. In fact, if you have a pet peeve
about software architecture then it can probably be refactored into an
entry in this list.

Process: What to do when

Cost-benefit analysis. We have seen that by identifying risks we can
selectively perform architecture modeling to reduce risks in order of
their priority. While this is better than simply guessing how much
architecture work is enough, it leaves much to be desired. It is
difficult to predict and prioritize risks, so choosing the right
amount amount of architecture is also difficult. Engineers will have
different opinions of risks and priorities, so you may find yourself
trusting one engineer’s estimates over another’s.

The community of agile software developers has an ongoing discussion
of how much advance planning needs to be done, with some arguing that
most projects are better off with no architecture planning. The best
developers have excellent design instincts and simply following their
hunches can lead to success.

Behavior. Architecture models that only describe quality attributes
tend to reach a natural level of detail in modeling, but models that
include functionality and behavior can easily be elaborated until
they describe data structures and operations. Architecture modeling
can transition into design, then detailed design, then a paper-based
version of coding. This ability to go deep is a benefit because we
can dig into details when needed, but a challenge because we must
decide when to dig in and when to resist. Time spent modeling has an
opportunity cost: time spent building the system.

Evaluating alternative architectures. Seen from a distance,
evaluating alternative designs is as simple as building a few models
and evaluating how well each enables the desired qualities. In
practice, evaluating alternatives is difficult because the devil
often lives in the details, and those details may not have been
elaborated yet. Since we have not yet committed to a design we are
hesitant to spend much time adding details, but we may not discover
problems until we take a look at details like the specifics of
external API’s we must use, or prototype to learn actual performance
figures.

Expressability / analyzability

Non-static component configurations. Most systems settle down into a
stable set of runtime component instances, even though during
initialization there are changes. When we draw diagrams showing the
runtime configuration of component instances, we usually simplify the
problem by not drawing diagrams of the intermediate configurations
during startup and shutdown. A diagram that shows the internal
configuration of components in our systems looks reasonable at first
glance, but it is static, not dynamic, and has omitted the other
configurations during startup and shutdown. We make the
simplification because reasoning about dynamic configurations is hard
and we have few tools to make it easier.

It is reasonable to only consider the steady-state configuration when
startup and shutdown are straightforward, but we can easily imagine
counter-examples. We even know that some systems change at runtime,
though we generally avoid this because of the possibility of
failure. Peer-to-peer systems evolve at runtime into different
configurations of components, as do frameworks that can dynamically
load new components. It is difficult to convince yourself that untime
re-configuration like this is free from problems, so as developers we
tend to avoid it, but some problem domains demand it.

Shoehorning abstractions. The transition to structured programming
saw some developers complaining that they could not express their
existing programs in the new, more constrained, programming
languages. They argued that their old programs were more efficient
and perfectly understandable, so the new constraints were
undesirable.

A similar transition happens when moving from a sea of unconstrained
object-oriented designs into a world constrained by a set of
architectural abstractions like components and connectors. At first
you may look to your existing designs and find that parts of them do
not match up well with the new abstractions. Perhaps you could have
designed them to fit, but looking at them now you see that they do not
fit, and you are tempted to reject the new abstractions. After a while
you will become accustomed to these abstractions, and you will feel
comfortable building within the limited set, but the transition can be
uncomfortable.

Frameworks. Frameworks present a particular example of shoehorning
abstractions because the interaction beween client code and a
framework does not neatly align with the standard architectural
abstractions. Frameworks provide complex, wide interfaces to the
clients that use them, often exposing the implementation details of
the framework. Ports, in contrast, provide narrow interfaces and
encourage encapsulation. Some frameworks can be represented as
components because they exist at runtime, while other frameworks,
especially older ones, are collections of code that cannot be
instantiated until augmented with client code.

Representing concurrency. Concurrency has always been one of the
most challenging problems in developing systems. Novice developers
may relish the challenge and seek out opportunities for concurrency,
but jaded developers view it warily as a source of difficult bugs and
are happy to get it right then leave it alone. Concurrency is
introduced into systems either through forces in the problem domain
or by a desire to improve a quality attribute, such as performance or
usability.

With a clean-slate design you might be able to perfectly align the
threads or processes in your system with the component boundaries. If
so, you can annotate the components and connectors, as we did in our
example in Chapter [cha:Example:-Media-Player], to indicate the
concurrency. Anytime a concern cross-cuts your decomposition (see
Section [sec:Separating-concerns]) there will be trouble expressing
it, and concurrency is particularly difficult.

View consistency. The ubiquitous advice on software architecture is
to build multiple views of your system. We do this because each view
can focus attention on one aspect, some views cannot be easily
reconciled (recall the definition of a viewtype), and creating a
single view would create a muddle of details that defeats the purpose
of having a model. Reasoning from a particular view means having to
separately reconcile the views for consistency. You might find one
arrangement of windows on the outside of the house aesthetically
pleasing, but a different arrangement of windows leads to good
lighting in a room, yet these views must be consistent.

The challenge is to detect consistency problems, preferably earlier
than later. Some view consistency problems are simply cruft because
you update one view but have not yet updated older views. Other
consistency problems stem from design errors and may lead to
un-buildable designs. We discuss ways to check for consistency in
Section [sec:SA-view-checks].

Precision. It is difficult to to know when your model is precise
enough, or detailed enough. The general advice is to make the model
precise enough to answer the questions will you ask of it or
sufficient to reduce the risks you perceive. However, you may not be
able to perceive the risk until after you have built the detailed
model. Analytic models, generally, require more precision and are
more expensive than analogic models.

Design

Promoting details. Choosing which details to promote to the
architectural level is difficult. The challenge is how to select
relevant details for the model at the same time keeping the model
minimally sufficient. Different developers are likely to choose
different details, which means that some models will be better than
others, yet we do not have guidance on how best to choose.

You may build a model of a component one day, then later decide to use
it in a concurrent setting, but your model does not show if the code
is thread-safe. Generally, a model built for one purpose will work for
another purpose only if you are lucky. You were not wrong to omit that
detail from your model if it was able to answer the questions
originally asked of it.

Modeling connectors and ports. Connectors provide a developer with
great flexibility in expressing how components communicate. Imagine
two components, A and B, that communicate via a third component C, or
even via a shared resource like a file. You could model this with
connections to component C or file, or hide the existence of
component C or file within a connector. An enterprise service bus
connector is likely an expensive purchase, so there is a temptation
to expose it as a component rather than a connector. Both options are
accurate and allowed.

The most common kind of connector is a simple two-way connector, but
N-way connectors are possible and make sense, for example an N-way
connector that reports a consensus. It would not be wrong to model
this N-way connector as several 2-way connectors that all connect to a
single component that calculates the consensus, and perhaps that more
accurately reflects the implementation. Neither is more right than the
other.

Another choice you will face is whether you should route two different
connectors to the same port, or to two different ports. Again, neither
is more right than the other, though the details of the situation may
bias you one way or the other, particularly if the port has a shared
state for the two connectors or not.

Refinement. Models will become unsychronized with other models and
with code, and problem is particularly hard with a refinement
relationship between models. It is easy to forget to revise the
high-level model of system when you revise the low-level
model. Forgetfulness aside, you may deliberately allow your various
models to become out-of-date because it is expensive to keep them
updated.

It is possible to be sufficiently precise in the refinement map so
that you can detect refinement problems, but prohibitively
expensive. In practice few developers even write down a sketch of the
correspondences between a high- and low-level model, though they may
eyeball each to convince themselves that the refinement is ok.

Modeling for prediction. Using architecture models to discover
problems in advance is harder, and requires more effort, than
modeling simply to document a design because small details can
distort predictions. A friend related an experience where his
performance predictions were substantially incorrect because the
actual distribution of requests into his system were burstier than
his model allowed. Improved architecture modeling technology holds
the promise of better predictions about performance, but producing a
sufficiently detailed model for accurate predictions can be
expensive.

Non-encapsulated issues and patterns. Components, modules, and nodes
allow us to encapsulate our thinking in different viewtypes, but some
ideas will crosscut these elements. Low-level ideas like coding
standards will apply to many modules, as will large-scale patterns
like using MapReduce whenever possible to ease handling of large
datasets. Many times we can use the open-ended ability to annotate
elements with custom properties, but that solution works poorly here
because there is no obvious element to annotate with the property.

Issues spanning engineering and management. It is unlikely that your
organization’s management will pay much attention to lower-level
design decisions like the indentation style in your code, but they
are likely to be interested in the functionality and qualities of
your system. Sometimes, when deciding the architecture for your
system, you will face a choice that can either be solved by
engineering or by management. For example, a distributed system might
be cheaper to build if you can assume that each site will support the
software that runs there, or you could design it for central
administration at a greater cost. The decision regarding system
administrators is likely to be made by management, not engineers, and
other similar situation occur at the architectural level of design.

Bridging design to implementation

Non-object languages. As we have discussed, every system will have
at least one component instance at runtime, which is the entire
system itself. When programming in object-oriented languages, it is
comfortable to think about this component having internal runtime
structures that are objects, and not a big stretch to think about
grouping those objects into subcomponents. In non-object-oriented
languages such as functional, rule-based, or even procedural
languages, it is harder to envision what the runtime substructure
is. It is possible to allocate responsibility to subcomponents and
then build the subcomponent with whatever style of language is most
appropriate, including non-object languages.

Bridging objects to components. Even when using an object-oriented
language there are problems moving between the architecture
abstractions and the object abstractions because each has a different
vocabulary and communication idioms. Objects (or functions, or
procedures, …) are concretely represented in programming languages,
and substantial design guidance exists for them. Architecture
abstractions are not yet concretely available in mainstream
programming languages, which raises the question of when to switch
from one abstraction to another.

A standard object-oriented pattern is to use an adapter to convert
from one interface to another. However, in the example in Section
[sec:Fear:-COTS] we represented an adapter as a component, not an
object. We had the choice of building this adapter into the existing
components and revealing its existence as a new port, or making the
adapter into its own component. It is hard to give general advice on
how big or small components should be, but it is uncommon for a single
object to be a component.

Multiple languages. Within a single language, you can develop a
coding style that makes the components and connectors evident (see
Section [sec:Architecturally-evident-coding]). In practice, scripting
languages are often used expediently and without the same attention
to coding discipline as the rest of the code. Keeping up the
discipline of an architecturally evident coding style can be
difficult with multiple languages, especially when they are
substantially different and the conventions you are following in one
do not translate well into the other.

Non-greenfield implementations. If you start with arbitrary machine
code it is difficult to imagine fitting structured programming
language constructs to it because the machine code was not written
with those constraints in mind. Similarly, it will be difficult to
align the architectural concepts with an existing system that was not
built with those constraints in mind. In this case, an expensive
strategy is to refactor the existing code into modules and
components, but more practical is simply to think of the existing
system as a collection of really large modules or components. As you
move forward you can build reasonably sized components or
subcomponents. If the existing code has truly poor encapsulation,
however, there are few inexpensive options to evolve it into a better
state.

About

George Fairbanks is a software developer, designer, and architect living in New York city

Contact

gf-web@georgefairbanks.com
124 W 60th St #37L
New York, NY 10023
+1-303-834-7760 (Recruiters: Please do not call)
Twitter: @GHFairbanks