Main object-oriented programming principles with real architecture example

How to implement OOP and SOLID principles in the real software development world? I share here our experience when we built the architecture for Tracker Query Langage -TQL-. My friend Nicolas previously explained how we ended-up creating our own parser in PHP. I will explain how we built the TQL architecture, as closely as possible with the SOLID principles for object-oriented programming.

SOLID principles

As a reminder and in a few words, SOLID is a term describing a collection of design principles for good coding, invented by Robert C. Martin, also known as Uncle Bob. :-)
It means :

  • Single Responsibility Principle
  • Open/Closed Principle
  • Liskov Substitution Principle
  • Interface Segregation Principle
  • Dependency Inversion Principle

Structure

We used PEG.js to generate a parser to transform a user query into an abstract syntax tree.


The abstract syntax tree generated contains several types of nodes:

  • Comparisons, which contain fields and values;
  • Operators and expressions, which contain the remainder of the expression;

So, for the following query, status IN(‘open’, ‘on going’) AND summary = ‘REST’ we have a tree with an AndExpression which has two comparisons :

  • InComparison with a list field;
  • EqualComparison with a string field.

Comparisons can support many different types of fields. To manage this complexity we established design patterns, which we will see later.

Before beginning the query transformation we separated it into two steps while using the same tree. This enabled us to:

  1. Validate the query. We checked things like whether or not the field is supported, field compatibility with the requested value, etc;
  2. Build an SQL query to be able to retrieve matching artifacts.


We will describe these two steps in greater detail below. Let’s begin by building the query.

Query builder

The richness of Tuleap trackers has led us to use design patterns to easily reuse or extend previously-created classes. Here, we used two design patterns in particular:

  • Strategy
  • Visitor

Strategy Pattern

The Strategy Pattern isolates and encapsulates the software’s behavior in interfaces. Because behaviors are encapsulated in separate interfaces, how a behavior is implemented can be changed according to the class implementing it, respecting the interface’s contract. Here, we created one “business” class per node syntax tree (e.g. with comparisons like =, between, not in, etc.). Each class implements several interfaces and their use can be done by taking the advantage of the dynamic resolution.

switch ($operator) {
  case ‘=’:
    // build query for equal comparison
  case ‘!=’:
   // build query for not equal comparison
  …
}

versus

class EqualComparison {
   // build query for equal comparison
}
class NotEqualComparison {
  // build query for not equal comparison
}

Therefore, TQL’s comparison capabilities can be extended easily. If you need to introduce a new operator like `BETWEEN(a, b)` all you need to do is use a new class:

class BetweenComparison {
  // build query for between comparison
}

There is no need to duplicate updates to code. The Strategy Pattern follows the open/closed principle (OCP, the O in SOLID): our code is open for extensions but closed for modifications.

There are also several types of fields. These fields are stored in different tables with different join types. To properly query the database, we need a FROM and a WHERE SQL associated with the correct tables by comparison and field type. Changing tables and operators makes it necessary to have specific builders. For a BETWEEN operator, the SQL query is just a BETWEEN, but for a NOT IN operator we have to do an “anti-join”.

Therefore, to get the builder associated with the type of the field in a dynamic manner, we used the Strategy Pattern and created one builder for each behavior defined by the interface FromWhereBuilder. This ensures that business classes are used by the right builders to create an SQL query appropriate to the tracker artifact structure.

So, we have one query builder per field type and comparison type. To use a text field with the following TQL query status=’Open’, several classes are used: OrExpression, AndExpression, and EqualComparison are created by the parser and are crossed to get the builder associated with the field in EqualComparison—in this case ForText, which is specified to construct an SQL query on text fields with the operator EQUAL.


Click to enlarge

Visitor Pattern

The Visitor Pattern can implement specific instructions on a business class. The advantage is that we can extend behaviors without changing business classes. For example, imagine you need to draw a shape, either a circle or a rectangle. A naive approach would be: “The circle (or the rectangle) knows how to draw itself, so we just need a method, draw(), in the Circle (or Rectangle) class”. This quickly becomes a problem as soon as you introduce new ways to interact with your objects.

Need to apply a transform() or scale()? Need to morph() a shape into another one? Adding new ways of interacting with objects will gradually transform our Circle and Rectangle into GodObjects, breaking the Single Responsibility Principle (SRP, the S in SOLID). Here is the Visitor Pattern:


You just add a new visitor—MorphVisitor, ScaleVisitor, etc.—to add new features. This also enforces the open/closed principle.

