Case Study 1: Building a Plugin System

The Scenario

Imagine you are building a text editor called MicroEdit. The editor itself handles basic text operations — opening files, editing content, saving. But users want more: word counting, spell checking, auto-formatting, syntax highlighting, and dozens of other features you cannot predict in advance.

You could build every feature into the editor, but that creates a monolithic codebase where every new feature means modifying existing code (violating the Open/Closed Principle). Instead, you decide to build a plugin system: a framework that lets anyone — including third-party developers — write new features that the editor can load and execute without modification.

This case study walks through the design and implementation of such a system using interfaces.

Defining the Plugin Interface

The core of any plugin system is its contract — the interface that every plugin must implement:

unit MicroEdit.PluginAPI;

{$mode objfpc}{$H+}

interface

type
  TPluginCapability = (pcTextTransform, pcAnalysis, pcAutoAction, pcUIExtension);
  TPluginCapabilities = set of TPluginCapability;

  IPlugin = interface
    ['{A1A1A1A1-B2B2-C3C3-D4D4-E5E5E5E5E5E5}']
    function GetName: String;
    function GetVersion: String;
    function GetDescription: String;
    function GetCapabilities: TPluginCapabilities;
    procedure Initialize;
    procedure Execute(var AText: String);
    procedure Shutdown;
  end;

implementation

end.

This interface defines the complete lifecycle of a plugin: it can identify itself (GetName, GetVersion, GetDescription), declare its capabilities, start up (Initialize), do work (Execute), and clean up (Shutdown).

Implementing Concrete Plugins

The Word Count Plugin

unit MicroEdit.Plugins.WordCount;

{$mode objfpc}{$H+}

interface

uses
  SysUtils, MicroEdit.PluginAPI;

type
  TWordCountPlugin = class(TInterfacedObject, IPlugin)
  private
    FWordCount: Integer;
    function CountWords(const S: String): Integer;
  public
    function GetName: String;
    function GetVersion: String;
    function GetDescription: String;
    function GetCapabilities: TPluginCapabilities;
    procedure Initialize;
    procedure Execute(var AText: String);
    procedure Shutdown;
  end;

implementation

function TWordCountPlugin.GetName: String;
begin
  Result := 'Word Count';
end;

function TWordCountPlugin.GetVersion: String;
begin
  Result := '1.0.0';
end;

function TWordCountPlugin.GetDescription: String;
begin
  Result := 'Counts words in the current document and displays the result.';
end;

function TWordCountPlugin.GetCapabilities: TPluginCapabilities;
begin
  Result := [pcAnalysis];
end;

procedure TWordCountPlugin.Initialize;
begin
  FWordCount := 0;
  WriteLn('[WordCount] Plugin initialized.');
end;

function TWordCountPlugin.CountWords(const S: String): Integer;
var
  i: Integer;
  InWord: Boolean;
