Quotes

Monday, July 9, 2018

Application Architecture Principles



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