1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
|
Consider the following situation: a software engineer is asked to design a
storage class tt(Storage). Data stored in tt(Storage) objects may either make
and store copies of the data or store the data as received. tt(Storage)
objects may also either use a vector or a linked list as its underlying
storage medium. How should the engineer tackle this request? Should four
different tt(Storage) classes be designed?
The engineer's first reaction could be to develop an all-in tt(Storage) class.
It could have two data members, a list and a vector, and its constructor could
be provided with maybe an enum value indicating whether the data itself or new
copies should be stored. The enum value can be used to initialize a series of
pointers to member functions performing the requested tasks (e.g., using a
vector to store the data or a list to store copies).
Complex, but doable. Then the engineer is asked to modify the class: in the
case of new copies a custom-made allocation scheme should be used rather than
the standard tt(new) operator. He's also asked to allow the use of yet
another type of container, in addition to the vector and the list that were
already part of the design. Maybe a tt(deque) would be preferred or maybe even
a tt(stack).
It's clear that the approach aiming at implementing all functionality and all
possible combinations in one class doesn't scale. The class tt(Storage) soon
becomes a monolithic giant which is hard to understand, maintain, test, and
deploy.
One of the reasons why the big, all-encompassing class is hard to deploy and
understand is that a well-designed class should
hi(class: enforcing constraints)
em(enforce constraints): the design of the class should, by itself,
disallow certain operations, violations of which should be detected by the
compiler, rather than by a program that might terminate in a fatal error.
Think about the above request. If the class offers both an interface to access
the vector data storage em(and) an interface to access the list data storage,
then it's likely that the class offers an overloaded ti(operator[]) member
to access elements in the vector. This member, however, will be syntactically
present, but semantically invalid when the em(list) data storage is selected,
which doesn't support tt(operator[]).
Sooner or later, em(users) of the monolithic all-encompassing class
tt(Storage) will fall into the trap of using tt(operator[]) even though
they've selected the list as the underlying data storage. The compiler won't
be able to detect the error, which only appears once the program is
running, confusing its users.
The question remains: how should the engineer proceed, when confronted with
the above questions? It's time to introduce hi(class: policy) em(policies).
|