Edgewall Software

Changes between Initial Version and Version 1 of TracDev/ComponentArchitecture


Ignore:
Timestamp:
May 16, 2005, 5:01:13 PM (19 years ago)
Author:
Christopher Lenz
Comment:

Description of the component architecture from TracPluggableModules

Legend:

Unmodified
Added
Removed
Modified
  • TracDev/ComponentArchitecture

    v1 v1  
     1= Trac Component Architecture =
     2
     3As the heart of Trac, [source:/trunk/trac/core.py#latest trac.core] implements a minimal ''component kernel'' that allows components to easily extend each others functionality. It provides a ''"meta-plugin-API"'': every component can easily offer its own plugin API by declaring ''"extension points"''.
     4
     5== What is a Component? ==
     6
     7For our purposes, a ''component'' is an object that provides a certain type of service within the context of the application. There is at most one instance of any component: components are singletons. That implies that a component does '''not''' map to an entity of the application's object model; instead, components represent functional subsystems.
     8
     9Components can declare ''"extension points"'' that other components can “plug in” to. This allows one component to enhance the functionality of the component it extends, without the extended component even knowing that the extending component exists. All that is needed is that the original component exposes – and uses – one or more extension points.
     10
     11http://projects.edgewall.com/trac/attachment/wiki/TracPluggableModules/xtnpt.png?format=raw
     12
     13A component can extend any number of other components and still offer its own extension points. This allows a plugin to itself offer a plugin API (i.e. extension point). This feature is the basis for a plugin-based architecture.
     14
     15The actual functionality and APIs are defined by individual components. The component kernel provides the “magic glue” to hook the different subsystems together – without them necessarily knowing about each other.
     16
     17== Public classes ==
     18
     19{{{trac.core.ComponentManager}}} [[BR]]
     20  Manages component life cycle, instantiating registered components on demand
     21
     22{{{trac.core.Component}}} [[BR]]
     23  Abstract base class for components.
     24
     25{{{trac.core.ExtensionPoint}}} [[BR]]
     26  Declares an extension point on a component that other components can plug in to.
     27
     28{{{trac.core.Interface}}} [[BR]]
     29  Every extension point specifies the contract that extenders must conform to via an {{{Interface}}} subclass.
     30
     31http://projects.edgewall.com/trac/attachment/wiki/TracPluggableModules/comparch.png?format=raw
     32
     33== Declaring a component ==
     34
     35The simplest possible component is an empty class derived from {{{trac.core.Component}}}:
     36
     37{{{
     38from trac.core import *
     39
     40class MyComponent(Component):
     41    pass
     42}}}
     43
     44In the context of a component manager, this component can already be used:
     45
     46{{{
     47    comp_mgr = ComponentManager()
     48    my_comp = MyComponent(comp_mgr)
     49}}}
     50
     51Remember that components follow the singleton pattern. There is only one active instance of any component per component manager. The component constructor is "magic" in that it checks with the component manager whether there's already an active instance before allocating a new instance. If the component was already instantiated, the existing instance is returned:
     52
     53{{{
     54    my_comp1 = MyComponent(comp_mgr)
     55    my_comp2 = MyComponent(comp_mgr)
     56    assert id(my_comp1) == id(my_comp2)
     57}}}
     58
     59If a component needs to initialize data members, it can override the {{{__init__}}} method. But because the component manager needs to be able to instantiate the component on demand, {{{__init__}}} must ''not'' require any extra parameters, ''including'' the reference to the component manager passed into the constructor:
     60
     61{{{
     62    from trac.core import *
     63
     64    class MyComponent(Component):
     65        def __init__(self):
     66            self.data = {}
     67
     68    comp_mgr = ComponentManager()
     69    my_comp = MyComponent(comp_mgr)
     70}}}
     71
     72Direct {{{Component}}} sub-classes also do not need to worry about invoking the base classes {{{__init__}}} method (which is empty).
     73
     74== Declaring an extension point ==
     75
     76The component above doesn't actually do anything. Making an object a component only makes it act as a singleton in the scope of a component manager, which isn't that exciting in itself.
     77
     78The real value of components becomes clearer when the facilities for extensions are used. As a simple example, the following component provides an extension point that lets other components listen to changes to the data it manages (in this case a list of to-do items) – following the widely known observable pattern:
     79
     80{{{
     81    from trac.core import *
     82   
     83    class ITodoObserver(Interface):
     84        def todo_added(name, description):
     85            """Called when a to-do item is added."""
     86
     87    class TodoList(Component):
     88        observers = ExtensionPoint(ITodoObserver)
     89
     90        def __init__(self):
     91            self.todos = {}
     92
     93        def add(self, name, description):
     94            assert not name in self.todos, 'To-do already in list'
     95            self.todos[name] = description
     96            for observer in self.observers:
     97                observer.todo_added(name, description)
     98}}}
     99
     100Here, the {{{TodoList}}} class declares an extension point called {{{observers}}} with the interface {{{ITodoObserver}}}. The interface defines the ''contract'' that extending components need to conform to.
     101
     102The {{{TodoList}}} component notifies the observers inside the {{{add()}}} method by iterating over {{{self.observers}}} and calling the {{{todo_added()}}} method for each. The {{{Component}}} class performs some magic to enable that: Conceptually, it intercepts access to the extension point attribute and finds all registered components that declare to extend the extension point. For each of those components, it gets the instance from the component manager, potentially activating it if it is getting accessed for the first time.
     103
     104== Plugging in to an extension point ==
     105
     106Now that we have an extendable component, let's add another component that extends it:
     107
     108{{{
     109    class TodoPrinter(Component):
     110        implements(ITodoObserver)
     111
     112        def todo_added(self, name, description):
     113            print 'TODO:', name
     114            print '     ', description
     115}}}
     116
     117This class {{{implements}}} the {{{ITodoObserver}}} interface declared above, and simply prints every new to-do item to the console. By declaring to implement the interface, it transparently registers itself as an extension of the {{{TodoList}}} class.
     118
     119''Note that you don't actually derive the component from the interface it implements. That is because conformance to an interface is orthogonal to inheritance; and because Python doesn't have static typing, there's no need to explicitly mark the component as implementing an interface.''
     120
     121You can specify multiple extension point interfaces to extend with the {{{implements}}} method by simply passing them as additional arguments.
     122
     123== Putting it together ==
     124
     125Now that we've declared both a component exposing an extension point, and another component extending that extension point, let's use the to-do list example to see what happens:
     126
     127{{{
     128    comp_mgr = ComponentManager()
     129    todo_list = TodoList(comp_mgr)
     130
     131    todo_list.add('Make coffee',
     132                  'Really need to make some coffee')
     133    todo_list.add('Bug triage',
     134                  'Double-check that all known issues were addressed')
     135}}}
     136
     137Running this script will produce the following output:
     138
     139{{{
     140    TODO: Make coffee
     141          Really need to make some coffee
     142    TODO: Bug triage
     143          Double-check that all known issues were addressed
     144}}}
     145
     146This output obviously comes from the {{{TodoPrinter}}}. Note however that the code snippet above doesn't even mention that class. All that is needed to have it participating in the action is to declare the class. ''(That implies that an extending class needs to be imported by a python script to be registered. The aspect of loading components is however separate from the extension mechanism itself.)''