CLOSE
Updated on 12 Jul, 202629 mins read 24 views

Introduction

In the previous chapter, we discovered a fundamental problem in software architecture.

As applications grow, new features constantly appear:

  • SEO metadata
  • Advertisements
  • Analytics
  • Logging
  • Caching
  • Notifications
  • Custom themes
  • Third-party plugins

If every feature requires modifying existing framework code, the application gradually becomes difficult to maintain.

We concluded that instead of allowing plugins to edit the framework, the framework should expose extension points where plugins can participate.

One of the most widely used extension mechanisms is the Filter.

Filters are used by CMSs, web frameworks, compilers, game engines, logging systems, middleware pipelines, and countless other software systems.

Although the name "Filter" sounds complicated, the underlying idea is remarkably simple.

A filter receives a value, optionally modifies it, and passes the modified value to the next filter.

That single concept is powerful enough to build entire plugin ecosystems.

What Is a Filter?

A filter is a function that accepts a value, transforms it if necessary, and returns the result.

Mathematically, it can be represented as:

Output = Filter(Input)

For example,

Input: Hello World
Filter: Convert everything to uppercase
Output: HELLO WORLD

The important point is that the original code doesn't know how the value was modified.

It simply receives the final result.

The Smallest Possible Filter

Consider the following function.

std::string addExclamation(const std::string& text)
{
   return text + "!";
}
Input: Hello
Output: Hello!

The function transformed the value.

That is exactly what a filter does.

A Filter Is Not an Event

Many beginners confuse filters with events.

Although they look similar, they solve different problems.

A filter changes data.

An event performs an action.

Filter:

Input
 ↓
Modify
 ↓
Return New Value

Event:

Something Happened
 ↓
Notify Listeners
 ↓
Done

Notice that events usually don't return anything.

Filters always return something.

A Real Example

Suppose our application displays blog articles.

Without filters:

std::string renderArticle()
{
   return article.content;
}

Output:

<p>Hello World</p>

Now suppose someone wants advertisements inserted.

Instead of changing the renderer, we execute a filter.

content = applyFilters("article.content", content);

The renderer still knows nothing about advertisements.

The advertisement plugin modifies the content.

Output becomes

<p>Hello World</p>
<div>Advertisement</div>

The renderer didn't change.

Only the filter did.

The Filter Pipeline

One filter is useful.

Multiple filters become extremely powerful.

Imagine three plugins.

Plugin A
	Trim spaces
Plugin B
	Convert to uppercase
Plugin C
	Add stars

Execution

Original
Hello
 ↓
Trim Hello
 ↓
Uppercase HELLO
 ↓
Stars ***HELLO***
 ↓
Final Output

Each filter receives the output produced by the previous one.

Visual Workflow

Original Value
       │
       ▼
+----------------------+
| Filter A             |
+----------------------+
       │
       ▼
+----------------------+
| Filter B             |
+----------------------+
       │
       ▼
+----------------------+
| Filter C             |
+----------------------+
       │
       ▼
Final Value

This sequence is often called a pipeline.

Why Pass the Value Along?

Suppose each filter always received the original value.

Example

Hello
Uppercase filter
HELLO
Star filter
***Hello***

Now which result should the framework use?

The filters are conflicting.

Instead, each filter receives the latest value.

Hello
↓
HELLO
↓
***HELLO***

Every modification builds upon the previous one.

The General Algorithm

Every filter system follows nearly the same algorithm.

value = original
for every registered filter
   value = filter(value)
return value

That's all.

Everything else—priorities, plugins, callbacks—is built on top of this simple loop.

Why Filters Return Values

Suppose we have three filters.

A
B
C

If Filter A didn't return anything, Filter B would have nothing to process.

Likewise, Filter C would receive nothing.

Returning the modified value creates a continuous processing chain.

Think of it like an assembly line in a factory.

Raw Material
↓
Machine A
↓
Machine B
↓
Machine C
↓
Finished Product

