Plug and play architecture, also known as modular architecture, is an approach where software components or modules can be easily added, removed, or replaced within a system without requiring significant modifications to the existing codebase. The architecture is designed to be highly flexible and allows for seamless integration of new functionalities or enhancements.
1. core system
- plugin registry
- runtime module (responsible for managing plugin lifecycles: loading, initialization)
- communication module
A standalone independent components that can have extension points. Extension point enables other plugins to extend its behavior.
Starting plugin is done by core system which has to
• Find plug at specific location
• Load and validate files
• Start plugin
In my experience I've found there are three types of plug-in Architectures
One follows the Eclipse model which is meant to allow for freedom and is open-ended
The other usually requires plugins to follow a narrow API because the plugin will fill a specific function
To state this in a different way, one allows plugins to access your application while the other allows your application to access plugins.
- combining scripting languages and libraries, which can be mixed in various proportions, similar to the balance between functional and object-oriented programming.
Managing libraries with scripts forms an effective architectural approach. This method leverages scripting languages for high-level manipulation of lower-level libraries, freeing the libraries from specific interfaces and placing application-level interaction in the hands of the scripting system.
There are typically two main approaches to this:
- configuration files (YAML, JSON, XML) In this approach, the application uses external configuration files to identify, configure, and load plugins. Each plugin's metadata, such as its name, location, and configuration parameters, is specified in these files.
in the code (annotations)
In this approach, the application uses code annotations or attributes within the source code to identify and configure plugins. Developers mark classes or methods with annotations that indicate their roles as plugins. For example, in Java, you might use annotations like
@Componentto mark class as plugins.
Plugin loading can occur at either compile time or runtime, and each approach has its own advantages and use cases. Here's an explanation of both:
Compile-Time Plugin Loading: involves including or linking the plugin code into the main application during the compilation process.
Efficiency: Compile-time loading typically results in faster execution since the code is already integrated into the main application.
Early Error Detection: Compilation can catch errors in the plugin code early in the development process.
Optimization: The compiler can perform optimizations that take into account the entire codebase, including the plugins.
C and C++ programs often use compile-time loading for plugins through dynamic linking or static libraries.
Some Java frameworks, like Apache Maven, load plugins at compile time when building projects.
Runtime Plugin Loading:
Runtime loading involves discovering and loading plugins when the application is already running
Dynamic: Allows for adding, removing, or updating plugins without recompiling the main application.
Flexibility: Useful when the set of plugins may change over time or needs to be customizable by users.
Extensibility: Allows third-party developers to create and distribute plugins independently.
Also Java and .NET applications can use reflection to load classes dynamically at runtime.
Plugin lifecycle management is an essential aspect of any plugin system, and it often involves defining hooks/callbacks and deciding whether plugins should run in single or in separate processes. Let's explore each of these concepts:
Hooks in the context of a plugin system, are predefined points in the application's execution flow where plugins can insert their custom code or behavior. These hooks/callbacks enable plugins to interact with the core application at specific moments, such as before or after certain events or processes. Here's how hooks work:
The core systems defines hooks at strategic points.
Plugins register themselves with these hooks, specifying which parts of their code should execute when the hook is triggered.
When the application reaches a hook, it notifies all registered plugins, allowing them to execute their custom code in response to the event.
Use Cases: before-and-after processing, like on start on load, on delete.
Hooks/callbacks vs extension points, while related concepts in the Plug in Play architecture they are distinct and should not be mixed.
Deciding whether plugins should run in same process as the application's process or in a separate processes is an important architectural choice and depends on various factors:
In this approach, plugins run within the same process as the core application- They share the same memory space and resources, which can lead to efficient communication and data sharing.
- Fast communication
- low overhead
- easy access to shared data structures.
Use Cases: Suitable for lightweight plugins or those that need close integration with the core application, such as UI components or low-level libraries.
Separate Process Plugins:
In this approach, each plugin runs in its own separate process or container. Plugins and the core application communicate through defined interfaces or protocols, often over a network or inter-process communication (IPC).
fault tolerance (if one plugin crashes, it doesn't bring down the entire application),
Use Cases: Useful when plugins need to be sandboxed for security reasons, or when they are developed and maintained independently, like in microservices architectures.
In a plugin-based architecture, communication between plugins is a crucial aspect that allows them to collaborate and interact to achieve the desired functionality. Here are some methods for plugin communication:
Shared Data and State:
Shared Data Structures: This could be in-memory data structures, configuration files, or shared databases.
Global State: Have a global state or context that plugins can read from and write to.
Events and Notifications:
Event Bus: Use an event bus or messaging system that allows plugins to subscribe to and publish events. When an event of interest occurs, plugins can react accordingly.
Observer Pattern: Use the observer pattern, where plugins can register as observers or listeners for specific events or changes in state. When the observed event occurs, registered plugins are notified.
Inter-Process Communication (IPC):
For separate process plugins, use IPC mechanisms like sockets, pipes, or message brokers to enable communication between processes.
Define RESTful Endpoints: Use RESTful APIs with HTTP endpoints for communication between plugins, especially in web-based applications.
Generally, plug-in modules should be independent of other plug-in modules, but you can certainly design plug-ins that require other plug-ins to be present
It is important to keep the communication between plug-ins to a minimum to avoid dependency issues.
Try to keep your non-extensible/non-user-modifiable core as small as possible. Delegate everything you can to a higher layer so that the extensibility increases so that you have Less stuff to correct in the core.
This is just the beginning. In our next posts, we'll delve into real-world examples of plug-and-play architecture. We'll explore how Drupal, WordPress, Visual Studio Code, Google Chrome and Eclipse IDE implement this architecture in practice.