This is the first article in a series focused on finding and examining ideas, patterns, idioms that will increase the quality of software.
Naturally, I will dive into concepts like modularity, extensibility, design but also architectural patterns will be scrutinized.
As a first task, I chose to look into type classes and chose Scala as an instrument to facilitate my endeavor.
According to the official website, Scala is a “general purpose programming language designed to express common programming patterns in a concise, elegant, and type-safe way”.
For getting the most out of this article, basic familiarity with Scala’s syntax is required. Also object-oriented concepts like inheritance, subtyping or polymorphism should not be foreign.
One of the major breakthroughs in the history of computer programming has been the introduction of languages that offer the programmer new tools to conceptualize, to move away from the nitty-gritty details of the machine and build abstractions. These abstractions help us reason in a better way about our designs, and will also lead to more modular and decoupled programs with the benefits of easier extensibility and maintenance in the long term.
Therefore, when faced with the task of designing a new system, the programmer
will attempt to ‘translate’ the concepts of the problem domain into abstractions
in the solution domain. For example, when he has to design a document management
system, the programmer will recognize the concept of a
Document as a key
abstraction in his problem/solution domain. In object-oriented jargon, he will
Document interface. When using Scala, the
Document becomes a type
(in scala an interface is called a
trait). For an introduction to types in
Scala, read this article.
He will then discover that his abstractions carry several properties, related to
the problem or application domain. In the case of the
Document, it could be
persisted, or you can add paragraphs to it, or it can be printed, and so on. The
object-oriented world will recognize these properties as methods of an interface.
Fundamentally, the programmer will look for a way to create a contract and to
enforce it across several entities that embody the essence of that contract. For
example, the programmer will discover a contract called
Eq for things that can
be equated. The contract could define the properties
== (equal) and
equal) applicable for that contract. If then we have another abstraction (say,
Document) and it makes sense to apply the equality functions to a document,
the programmer can enforce the contract of
To make things clearer, I’ll give an example.
Let’s consider the concept of aggregating. This is an abstraction since you can
‘aggregate’ many ‘things’, like, datastore columns/rows, in-memory collection
Documents, and so on.
I have defined a trait,
Aggregate, which models this abstract concept.
In this hypothetical example, only a couple of operations are supported:
inserting into an aggregate and querying the size of the aggregate (the number
of parts that comprise the aggregate).
Since this aggregate behavior can be shared by many abstractions, the
type is parameterized, or, more formally, the operations it supports can be
applied identically to different types, a characteristic which is sometimes
defined as parametric polymoprphism. You can
find here an introduction
to parameterized types.
The usual way to model the fact that an abstraction ‘supports’ a contract is through inheritance.
The implementations have been purposely omitted.
There are a few interesting comments to be made by looking at this example.
A first observation is the fact that
represent concepts in the domain they are modeling. They have properties relevant to the
problem to be solved but it can be argued that representing a
Document as an
Documents is an orthogonal matter, that can ‘cut across’ many
different abstractions and that does not necessarily concern the
Furthermore, using this ‘traditional’ approach based on inheritance, for a class
or a trait to be ‘member’ of an interface, it must declare that at the site of
its own definition (using the
with keywords in our case). In
other words, the
Document type ‘is aware’ that it is also an aggregate.
Document implementations will be strongly coupled to the
This raises another interesting question. What if, at some point in time,
someone decides that
Documents need to implement another contract? Not only
that every consumer of the
Document API is forced to deal with characteristics
that might not be interesting to him (the
Document interface becomes ‘fatter’),
but also the source code of the
Document needs to be changed. Also, all types
Document contract (all implementations) need to be adapted to
the new change (they need to implement the new contract and their source code
needs to be changed too).
While it is possible, via polymorphism, or because of the elasticity of the
type system, to have different clients talk only to the interface they are
interested in (in our case, a method expecting a parameter of type
Aggregate[Document] can be passed a
Document instance because a
can also be seen as an
Aggregate[Document]), this solution will only
partially mitigate some of these problems.
Adapters to the rescue
I’ll assume that the creator of the
Document abstraction is aware of the
aforementioned disadvantages. As an alternative, he designs an adapter that is
responsible for encapsulating the
Aggregate behavior of the target type.
However, this approach comes with disadvantages, too. Leaving aside the ones that
are commonly associated with the ‘Adapter’ pattern, the most important is that,
ideally, the client code would want to code against the
interface, but also retain the possibility to view the
Document as an
Aggregate without the forced shift that the adapter option represents.
So at this point, it seems like the designers have reached a dead end.
In the second part of this article, I will show that it’s possible to have ‘the
best of the two worlds’, meaning, to code against the
Document interface while
still being able to use the
Aggregate[Document] view and at the same time,
Document interface decoupled and free of other orthogonal concerns.