begin
  Result := 0;
  InWord := False;
  for i := 1 to Length(S) do
  begin
    if S[i] in [' ', #9, #10, #13] then
    begin
      if InWord then
      begin
        Inc(Result);
        InWord := False;
      end;
    end
    else
      InWord := True;
  end;
  if InWord then
    Inc(Result);
end;

procedure TWordCountPlugin.Execute(var AText: String);
begin
  FWordCount := CountWords(AText);
  WriteLn(Format('[WordCount] Document contains %d words.', [FWordCount]));
end;

procedure TWordCountPlugin.Shutdown;
begin
  WriteLn('[WordCount] Plugin shut down. Final count: ', FWordCount);
end;

end.

The Auto-Trimmer Plugin

unit MicroEdit.Plugins.AutoTrim;

{$mode objfpc}{$H+}

interface

uses
  SysUtils, MicroEdit.PluginAPI;

type
  TAutoTrimPlugin = class(TInterfacedObject, IPlugin)
  private
    FLinesProcessed: Integer;
  public
    function GetName: String;
    function GetVersion: String;
    function GetDescription: String;
    function GetCapabilities: TPluginCapabilities;
    procedure Initialize;
    procedure Execute(var AText: String);
    procedure Shutdown;
  end;

implementation

function TAutoTrimPlugin.GetName: String;
begin
  Result := 'Auto Trim';
end;

function TAutoTrimPlugin.GetVersion: String;
begin
  Result := '1.0.0';
end;

function TAutoTrimPlugin.GetDescription: String;
begin
  Result := 'Removes trailing whitespace from every line.';
end;

function TAutoTrimPlugin.GetCapabilities: TPluginCapabilities;
begin
  Result := [pcTextTransform, pcAutoAction];
end;

procedure TAutoTrimPlugin.Initialize;
begin
  FLinesProcessed := 0;
  WriteLn('[AutoTrim] Plugin initialized.');
end;

procedure TAutoTrimPlugin.Execute(var AText: String);
var
  Lines: TStringArray;
  i: Integer;
begin
  Lines := AText.Split([LineEnding]);
  FLinesProcessed := Length(Lines);
  for i := 0 to High(Lines) do
    Lines[i] := TrimRight(Lines[i]);
  AText := String.Join(LineEnding, Lines);
  WriteLn(Format('[AutoTrim] Trimmed %d lines.', [FLinesProcessed]));
end;

procedure TAutoTrimPlugin.Shutdown;
begin
  WriteLn('[AutoTrim] Plugin shut down. Total lines processed: ', FLinesProcessed);
end;

end.

The Uppercase Converter Plugin

unit MicroEdit.Plugins.UpperCase;

{$mode objfpc}{$H+}

interface

uses
  SysUtils, MicroEdit.PluginAPI;

type
  TUpperCasePlugin = class(TInterfacedObject, IPlugin)
  public
    function GetName: String;
    function GetVersion: String;
    function GetDescription: String;
    function GetCapabilities: TPluginCapabilities;
    procedure Initialize;
    procedure Execute(var AText: String);
    procedure Shutdown;
  end;

implementation

function TUpperCasePlugin.GetName: String;
begin
  Result := 'Uppercase Converter';
end;

function TUpperCasePlugin.GetVersion: String;
begin
  Result := '1.0.0';
end;

function TUpperCasePlugin.GetDescription: String;
begin
  Result := 'Converts all text to uppercase.';
end;

function TUpperCasePlugin.GetCapabilities: TPluginCapabilities;
begin
  Result := [pcTextTransform];
end;

procedure TUpperCasePlugin.Initialize;
begin
  WriteLn('[UpperCase] Plugin initialized.');
end;

procedure TUpperCasePlugin.Execute(var AText: String);
begin
  AText := UpperCase(AText);
  WriteLn('[UpperCase] Text converted to uppercase.');
end;

procedure TUpperCasePlugin.Shutdown;
begin
  WriteLn('[UpperCase] Plugin shut down.');
end;

end.

The Plugin Manager

The TPluginManager class manages the lifecycle of all registered plugins:

unit MicroEdit.PluginManager;

{$mode objfpc}{$H+}

interface

uses
  SysUtils, MicroEdit.PluginAPI;

type
  TPluginManager = class
  private
    FPlugins: array of IPlugin;
    FCount: Integer;
  public
    constructor Create;
    procedure RegisterPlugin(APlugin: IPlugin);
    procedure InitializeAll;
    procedure ExecuteAll(var AText: String);
    procedure ShutdownAll;
    procedure ListPlugins;
    property Count: Integer read FCount;
  end;

implementation

constructor TPluginManager.Create;
begin
  inherited Create;
  FCount := 0;
  SetLength(FPlugins, 0);
end;

procedure TPluginManager.RegisterPlugin(APlugin: IPlugin);
begin
  Inc(FCount);
  SetLength(FPlugins, FCount);
  FPlugins[FCount - 1] := APlugin;
end;

procedure TPluginManager.InitializeAll;
var
  i: Integer;
begin
  WriteLn('=== Initializing ', FCount, ' plugins ===');
  for i := 0 to FCount - 1 do
    FPlugins[i].Initialize;
  WriteLn('=== All plugins initialized ===');
  WriteLn;
end;

procedure TPluginManager.ExecuteAll(var AText: String);
var
  i: Integer;
begin
  WriteLn('=== Executing all plugins ===');
  for i := 0 to FCount - 1 do
    FPlugins[i].Execute(AText);
  WriteLn('=== All plugins executed ===');
  WriteLn;
end;

procedure TPluginManager.ShutdownAll;
var
  i: Integer;
begin
  WriteLn('=== Shutting down all plugins ===');
  for i := FCount - 1 downto 0 do
    FPlugins[i].Shutdown;
  SetLength(FPlugins, 0);
  FCount := 0;
  WriteLn('=== All plugins shut down ===');
end;

procedure TPluginManager.ListPlugins;
var
  i: Integer;
begin
  WriteLn('Registered Plugins:');
  for i := 0 to FCount - 1 do
    WriteLn(Format('  %d. %s v%s — %s',
      [i + 1, FPlugins[i].GetName, FPlugins[i].GetVersion,
       FPlugins[i].GetDescription]));
  WriteLn;
end;

end.

Putting It All Together

program MicroEditDemo;

{$mode objfpc}{$H+}

uses
  SysUtils, MicroEdit.PluginAPI, MicroEdit.PluginManager,
  MicroEdit.Plugins.WordCount, MicroEdit.Plugins.AutoTrim,
  MicroEdit.Plugins.UpperCase;

var
  Manager: TPluginManager;
  DocumentText: String;
begin
  DocumentText := 'Hello, world!   ' + LineEnding +
                  'This is MicroEdit.  ' + LineEnding +
                  'Plugins make it extensible.   ';

  WriteLn('Original text:');
  WriteLn(DocumentText);
  WriteLn;

  Manager := TPluginManager.Create;
  try
    Manager.RegisterPlugin(TWordCountPlugin.Create);
    Manager.RegisterPlugin(TAutoTrimPlugin.Create);
    Manager.RegisterPlugin(TUpperCasePlugin.Create);

    Manager.ListPlugins;
    Manager.InitializeAll;
    Manager.ExecuteAll(DocumentText);

    WriteLn('Processed text:');
    WriteLn(DocumentText);
    WriteLn;

    Manager.ShutdownAll;
  finally
    Manager.Free;
  end;
end.

Design Analysis

Why Interfaces Were the Right Choice

  1. Open/Closed Principle: Adding a new plugin means writing one new class. The TPluginManager, the IPlugin interface, and all existing plugins remain unchanged.
  2. Loose coupling: The manager knows nothing about specific plugin implementations. It only knows IPlugin.
  3. Independent development: Each plugin is in its own unit. Different developers can work on different plugins without conflicts.
  4. Testability: You can create a TMockPlugin for testing the manager without any real plugin logic.

Extension Points

In a production system, you would extend this further: - Priority ordering: Add a GetPriority method to IPlugin so the manager executes plugins in the correct order. - Capability filtering: Execute only plugins with specific capabilities (e.g., only pcTextTransform plugins). - Dynamic loading: Load plugins from DLLs at runtime using LoadLibrary and function pointers — a technique we will explore in Chapter 37. - Error isolation: Wrap each Execute call in a try..except block (Chapter 19) so one crashing plugin does not bring down the entire editor.

The Takeaway

A plugin system is the canonical use case for interfaces. The system defines a contract; plugins fulfill it. The system does not depend on any specific plugin; plugins do not depend on each other. This is the essence of good software architecture: components that communicate through abstractions and can evolve independently.