While developing a drawing application that requires real-time stroke stabilization, I encountered a design challenge that I struggled to solve cleanly with standard Design Patterns.
I needed to manage multiple processing steps where I could add, remove, and update them dynamically at runtime based on user settings, all while maintaining a clean, method-chaining syntax.
I have tentatively named this the "Dynamic Pipeline" pattern, though I am not entirely sure if this is the best name or if a similar concept already exists under a different name. As this is a concept born from a personal hobby project, I would greatly appreciate any feedback, corrections, or pointers to existing similar patterns.
What is the Dynamic Pipeline?
The way I see it, it is a method-chaining pattern that allows for the "addition and subtraction" of processing steps at runtime.
Processor (Mutable)
+------------------------------------+
| |
Input ((Data)) ---------> | Filter A --> Filter B --> Filter C | ---------> Output ((Data))
| |
+------------------------------------+
^ ^ ^
| | |
User Settings / Events -------------+---------+---------+
(add / update / remove)
// Incrementally add processing steps
const processor = new Processor()
.addFilterA(param1) // + Process A
.addFilterB(param2) // + Process B
.addFilterC(param3); // + Process C
// Update parameters later
processor.updateFilterA(newParam);
// Subtract (remove) a process
processor.removeFilter('B');
Key Characteristics
- Addition (add): New processing steps can be appended at any time.
- Subtraction (remove): Existing steps can be detached dynamically.
- Update (update): Parameters for specific steps can be modified without rebuilding the whole pipeline.
- Order-sensitive: The sequence of addition determines the execution order.
-
Immediate Use: No
.build()call is required; the processor is always in an executable state.
A Metaphor: Human Growth and Experience
This pattern can be intuitively understood through the metaphor of a person gaining experience and growing over time.
const person = new Person()
.addEducation('University') // + Received education
.addSkill('Programming') // + Acquired a skill
.addExperience('Living abroad') // + Gained experience
.addTrauma('Major failure'); // + Even scars become part of one's identity
Just as humans evolve, you "add" abilities and experiences. And life continues to shape us:
// Forgetting a skill over time (remove)
person.removeSkill('Programming');
// Years later, leveling up through practice (update)
person.updateSkill('Programming', { level: 'expert' });
// Losing something precious (remove)
person.removeExperience('Living abroad');
Depending on the context, you can exercise, update, or let go of these attributes.
Comparison with Existing Patterns
Below is my attempt to compare this approach with established patterns. I may be oversimplifying some of these, so please take this with a grain of salt.
| Pattern | Operation | Order | Runtime Mutation |
|---|---|---|---|
| Builder | Configure | Irrelevant | Immutable after .build()
|
| Decorator | Wrap | Relevant | Difficult to "unwrap" once applied |
| Middleware | Register | Relevant | Usually static after initial registration |
| RxJS pipe | Transform | Relevant | Immutable (always returns a new instance) |
| Chain of Responsibility | Link | Relevant | "One handles and stops the chain" |
| Dynamic Pipeline | Add/Rem/Upd | Relevant | Fully Mutable |
If I had to summarize, the potential advantage seems to lie in balancing a structured pipeline with the flexibility of dynamic runtime adjustments.
How I Used It: Stroke Stabilization
Here is how I applied this approach in my own project. Please note that stroke stabilization is a fairly niche topic—if you are unfamiliar with it, feel free to skip this section. The pattern itself is not limited to this domain.
In my implementation, stroke stabilization involves applying convolution-based filters (such as Gaussian smoothing or Kalman filtering) to raw pointer input. These filters need to be responsive to user interaction.
const pointer = new StabilizedPointer()
.addNoiseFilter(1.5)
.addKalmanFilter(0.1)
.addGaussianFilter(5);
// Adjusting settings via the UI
settingsPanel.onChange((settings) => {
pointer.updateNoiseFilter(settings.noiseThreshold);
if (!settings.useSmoothing) {
pointer.removeFilter('gaussian');
}
});
// Dynamic adjustments based on pen velocity
pointer.onVelocityChange((velocity) => {
if (velocity > 500) {
// Prioritize performance by removing heavy filters during high-speed motion
pointer.removeFilter('gaussian');
} else {
pointer.addGaussianFilter(5);
}
});
Possible Implementation Strategies
The following are some ideas for implementation that may be worth considering.
1. Array-based Pipeline Management
One straightforward approach might be to maintain the pipeline in a simple array, which would help preserve the order of addition during execution.
2. Identification via Type or Unique ID
Processing steps could potentially be identified by their type (e.g., 'validation', 'transform') for simple use cases. If your application requires multiple instances of the same step type, managing them with unique IDs might be a more robust approach.
3. Caching the Pipeline as a Single Function
For high-frequency execution scenarios, it might be beneficial to cache the pipeline as a single function whenever the configuration changes. This could help minimize runtime overhead, though I have not tested this approach in practice.
Final Thoughts
I am not sure if this qualifies as a "pattern" in the formal sense—it might be closer to an architectural idiom or simply a common-sense approach that many developers already use intuitively.
That said, I have found this approach helpful for my specific use case of bridging a clean, readable API with the need for dynamic, real-time adjustments.
If there is an established name for this approach, or if you see any potential pitfalls in maintaining a mutable pipeline, I would sincerely appreciate your insights in the comments. Thank you for reading.
Top comments (0)