Each machine works on the result of the previous one.

The Framework Doesn't Know the Filters

This is one of the most important ideas.

The framework writes only this:

content = applyFilters("article.content", content);

It never writes

content = seoPlugin(content);
content = adsPlugin(content);
content = analyticsPlugin(content);

The framework has no knowledge of any plugin.

Instead, it simply asks:

"Who wants to modify this value?"

That makes the architecture extensible.

Registering Filters

Before a filter can execute, it must be registered.

Conceptually,

Register Filter
↓
Store Callback
↓
Wait

Nothing happens immediately.

The framework simply remembers that someone is interested in a particular hook.

Applying Filters

Later, the framework reaches an extension point.

applyFilters("article.content", html);

Internally,

Find every filter registered under
article.content
↓
Execute them one by one
↓
Return final value

The caller doesn't know how many filters exist.

It doesn't even know whether any filters exist.

Multiple Filters

Suppose four plugins register.

SEO
Ads
Translator
Markdown Parser

Execution becomes

Original HTML
↓
Markdown
↓
Translation
↓
SEO
↓
Ads
↓
Final HTML

Each plugin performs only its own responsibility.

None of them knows about the others.

Additional Parameters

Sometimes filters need more than just the value.

Imagine rendering a page.

Besides the HTML, a plugin might also need:

  • Current user
  • Current language
  • Current theme
  • Current page

Conceptually,

Filter(
HTML,
CurrentUser,
Language,
Theme
)

Only the first parameter is modified.

The remaining parameters provide context.

For example, an advertisement plugin may display different advertisements depending on the current language.

Execution Order

Suppose two filters exist.

Filter A Compress HTML
Filter B Inject Advertisement

If compression runs first,

the advertisement can no longer be inserted correctly.

Correct order:

Inject Advertisement
↓
Compress HTML

Execution order matters.

That is why filter systems support priorities.

Priorities

Each filter receives a priority number.

Example

Priority 10
SEO
Priority 20
Advertisements
Priority 30
Compression

Execution follows numerical order.

SEO
↓
Advertisements
↓
Compression

Priorities make execution deterministic.

No matter how many plugins are installed, the framework always knows which one executes first.

What Happens If No Filters Exist?

Suppose nobody registers anything.

Framework

content = applyFilters("article.content", content);

Internally,

No registered filters
↓
Return original value

Nothing breaks.

The framework behaves exactly as before.

This makes filters optional.

Plugins can come and go without affecting the framework.

Advantages of Filters

Filters provide several architectural benefits.

Open for Extension

New behavior can be added without editing existing framework code.

Loose Coupling

The framework never depends on plugin implementations.

Plugin Ecosystem

Independent developers can extend the application without modifying the core.

Reusability

Multiple plugins can reuse the same extension point.

Maintainability

The framework remains small while functionality continues to grow.

Runtime Extensibility

Plugins can be enabled or disabled without rewriting existing components.

Limitations

Filters are powerful, but they require discipline.

If too many plugins modify the same value, understanding the execution flow becomes difficult.

Poorly chosen priorities can lead to unexpected behavior.

Debugging may require inspecting every registered filter.

Excessive filtering can also introduce performance overhead if dozens of callbacks execute for every request.

These issues are manageable, but they highlight the importance of designing hook systems carefully.

Real-World Examples

Filters appear in many different forms.

A compiler transforms source code through multiple optimization passes.

A web framework modifies an HTTP request before it reaches a controller.

A logging library filters log messages before writing them to disk.

An image editor applies brightness, contrast, sharpening, and color correction filters in sequence.

A CMS allows plugins to modify HTML before it is sent to the browser.

Although the domains differ, the underlying idea is always the same:

Pass data through a sequence of independent transformations.
Buy Me A Coffee

Leave a comment

Your email address will not be published. Required fields are marked *

Your experience on this site will be improved by allowing cookies Cookie Policy