In TQL, each comparison or expression class is visitable by our QueryBuilderVisitor. The visitor has methods to do instructions by type (visitEqualComparison, visitNotEqualComparison, etc.). This is the same logic as for the shape example above. The visitor takes a visitable class and applies the generic method to it so that the class calls the right business method.


Click to enlarge

Here, the Visitor Pattern let us create stable business classes with the generic method accept(). Once we created all of our business classes we no longer needed to modify them.
So, to make the link between the grammar, PHP, and SQL query, we represented each expression and comparison type in the grammar in a PHP class containing only the field name and the value for comparisons, or the subexpression for each expression. We have, for example, EqualComparison, InComparison, BetweenComparison, etc. This is done directly in the parser, thanks to the grammar.
Here is an excerpt:

EqualComparison
   = field:Field _ "=" _ value_wrapper:SimpleExpr {
       return new EqualComparison($field, $value_wrapper);
   }

NotEqualComparison
   = field:Field _ "!=" _ value_wrapper:SimpleExpr {
       return new NotEqualComparison($field, $value_wrapper);
   }

LesserThanComparison
   = field:Field _ "<" _ value_wrapper:SimpleExpr {
       return new LesserThanComparison($field, $value_wrapper);
   }
...

BetweenComparison
   = field:Field _ "between"i _ "(" _ min_value_wrapper:SimpleExpr _ "," _ max_value_wrapper:SimpleExpr _ ")" {
       return new BetweenComparison($field, new BetweenValueWrapper($min_value_wrapper, $max_value_wrapper));
   }

NotInComparison
   = field:Field _ "not in"i _ "(" _ list:InComparisonValuesList _ "," ? _ ")" {
       return new NotInComparison($field, new InValueWrapper($list));
   }

InComparison
   = field:Field _ "in"i _ "(" _ list:InComparisonValuesList _ "," ? _ ")" {
       return new InComparison($field, new InValueWrapper($list));
   }

Workflow

We can now illustrate all steps between a TQL query and the associated SQL query using the patterns seen above. We have taken the same TQL query as above status=’Open’. We can see the query builder ForText returning an object FromWhere that encapsulates two different parts of the query. The first part is from, corresponding to the FROM part of the main SQL query, and the second part is where, corresponding to the WHERE part of the SQL query. This information will be used by our Tracker_Report to finalize the query and retrieve matching artifacts.


Click to enlarge

Invalid fields

We used the same strategy for our field validation structure. In other words, we used visitors to validate the query. We can check tree size with our SizeValidatorVisitor, or collect invalid fields used in the query with our InvalidFieldsCollectorVisitor. All without affecting the existing code.

In the field validation example, we used the same patterns to validate as we did to build the query. We used checkers by field type, respecting the common interface InvalidFieldChecker, and visitors to get the right checker.


Click to enlarge

Here you can see the different behavior applied to our syntax tree using design patterns.


One final word…

The final architecture adheres to several SOLID principles required to ensure robust code:
  • Single responsibility: business classes, builders, and checkers have only one responsibility per field type or syntax tree node type;
  • Open/closed: with Visitor Patterns our business classes never change, but we can extend the behavior by adding other comparison or expression classes, for example;
  • Liskov substitution: so that our classes respect the contract defined by their interfaces;
  • Interface segregation: the minimum contract is defined by the interfaces;
  • Dependency inversion: using polymorphism we inject interfaces and resolution is done dynamically; the code depends on abstractions not details.

Challenges

We encountered several challenges during implementation. But in the Enalean team, we are committed to continuous improvement, even when it comes to code. We prefer to develop a simple functional code and continue to improve it gradually over time.

The first challenge was that the first instance of TQL didn’t respect one of the SOLID principles: OCP. We used the PHP keyword instance of to recover the right builder according to the given field and build the SQL query. Of course, during a refactoring we decided to delete this big switch case on class type using the different patterns seen above.

Another difficulty was to implement several visitors to cope with the genericity of trackers and user queries. As of today we have 33 visitors in TQL code to support this complexity. However, since each class (business, visitors, builders, etc.) has a single responsibility, it makes things easy to update or extend for new behaviors.

What’s next…

Another story is expected in TQL concerning follow-up comments. Because the architecture is founded on SOLID principles, it is simple to extend by:

  • creating its own checker for the associate field
  • creating a builder for each comparison type
  • extending visitors

So watch the place...

Stay tuned Join Online meetings

Share this post

Leave a comment

To prevent automated submissions please leave this field empty.

Plain text

  • No HTML tags allowed.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Lines and paragraphs break automatically.