Business rule combination of the Specification pattern
Combining Business Rules with the Specification Pattern
The Specification pattern is a behavioral design pattern used to encapsulate business rules into reusable objects and combine these rules through logical operations. It is particularly suitable for handling complex business condition judgment scenarios, making the code clearer and easier to maintain.
Core Concepts of the Specification Pattern
The Specification pattern consists of three core components:
- Specification Interface: Defines the validation method.
- Concrete Specification: Implements specific business rules.
- Composite Specification: Combines multiple specifications through logical operations.
// Specification Interface
class Specification {
isSatisfiedBy(candidate) {
throw new Error('Method not implemented');
}
and(otherSpec) {
return new AndSpecification(this, otherSpec);
}
or(otherSpec) {
return new OrSpecification(this, otherSpec);
}
not() {
return new NotSpecification(this);
}
}
Concrete Specification Implementation
Concrete specifications are classes that implement specific business rules. For example, in an e-commerce system, there might be the following specifications:
// Price Specification
class PriceSpecification extends Specification {
constructor(minPrice, maxPrice) {
super();
this.minPrice = minPrice;
this.maxPrice = maxPrice;
}
isSatisfiedBy(item) {
return item.price >= this.minPrice && item.price <= this.maxPrice;
}
}
// Category Specification
class CategorySpecification extends Specification {
constructor(category) {
super();
this.category = category;
}
isSatisfiedBy(item) {
return item.category === this.category;
}
}
// In-Stock Specification
class InStockSpecification extends Specification {
isSatisfiedBy(item) {
return item.stock > 0;
}
}
Composite Specification Implementation
Composite specifications allow combining multiple specifications through logical operators:
// AND Composite Specification
class AndSpecification extends Specification {
constructor(left, right) {
super();
this.left = left;
this.right = right;
}
isSatisfiedBy(candidate) {
return this.left.isSatisfiedBy(candidate) &&
this.right.isSatisfiedBy(candidate);
}
}
// OR Composite Specification
class OrSpecification extends Specification {
constructor(left, right) {
super();
this.left = left;
this.right = right;
}
isSatisfiedBy(candidate) {
return this.left.isSatisfiedBy(candidate) ||
this.right.isSatisfiedBy(candidate);
}
}
// NOT Composite Specification
class NotSpecification extends Specification {
constructor(spec) {
super();
this.spec = spec;
}
isSatisfiedBy(candidate) {
return !this.spec.isSatisfiedBy(candidate);
}
}
Practical Application Example
Assume we have an e-commerce product filtering system:
// Product data
const products = [
{ id: 1, name: 'Laptop', category: 'Electronics', price: 999, stock: 5 },
{ id: 2, name: 'Smartphone', category: 'Electronics', price: 699, stock: 0 },
{ id: 3, name: 'Desk', category: 'Furniture', price: 199, stock: 10 },
{ id: 4, name: 'Chair', category: 'Furniture', price: 149, stock: 15 },
{ id: 5, name: 'Monitor', category: 'Electronics', price: 299, stock: 8 }
];
// Create specifications
const electronicsSpec = new CategorySpecification('Electronics');
const priceSpec = new PriceSpecification(100, 500);
const inStockSpec = new InStockSpecification();
// Combine specifications: Electronics category, price between 100-500, and in stock
const combinedSpec = electronicsSpec
.and(priceSpec)
.and(inStockSpec);
// Filter products
const filteredProducts = products.filter(p => combinedSpec.isSatisfiedBy(p));
console.log(filteredProducts);
// Output: [{ id: 5, name: 'Monitor', category: 'Electronics', price: 299, stock: 8 }]
Advanced Usage of the Specification Pattern
Parameterized Specifications
Specifications can accept parameters for more flexible condition judgments:
class DynamicPriceSpecification extends Specification {
constructor(getPriceRange) {
super();
this.getPriceRange = getPriceRange;
}
isSatisfiedBy(item) {
const { min, max } = this.getPriceRange();
return item.price >= min && item.price <= max;
}
}
// Usage
const dynamicPriceSpec = new DynamicPriceSpecification(() => {
// Can fetch price range from configuration or API
return { min: 100, max: 500 };
});
Asynchronous Specifications
For scenarios requiring asynchronous validation:
class AsyncStockSpecification extends Specification {
constructor(stockService) {
super();
this.stockService = stockService;
}
async isSatisfiedBy(item) {
const stockInfo = await this.stockService.getStock(item.id);
return stockInfo.quantity > 0;
}
}
// Usage
const asyncFilter = async (products, spec) => {
const results = [];
for (const product of products) {
if (await spec.isSatisfiedBy(product)) {
results.push(product);
}
}
return results;
};
Specification Pattern and Domain-Driven Design
The Specification pattern plays an important role in Domain-Driven Design (DDD):
- Repository Pattern: Used for querying data.
- Specification Pattern: Used to define query conditions.
- Domain Model: Contains business logic.
class ProductRepository {
constructor(products = []) {
this.products = products;
}
findAll(specification) {
return this.products.filter(p => specification.isSatisfiedBy(p));
}
findOne(specification) {
return this.products.find(p => specification.isSatisfiedBy(p));
}
}
// Usage
const repo = new ProductRepository(products);
const expensiveElectronicsSpec = new CategorySpecification('Electronics')
.and(new PriceSpecification(500, Infinity));
const results = repo.findAll(expensiveElectronicsSpec);
Performance Optimization for the Specification Pattern
For large datasets, consider the following optimization strategies:
- Early Termination: Implement short-circuit evaluation in composite specifications.
- Result Caching: Cache validation results for immutable data.
- Query Conversion: Convert specifications into database query conditions.
class OptimizedAndSpecification extends Specification {
constructor(left, right) {
super();
this.left = left;
this.right = right;
}
isSatisfiedBy(candidate) {
// Short-circuit evaluation: If the left side is not satisfied, skip evaluating the right side
return this.left.isSatisfiedBy(candidate) &&
this.right.isSatisfiedBy(candidate);
}
// Convert to SQL WHERE condition
toSQL() {
return `${this.left.toSQL()} AND ${this.right.toSQL()}`;
}
}
// Usage
const sqlCondition = combinedSpec.toSQL();
// Output: "category = 'Electronics' AND price BETWEEN 100 AND 500 AND stock > 0"
Testing Strategies for the Specification Pattern
When testing the Specification pattern, a layered testing approach can be adopted:
// Unit test concrete specifications
describe('PriceSpecification', () => {
it('should satisfy when price in range', () => {
const spec = new PriceSpecification(100, 200);
const item = { price: 150 };
expect(spec.isSatisfiedBy(item)).toBe(true);
});
});
// Test composite specifications
describe('AndSpecification', () => {
it('should satisfy when both specs are satisfied', () => {
const spec1 = new MockSpecification(true);
const spec2 = new MockSpecification(true);
const andSpec = new AndSpecification(spec1, spec2);
expect(andSpec.isSatisfiedBy({})).toBe(true);
});
});
// Integration testing
describe('ProductFilter', () => {
it('should filter products correctly', () => {
const spec = /* composite specification */;
const filtered = productFilter.filter(products, spec);
expect(filtered.length).toBe(2);
});
});
Application of the Specification Pattern in Frontend State Management
The Specification pattern can be used for complex state filtering:
// Specification pattern in Redux selectors
const createTodoSelector = (filterSpec) => (state) => {
return state.todos.filter(todo => filterSpec.isSatisfiedBy(todo));
};
// Define specifications
const activeTodoSpec = new AndSpecification(
new NotSpecification(new IsCompletedSpecification()),
new NotSpecification(new IsArchivedSpecification())
);
// Use selector
const selectActiveTodos = createTodoSelector(activeTodoSpec);
const activeTodos = selectActiveTodos(store.getState());
Specification Pattern and Functional Programming
The Specification pattern aligns well with functional programming principles:
// Functional implementation
const createSpec = (predicate) => ({
isSatisfiedBy: predicate,
and: (other) => createSpec(c => predicate(c) && other.isSatisfiedBy(c)),
or: (other) => createSpec(c => predicate(c) || other.isSatisfiedBy(c)),
not: () => createSpec(c => !predicate(c))
});
// Usage
const isEven = createSpec(n => n % 2 === 0);
const isPositive = createSpec(n => n > 0);
const spec = isEven.and(isPositive);
[1, 2, 3, 4].filter(n => spec.isSatisfiedBy(n)); // [2, 4]
Application of the Specification Pattern in UI Components
Using the Specification pattern in React components to control rendering:
const ProductList = ({ products }) => {
const [priceRange, setPriceRange] = useState({ min: 0, max: 1000 });
const [category, setCategory] = useState('all');
const filterSpec = new AndSpecification(
category === 'all'
? new AlwaysTrueSpecification()
: new CategorySpecification(category),
new PriceSpecification(priceRange.min, priceRange.max)
);
const filteredProducts = products.filter(p => filterSpec.isSatisfiedBy(p));
return (
<div>
{/* Filter controls */}
{filteredProducts.map(product => (
<ProductItem key={product.id} product={product} />
))}
</div>
);
};
Variants and Extensions of the Specification Pattern
- Interpreter Specification: Converts specifications into interpretable expressions.
- Weighted Specification: Adds weights to specifications for weighted scoring.
- Partial Satisfaction Specification: Returns the degree of satisfaction rather than a boolean value.
// Weighted scoring specification
class WeightedSpecification extends Specification {
constructor(spec, weight) {
super();
this.spec = spec;
this.weight = weight;
}
isSatisfiedBy(candidate) {
return this.spec.isSatisfiedBy(candidate);
}
getScore(candidate) {
return this.isSatisfiedBy(candidate) ? this.weight : 0;
}
}
// Usage
const scoringSpec = new WeightedSpecification(
new CategorySpecification('Electronics'), 0.6)
.and(new WeightedSpecification(
new PriceSpecification(100, 500), 0.4));
function sortByScore(items, spec) {
return items
.map(item => ({
item,
score: spec.getScore(item)
}))
.sort((a, b) => b.score - a.score)
.map(({ item }) => item);
}
本站部分内容来自互联网,一切版权均归源网站或源作者所有。
如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn