DEV Community

usapopopooon
usapopopooon

Posted on

The "Dynamic Pipeline" Pattern: A Mutable Method Chaining for Real-time Processing

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)
Enter fullscreen mode Exit fullscreen mode
// 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');
Enter fullscreen mode Exit fullscreen mode

Key Characteristics

  1. Addition (add): New processing steps can be appended at any time.
  2. Subtraction (remove): Existing steps can be detached dynamically.
  3. Update (update): Parameters for specific steps can be modified without rebuilding the whole pipeline.
  4. Order-sensitive: The sequence of addition determines the execution order.
  5. 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
Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode

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);
  }
});
Enter fullscreen mode Exit fullscreen mode

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)