1.
Single Responsibility Principle
Description
A class, procedure, subroutines, or stored procedure should
have one, and only one, reason to change.
This is aligned to the principle of cohesion in structured programming
design. This is also related to
separating the concerns.
Rationale
Structured programs’ classes, procedures and code that have
many different responsibilities generally have higher complexity, and many
different reasons to change. The code is
generally less readable, and harder to maintain. Side effects on functional responsibilities
not intended to change are common when a change is made in what had been
thought to be an isolated section of the code.
Functional decomposition to the smallest common elements with
a single responsibility, and thus one reason to change, and written in a simple
concise manner, makes code more readable, understandable and reusable.
Recombining these components, procedures, subroutines, or
methods up into a collection of elements that belong together or work together
to support a new higher order responsibility, is the natural outcome of
following the single responsibility principle.
Implications
All custom implemented logic must be analyzed and broken down
into logically discrete concerns, or responsibilities.
These smallest elements of functionality must then be
implemented in isolation, either as part of a larger group of responsibilities,
as in methods on a class in object orientation, or as a distinct separate class
with a collection of methods for that class.
Depending on programming language, there are different mechanisms for
building more modular software: subroutines and procedures are examples of
candidate structures for isolating implementation logic.
2.
Open for extensibility, Closed for modification
Description
Software entities (classes, modules, functions, procedures,
etc…) should be open for extension, but closed for modification that affect dependent
modules. The software code, as written,
should not change, but should allow for extensions that can make the software
behave in new ways.
Rationale
A single change in a software code block could result in
cascading changes to dependent modules. If one of the dependencies did not need that
change, the software developer has caused a software system to become fragile,
unpredictable, and functionally un-reusable for the dependency that did not
need to change. These aspects of a software system are generally considered to
be indicative of a bad system design or implementation.
There is a relation to “common coupling” and “control
coupling”. If an external software
entity, (.e. a subroutine, sub-class or an external class) can change an
internal variable which results in a behavior change (i.e. using a “do this”
flag), this also violates the principle of being closed for modification. We frequently call the need to hide variables
which control behavior of a software module as information hiding. We frequently call the variables which
influence behavior as configuration data.
Closed for modification, but open for extensibility, requires
careful analysis of what should be allowed in software behavior changes versus
the probability of the need for the software behavior change. Also, the
behavior change should be isolated and only present for the modules that have a
dependency on a specific behavior.
Those areas with a high probability need for change should be
closed for modification, but proper abstractions must be put in place to
eliminate the need for modification but still allow extensibility.
Implications
The design of the software requires abstractions in those
parts of the program where the designer believes the areas will be subject to
change.
Do not make public variables that control the behavior of the
software module. The software module
should react to the data on which it operates, but the data should be aligned
to the problem domain – not technical control data. The behaviors should be aligned to proper
software functionality for the problem domain’s data.
The use of object oriented languages and dynamic languages
make this straightforward, as does the use of inversion of control and
dependency injection.
In OO design, the use of the template pattern, strategy
pattern, and visitor pattern are commonly used to support this principle.
3.
Liskov’s Substitution Principle
Description
Derived classes, module’s function, procedure or subroutine,
must be substitutable for their base classes or original functional
definitions. The dynamic definition,
i.e. the behaviors, not the static definition, i.e. the structure, is what must
be substitutable.
Rationale
The definitions of a classes operations, module’s function,
procedure or subroutine, is one of behavior, not data or the intrinsic
definition of a thing. Even data, when
looked as a responsibility, is about behavior (is it a responsibility to handle
the request and keep track of data – if so, find the source of that data and
return it), not structure. In
inheritance and polymorphic models, or software models where behaviors can
change due to runtime dynamics of the language, changes in behavior can break
the consumers of the operation.
Rigorously defined preconditions and postconditions of “base”
cases must be defined and not violated by more derived classes, or via dynamic
runtime changes to the software. If a
precondition is that 3 valid values are passed in, and the defined behavior is:
a calculation will occur that results in a new calculated value, and there will
be an internally consistent state keeping the 3 valid values for future use;
the postcondition is that the internal state has the 3 valid values and will
return a calculated value. No violation
of this precondition and postcondition shall be allowed.
The variability point which could result in a change in
behavior must be closed, which is aligned to the open for extensibility and
closed for modification principle.
Implications
Intuitively related models may not satisfy Liskov’s
substitution principle. A simple example
is a shape structure. In mathematical
definitions, squares are rectangles, but the behavior of a square’s dimensions
are different than a rectangles. The
preconditions and postconditions for rectangle’s and square’s dimensions are
not compatible, so a square is not substitutable for a rectangle, even though
intuition based on the non-software modeling world would have indicated they
were.
When using an object or a functional call through its
interface, the user knows only the preconditions and postconditions. The actual
implementation must not expect such users to obey preconditions that are
stronger than those required by the original interface. That is, they must
accept anything that the original function could accept. Also, the actual
implementation must conform to all the postconditions of the original
functional definition. In other words, their behaviors and outputs must not
violate any of the constraints established for the original functionality.
4.
Interface Segregation Principle
Description
Make fine grained interfaces that are integration
specific. Integration clients should not
be forced to depend on interfaces they do not use.
Rationale
Aligning to the Single Responsibility Principle (paraphrased
as you should have a single responsibility in a module), the need for
isolation, and separation of concerns, interfaces should also be narrowly
focused and cohesive. Without narrowly
focused and cohesive interfaces, any module implementing an interface would violate
the Single Responsibility Principle.
Additionally, with very large interfaces, any change to the
interface definition affects all of the interface consumers who do not need the
advertised functionality, and thus all clients are coupled even if their functional
domains are different.
Implications
Define fine grained interfaces, such that a single operation
on the interface is usable in a very tightly defined scenario.
In languages where grouping of interfaces definitions are
possible, group the interfaces along similar functional domains. Do not make an interface grouping contain
unrelated functionality from different domains, such as “orders” and “firm”.
5.
Dependency Inversion Principle
Description
Depend on abstractions, not concretions.
Rationale
Having a dependency on a specific implementation of
functionality transmits any change in that implementation to the consumer, even
if that change was not intended to alter the functionality. This is true in statically linked languages,
as well as dynamically linked or dynamic dispatch languages.
In statically linked programs, the dependent module must be
rebuilt. Dynamic linked languages are
generally not impacted by functions that are added and not used, but the actual
implementation change impacts the program.
As the dynamically linked object is referenced directly, any change to
the logic or structure can result in breaking the dependent module. In some languages, due to how references are
used, any change may be communicated to the dependent module (for example, Dlls
and C++ .h files).
In any dynamically linked language, if you wanted to swap
objects that implemented the functionality, the reference (i.e. pointer) to the
implementation object or method would have to change. If you used an abstraction, then the
reference is not changed, and the implementation is only required to adhere to
its contract - the location of the functionality is no longer a concern of the
consumer.
Implications
Use interfaces for the consumer classes, and for the
implementation classes. They both depend
on an abstraction, now.
Use Dependency Injection to supply implementations at runtime
independent of the consuming class’s references (the reference should be to an
interface).
6.
Architect and Design for Testability
Description
All architectures and designs should be created such that one
quality factor that is achieved is high testability.
Rationale
At the time of integration testing, major architecture issues
may be found, and significant re-work would be required. Architecture is the most significant area in
which mistakes can be made and take the longest to remediate. Due to a customary process of project’s
sequencing, testing of the full architecture usually takes place near the end
of the project construction, which creates significant project risk.
Brittle architectures, where components are not
interchangeable, make isolated testing difficult if not impossible.
Static classes, as dependencies, are not very testable,
because a stub or a mock may not be substituted.
During maintenance phases, making changes to systems that are
not testable introduces unacceptable risk, and the changes that will get
approved are usually urgent and important, thereby increasing the baseline
risk.
Implications
Define architecture components such that the dependency is on
abstractions (interfaces), not concretions (implementations).
Systems must support being setup in a known state in a
repeatable manner.
Do not use static classes.
Avoid embedding business logic in database objects.
Use Dependency Injection and Inversion of Control if
available.
7.
Avoid Technology that Hinders
Organizational Agility
Description
Avoid building applications coupled to any one framework, or
platform. Instead, identify the capabilities needed, and select a framework,
tool or platform that supports it, and have a secondary one in case a switch is
required at a future point. Design and
build in proper abstractions to make the switch easier.
If the technology to be selected does not work well, or at
all, when wrapped in additional abstractions, the technology and the vendor is
achieving lock-in.
Resist technology lock-in or vendor lock-in, and only accept
lock-in as an explicit tradeoff for achieving the time-to-market goal, or other
valuable identified goal.
Rationale
If the selected framework, tool or platform is found to be
limiting in meaningful, measurable ways after the initial usage period, and the
architecture needs to evolve, experiencing lock-in with a vendor or a
technology greatly reduces organizational agility.
If the benefits for lock-in no longer outweigh the negative
impacts, then the option to move to a competing framework, tool or platform
should be immediately available.
Implications
Align to Enterprise Architecture principles on technology
diversity.
Document areas where technology lock-in or vendor lock-in
exists or will exist, and the value of accepting that lock-in.
8.
Don’t Repeat Yourself
(DRY)
Description
Every piece of knowledge must have a single, unambiguous,
authoritative representation within a system.
Rationale
Duplication (inadvertent or purposeful duplication) can lead
to duplicate maintenance, poor factoring, and logical contradictions as the
duplicated code in one location diverges, accidentally, from the code in
another location.
Duplication, and the strong possibility of eventual
contradiction, can arise anywhere: in architecture, requirements, code, or
documentation. The effects can range from mis-implemented code and developer
confusion to complete system failure.
“Knowledge” in this case is partially about representing the
intent. If the intent is to write an
algorithm for a decision rule (with specific requirements, of course), and
there is a procedure to describe that with an interface, the use of the
interface to call the procedure is not considered duplication. The use of the interface, though it may be
textual duplication of some elements of the procedure, has an intent that is
completely different than writing the source algorithm.
This is similar in spirit to a “single source of truth”. In that it's okay to have mechanical, textual
duplication (the equivalent of caching values: a repeatable, automatic
derivation of one source file from some meta-level description, gold and silver
copies of data), as long as the authoritative source is well known.
Implications
Algorithms (and the responsibility they represent) must be
implemented once and only once. This
doesn’t mean that everyone must exist once and only once.
It is permissible to have more than one representation of a
piece of knowledge provided an effective mechanism for ensuring consistency
between them is engaged (replication, compiler checks, metadata
synchronization, etc…).
Users of the interface (the process) may duplicate the
signature if and only if the implementation environment requires it.
Refactor common functionality to single code classes and
frameworks upon discovery of duplication.
No comments:
Post a Comment