1. Why this project?

1.1. Specification pattern

The specification pattern is a particular software design pattern, whereby business rules can be recombined by chaining the business rules together using boolean logic.

— Wikipedia
https://en.wikipedia.org/wiki/Specification_pattern

Concretely, instead to implement rules using if/else like with this code:

boolean canSellToMinor;
if (!product.isDangerous() && (product.getType().equals("toy") || product.getAmount() < 10))
    canSellToMinor = true;

the business rules are implemented unitary and combined to produce the final rule:

Rule isDangerous, isToy, amountLessThan10 = ...;
Rule combined = isDangerous.not().and( isToy.or(amountLessThan10) );
boolean canSellToMinor = combined.test(product);

1.2. Need of dynamism

The specification pattern allow to produce readable and maintainable code. But the final source code is static: you have to update/compile/deploy application to change business rules.

The goal of this project is to define to manage and build dynamically rules at runtime, without any modification into source code.

2. General

3. Core API

The Rule interface is the base of specification pattern.

3.1. Simple usage

Definition:

class DangerousProductRule implements Rule<Product> {
    @Override
    public boolean test(Product param) {
        return param.isDangerous();
    }
}

Usage:

Rule rule = new DangerousProductRule();
Product product = new Product(...);
boolean isDangerousProduct = rule.test(product);

3.2. Composition

Example for this composition:

(NOT first) AND (second OR third)

based on these 3 basic rules:

Rule first = ...
Rule second = ...
Rule third = ...

3.2.1. Method chaining

Rule interface defines default methods, used for method chaining.

Rule composedRule = first.not().and( second.or(third) );

3.2.2. Composite pattern

AndRule/OrRule/NotRule are special Rule implementations, who work like composite pattern by accepting variable length argument into constructor.

import fr.pinguet62.springspecification.core.api.*;

Rule composedRule = new AndRule(
                        new NotRule(
                            first
                        ),
                        new OrRule(
                            second,
                            third
                        )
                    );

3.2.3. Utility methods

and()/or()/not() are static methods of RuleUtils.

import static fr.pinguet62.springspecification.core.api.RuleUtils.*;

Rule composedRule = and(
                        not(
                            first
                        ),
                        or(
                            second,
                            third
                        )
                    );

3.3. Parameters

Create minimal parameterized rules, instead of many specific rules.

Don’t:

class ToyProductRule {}
class FoodProductRule {}
...

Do:

class TypeProductRule implements Rule<Product> {
    String type;

    TypeProductRule(String param) {
        this.color = param;
    }

    // ...
}

Rule toyProductRule = new TypeProductRule("toy");
Rule foodProductRule = new TypeProductRule("food");
...

4. Builder

4.1. Rule component

4.1.1. Registering

To register a Rule like Spring component, the class must be annotated by @SpringRule.

@SpringRule
class CustomRule implements Rule<Product> {
    // ...
}

4.1.2. Key & Factory

All Rule are identified by unique key into database.

Default: the key is the Class::getName().

4.1.3. Factory

The factory use BeanFactory::getBean() to create an instance of rule.

4.2. Composite rule

Sub-rules are dynamically injected.

4.2.1. Annotation & injection

Use @RuleChild or @RuleChildren on field or setter or constructor argument to define the injection point of sub-rules (used into database).

class ComposedRule<T> implements Rule<T> {
    @RuleChildren(
    List<Rule<T>> subRules;

    // ...
}
class ComposedRule<T> implements Rule<T> {
    List<Rule<T>> subRules;

    @RuleChildren
    void setType(List<Rule<T>> subRules) {
        this.subRules = subRules;
    };

    // ...
}
class ComposedRule<T> implements Rule<T> {
    final List<Rule<T>> subRules;

    ComposedRule(@RuleChildren List<Rule<T>> subRules) {
        this.subRules = subRules;
    };

    // ...
}

4.2.2. Behavior

The injection works like @Autowired specification. So injection support:

  • conversion: Collection, array, varargs

4.3. Parameters

Parameters are dynamically injected.

4.3.1. Annotation & injection

Use @RuleParameter() on field or setter or constructor argument to define the injection point and the key of parameter (used into database).

class SampleRule implements Rule<T> {
    @RuleParameter("key")
    String param;

    // ...
}
class SampleRule implements Rule<T> {
    String param;

    @RuleParameter("key")
    void setType(String param) {
        this.param = param;
    };

    // ...
}
class SampleRule implements Rule<T> {
    final String param;

    SampleRule(@RuleParameter("key") String param) {
        this.param = param;
    };

    // ...
}

4.3.2. Behavior

The injection works like @Value specification. So injection support:

class SampleRule implements Rule<T> {
    @RuleParameter("rand")
    Integer param;

    // ...
}

TypeProductRule rule = ...;
// database parameter context = { "rand": "#{ T(java.lang.Math).ramdom() * 100 }" }
assertTrue(0 <= rule.param && rule.param <= 100);

4.4. Database

Because the target database usage is depending to the use case, the configuration of datasource and JPA options is fully delegated to user.

Tip
By default, the H2 is used for in-memory storage.

4.4.1. Configuration

Like this project use Spring Data JPA for database access, you have to declare these 3 beans:

4.4.2. Auto-configuration

To facilitate the configuration, this project defines and uses a Spring Boot auto-configuration module named spring-specification-datasource-autoconfiguration.

The behavior is the same than for Spring Boot (see Spring Boot - Working with SQL databases).
But to avoid collision, the differences are:

Warning
2 auto-configuration DataSourceAutoConfiguration and HibernateJpaAutoConfiguration will inevitably be enabled, because of Spring Boot Data JPA transitive dependency.
If there is any side effect because you don’t use database, you can exclude theses auto-configurations.

4.4.3. Example

pom.xml
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <version>LATEST</version>
</dependency>
application.properties
spring-specification:
    datasource:
        url: jdbc:h2:mem:testdb (1)
        data: spring-specification/data.sql (2)
    jpa:
        show-sql: true (3)
  1. Target database

  2. Script to initialize the database (default value, if present)

  3. Enable logging

5. Admin application

5.1. Admin server

The REST API exposed can be deployed separately by including the dependency into Spring Boot project:

pom.xml
<dependency>
    <groupId>fr.pinguet62</groupId>
    <artifactId>spring-specification-admin-server</artifactId>
    <version>LATEST</version>
</dependency>

Documentation: see /swagger-ui.html page.

5.2. Admin UI

The UI application can be deployed by including the dependency using Spring Boot project (in addition to server):

pom.xml
<dependency>
    <groupId>fr.pinguet62</groupId>
    <artifactId>spring-specification-admin-server</artifactId>
    <version>LATEST</version>
</dependency>
<dependency>
    <groupId>fr.pinguet62</groupId>
    <artifactId>spring-specification-admin-client</artifactId>
    <version>LATEST</version>
</dependency>

Index page: /spring-specification-admin-client.