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.
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.
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:
-
conversion
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:
-
springSpecification.dataSource
of typeDataSource
; -
springSpecification.entityManagerFactory
of typeEntityManagerFactory
; -
springSpecification.transactionManager
of typePlatformTransactionManager
.
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:
-
base properties:
spring-specification.datasource
andspring-specification.jpa
(see Spring Boot - Data Access); -
root folder for database initialization:
/spring-specification
(see Spring Boot - Database initialization); -
bean name are prefixed by
springSpecification.
.
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
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>LATEST</version>
</dependency>
spring-specification:
datasource:
url: jdbc:h2:mem:testdb (1)
data: spring-specification/data.sql (2)
jpa:
show-sql: true (3)
-
Target database
-
Script to initialize the database (default value, if present)
-
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:
<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):
<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
.