SOLID design principles
Delphi is sometimes assimilated to a RAD product - and this is a marketing
label - but IMHO Delphi is much more than RAD.
With Delphi, you can make very serious and clean programming.
Including SOLID style of coding.
The acronym SOLID is derived from the following OOP principles (quoted from the corresponding Wikipedia article):
- Single responsibility principle: the notion that an object should have only a single responsibility;
- Open/closed principle: the notion that “software entities ... should be open for extension, but closed for modification”;
- Liskov substitution principle: the notion that “objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program” - also named as "design by contract";
- Interface segregation principle: the notion that “many client specific interfaces are better than one general purpose interface.”;
- Dependency inversion principle: the notion that one should “Depend upon Abstractions. Do not depend upon concretions.”. Dependency injection is one method of following this principle.
If you have some programming skills, those principles are general statements you may already found out by yourself. If you start doing serious object-oriented coding, those principles are best-practice guidelines you would gain following.
They certainly help to fight the three main code weaknesses:
- Rigidity – Hard to change something because every change affects too many other parts of the system;
- Fragility – When you make a change, unexpected parts of the system break;
- Immobility – Hard to reuse in another application because it cannot be disentangled from the current application.
Single responsibility principle
When you define a class, it shall be designed to implement only one feature. The so-called feature can be seen as an "axis of change" or a "a reason for change".
- One class shall have only one reason that justifies changing its implementation;
- Classes shall have few dependencies on other classes;
- Classes shall be abstract from the particular layer they are running.
For instance, a
TRectangle object should not have both
Draw methods defined at once - they
would define two responsibilities or axis of change: the first responsibility
is to provide a mathematical model of a rectangle, and the second is to render
it on GUI.
When you define an ORM object, do not put GUI methods within. In fact, the
fact that our
TSQLRecord class definitions are common to both
Client and Server sides makes this principle mandatory. You won't have any GUI
related method on the Server side, and the Client side could use the objects
instances with several GUI implementations (Delphi Client, AJAX Client...).
Therefore, if you want to change the GUI, you won't have to recompile the
TSQLRecord class and the associated database model.
Another example is how our database classes are defined in
- The connection properties feature is handled by
- The actual living connection feature is handled by
- And database requests feature is handled by
TSQLDBStatementinstances using dedicated
Therefore, you may change how a database connection is defined (e.g. add a
property to a
TSQLDBConnectionProperties child), and you won't
have to change the statement implementation itself.
Following this Single responsibility principle may sound simple and easy, but in fact, it is one of the hardest principles to get right. Naturally, we tend to join responsibilities in our class definitions. Our ORM architecture will enforce you, by its Client-Server nature, to follow this principle, but it is always up to the end coder to design properly his/her interfaces.
When you define a class or a unit, at the same time:
- They shall be open for extension;
- But closed for modification.
When designing our ORM, we tried to follow this principle. In fact, you should not have to modify its implementation. You should define your own units and classes, without the need to hack the framework source code.
Even if Open Source paradigm allows you to modify the supplied code, this shall not be done unless you are either fixing a bug or adding a new common feature. This is in fact the purpose of our http://synopse.info web site, and most of the framework enhancements have come from user requests.
The framework Open Source license may encourage user contributions in order to fulfill the Open/closed design principle:
- Your application code extends the Synopse SQLite3/mORMot Framework by defining your own classes or event handlers - this is how it is open for extension;
- The main framework units shall remain inviolate, and common to all users - this illustrates the closed for modification design.
Furthermore, this principle will ensure your code to be ready to follow the main framework updates (which are quite regular). When a new version is available, you would be able to retrieve it for free from our web site, replace your files locally, then build a new enhanced version of your application. Even the source code repository is available - at http://synopse.info/fossil - and allows you to follow the current step of evolvment of the framework.
In short, abstraction is the key. All your code shall not depend on a particular implementation.
In order to implement this principle, several conventions could be envisaged:
- You shall better define some abstract classes, then use specific overridden classes for each and every implementation: this is for instance how Client-Server classes were implemented;
- All object members shall be declared
protected- this is a good idea to use an Service-Oriented-Architecture for defining server-side process, and/or make the
TSQLRecordpublished properties read-only and using some client-side
- No singleton nor global variable - ever;
- RTTI is dangerous - that is, let our framework use RTTI functions for its own cooking, but do not use it in your code.
Some other guidelines may be added, but you got the main idea. Conformance to this open/closed principle is what yields the greatest benefit of OOP, i.e.:
- Code re-usability;
- Code maintainability;
- Code extendibility.
Following this principle will make your code far away from a regular RAD style. But benefits will be huge.
Liskov substitution principle
Even if her name is barely unmemorable, Barbara Liskov is a great computer scientist, we should better learn from.
Her "substitution principle" states that, if
TChild is a
TParent, then objects of type
be replaced with objects of type
TChild (i.e., objects of type
TChild may be substitutes for objects of type
TParent) without altering any of the desirable properties of that
program (correctness, task performed, etc.).
For our framework, it would signify that
TSQLRestClient instances can be substituted to a
TSQLRest object. Most ORM methods expect a
parameter to be supplied.
Your code shall refer to abstractions, not to implementations. By using only methods and properties available at classes parent level, your code won't need to change because of a specific implementation.
The main advantages of this coding pattern are the following:
- Thanks to this principle, you will be for instance able to stub or mock an interface or a class - this principle is therefore mandatory for implementing unitary testing to your project;
- Furthermore, testing would be available not only at isolation level (testing each child class), but also at abstracted level, i.e. from the client point of view - you can have implementation which behave correctly when tested individually, but which failed when tested at higher level if the Liskov principle was broken;
- If this principle is violated, the open/close principle will be - the parent class would need to be modified whenever a new derivative of the base class is defined;
- Code re-usability is enhanced by method re-usability: a method defined at a parent level does not require to be implemented for each child.
Some patterns which shall not appear in your code:
- Statements like
if aObject is TAClass then begin .... end else if aObject is TAnotherClass then ...in a parent method;
- Use an enumerated item and a
case ... ofor nested
if ... thento change a method behavior (this will also probably break the single responsibility principle: each enumeration shall be defined as a class);
- Define a method which will stay
abstractfor some children;
- Need to explicitly add all child classes units to the parent class unit
In order to fulfill this principle, you should:
- Use the "behavior" design pattern, when defining your objects hierarchy -
for instance, if a square may be a rectangle, a
TSquareobject is definitively not a
TRectangleobject, since the behavior of a
TSquareobject is not consistent with the behavior of a
TRectangleobject (square width always equals its height, whereas it is not the case for most rectangles);
- Write your tests using abstract local variables (and this will allow test code reuse for all children classes);
- Follow the concept of Design by Contract, i.e. the Meyer's rule defined as "when redefining a routine [in a derivative], you may only replace its precondition by a weaker one, and its postcondition by a stronger one" - use of preconditions and postconditions also enforce testing model;
- Separate your classes hierarchy: typically, you may consider using separated object types for implementing persistence and object creation (this is the common separation between Factory and Repository).
The SOA and ORM concepts as used by our framework are compatible with the
Liskov substitution principle. Future versions shall enforce this, by providing
some direct Design by Contract methods (involving a more wide usage of
Interface segregation principle
This principle states that once an interface has become too 'fat' it shall be split into smaller and more specific interfaces so that any clients of the interface will only know about the methods that pertain to them. In a nutshell, no client should be forced to depend on methods it does not use.
As a result, it will help a system stay decoupled and thus easier to re-factor, change, and redeploy.
In its current state, our framework does not make direct use of
interfaces. But its Client-Server SOA architecture helps
decoupling all services to individual small methods. And its stateless design
will also reduce the use of 'fat' session-related processes.
Dependency Inversion Principle
Another form of decoupling is to invert the dependency between high and low level of a software design:
- High-level modules should not depend on low-level modules. Both should depend on abstractions;
- Abstractions should not depend upon details. Details should depend upon abstractions.
In conventional application architecture, lower-level components are designed to be consumed by higher-level components which enable increasingly complex systems to be built. This design limits the reuse opportunities of the higher-level components, and certainly breaks the Liskov's substitution principle.
The goal of the dependency inversion principle is to decouple high-level components from low-level components such that reuse with different low-level component implementations becomes possible. A simple implementation pattern could be to use only interfaces owned by, and existing only with the high-level component package.
In other languages (like Java or .Net), various patterns such as Plug-in, Service Locator, or Dependency Injection are then employed to facilitate the run-time provisioning of the chosen low-level component implementation to the high-level component.
Our Client-Server architecture facilitated this decoupling pattern, even if
interface is used by default in the framework yet.
But such a WCF-like or SOAP/Datasnap
definition is on the top of my to-do list.
Comments and feedback are welcome on our forum.