
Component Models
The LOOP project extends the Lua programming language to support popular object-oriented programming features combined with adaptive and reflection mechanisms. Additionally, it also supports other programming paradigms related to the object concept like component-based development. Similar to the class models, the component model also provides dynamic features like reflection support.
General Model
Similarly to the class model, the component model is also provided by a set of packages that present different features. Basically, the component model is implemented by two kinds of packages: component and port packages. A component package provides means to define and create components with different features and the port packages provides different implementations of ports used to link components together. In the examples below, we assume that a component package is loaded in variable component
and a port package is loaded in variable port
. The packages provided by LOOP that implement the model described in this section are listed below.
- Component packages:
-
loop.component.base
loop.component.wrapped
loop.component.contained
loop.component.dynamic
- Port packages:
-
loop.component.base
loop.component.intercepted
Ports
The LOOP component model defines only two kinds of ports, called facets and receptacles. Each port is identified by a unique name on the component. Component systems are assembled connecting facets to receptacles.
A facet is a port that provides some functionality by means of an interface, i.e. values and methods. Facet represents the functionalities provided by the component. In LOOP, a facet is realized by an object that provides the values and methods of the facet. A component can have multiple facets and even facets providing the same service with different features, e.g. performance.
On the other hand, a receptacle is a port that requires some functionality through an interface. Receptacles defines the explicit dependencies of the component. A receptacle is realized as a table field that holds a reference to the object that provides the required functionality, i.e. the object connected to the receptacle. LOOP also defines the concept of multiple receptacles, i.e. receptacles that can hold multiple references simultaneously. Multiple receptacles are realized as a table containing the references to all objects connected to the receptacle. Multiple receptacles provides the following operations that may be used to access the objects connected.
receptacle:__bind(object [, id])
- Binds
bind
to the receptacle using the optional parameterid
as the connection identifier and returns the connection identifier used. receptacle:__unbind(id)
- Removes de connection identified by parameter
id
. receptacle:__hasany()
- Returns a non-nil value if there is any object connected to the receptacle.
receptacle:__get(id)
- Returns the object connected to the receptacle with identifier
id
. receptacle:__all()
- Returns an iterator that can be used in a
for
statement to iterate over all objects connected to the receptacle with the following construction
for conn_id, object in receptacle:__all() do ... end
There are three different types of multiple receptacles, as described below.
ListReceptacle
- Accepts connections and automatically generates identifiers, therefore it ignores the identifier provided to
__bind
method. HashReceptacle
- Accepts connections with application-defined identifiers, therefore it is necessary to provide an identifier to the
__bind
method. SetReceptacle
- Accepts only one connection for each object and uses the object as the connection identifier, therefore it also ignores the identifier provided to
__bind
method.
Templates
A component is a computational element that provides a set of ports that can be connected to other components or objects in order to assemble a computational system. The set of ports provided by a component is defined by a template. A component template is realized as an object that maps port names to a specific port type. The code below illustrates the definition of a component template.
local DispatcherTemplate = component.Template{ requester = port.Facet, scheduler = port.Receptacle, objectmap = port.HashReceptacle, }
Component templates are responsible to create all the infrastructure necessary for the execution of a component implementation.
Factories
Once a component template is defined we can create factories of components that follow that template. For example, consider the following class that is used as the constructor of implementations of components of the template defined above.
-- implementation Requester = oo.class{ context = false } function Requester:push(request) local object = self.context.objectmap[request.object_key] if object then local operation = object[request.operation] or self.defaultops and self.defaultops[request.operation] if operation then self.context.scheduler:start(operation, object, unpack(request.params)) end end end -- factory DispatcherFactory = DispatcherTemplate{ requester = Requester } -- component local dispatcher = DispatcherFactory()
To create a factory, we use a component template as the factory constructor. Such constructor gets as parameter the factory definition, i.e. a table containing the constructor of each facet defined by the template. The constructor of each facet must be a callable object (e.g. function or class) that returns a new implementation for the facet of each new component created by the factory. In the example above, we create a factory of components that follow the template DispatcherTemplate
and each facet requester
is an instance of class Requester
.
Alternatively, a factory may also define a constructor for the component itself aside from the constructor of its facets. The component constructor is defined by index 1 or using the special name __component
in the factory definition. If a constructor is provided for the component then all facets that do not have a specific constructor defined are implemented by the implementation returned by the component constructor. For example, we can create a factory of dispatcher components similar to the one described above using the following code:
-- factory with component implementation that also implement all facets OtherDispatcherFactory = DispatcherTemplate{ Requester }
When a component is created, the component implementation's constructor is executed first if it is provided. This constructor receives as parameter all the values passed to the factory. After that, the constructor of each facet is executed. The facet constructors receive as parameter the field of the component implementation that matches the name of the facet plus a reference to this component implementation. Therefore, the component implementation may provide an initialization object to the provided the facet constructor. For example, the following code will create a component which the requester
facet has the field defaultops
defined.
-- component local dispatcher = DispatcherFactory{ requester = { -- object passed to class 'Requester' defaultops = { getid = function(self) return self.id end, } } }A similar approach may be used to define initial values for the receptacles, like in the example below.
-- component local dispatcher = DispatcherFactory{ objectmap = _G, -- allow disptach to global objects. }
Implementation
A single component instance may be made of different objects that are responsible to implement different facets. In addition to that, the component is also made of its receptacles that contains references to its dependencies. All this information if provided to the component by the context object. The context object is a table that maps the name of each port to its implementation, in case of facets, or to the objects connected in case of receptacles. Additionally, the context object also contains the factory that created the object and the component implementation at indexes __factory
and __component
, respectively.
The context object is provided to the component's implementation and each facet implementation by field context
. If the implementation object provides the field context with a value different from nil
, then this field is changed to hold the context object, like in the example of the section above.
Alternatively, if the field context
is a function then this function is called as a method of the implementation object that receives the context object as a parameter. The invocation of the context
function also signals that all implementation objects are created, so it is useful if some part of the component needs to perform some initialization only when the whole component's implementation is created. Since the order in which the implementation objects are created is not defined, the context
method may also be used to initialize some state of the object that is dependable of the others component's implementation objects, like in the example below.
-- facet 'requester' implementation Requester = oo.class() function Requester:context(context) self.context = context -- check if the 'defaultops' are defined in the component's implementation self.defaultops = self.defaultops or context.__component.defaultops end
Assembly
Once components are created they shall be assembled by connection of their ports. Connections are typically established by setting a facet of a component to the receptacle of another. However, in case of multiple receptacles, this is done by operation __bind
provided by this kind receptacles. The following example is extracted from the basic kernel architecture of the OiL ORB.
-- CLIENT SIDE ClientBroker = component.Template{ broker = port.Facet, proxies = port.Receptacle, references = port.Receptacle, } ObjectProxies = component.Template{ proxies = port.Facet, invoker = port.Receptacle, } OperationInvoker = component.Template{ invoker = port.Facet, requester = port.Receptacle, } -- SERVER SIDE ServerBroker = component.Template{ broker = port.Facet, objects = port.Receptacle, acceptor = port.Receptacle, references = port.Receptacle, } RequestDispatcher = component.Template{ objects = port.Facet, dispatcher = port.Facet, } RequestReceiver = component.Template{ acceptor = port.Facet, dispatcher = port.Receptacle, listener = port.Receptacle, } function assemble(components) setfenv(1, components) -- to avoid the 'components.' prefix. -- Client side OperationInvoker.requester = OperationRequester.requests ObjectProxies.invoker = OperationInvoker.invoker ClientBroker.proxies = ObjectProxies.proxies ClientBroker.references = ObjectReferrer.references -- Server side RequestReceiver.listener = RequestListener.listener RequestReceiver.dispatcher = RequestDispatcher.dispatcher ServerBroker.objects = RequestDispatcher.objects ServerBroker.acceptor = RequestReceiver.acceptor ServerBroker.references = ObjectReferrer.references end
Introspection
All LOOP component packages provide some introspection functions that are used to retrieve information about the structure of the components like the type of their ports or their implementations, like is listed below.
factoryof(component)
- Returns the factory object that was used to create the component
component
. templateof(component|factory)
- Returns the template of a component or the template used by a component factory.
ports(template)
- Returns an iterator for all ports defined by a component template. The iteration variables get the name of the current port and the kind of the port that is one of the field of the port package like
port.Facet
orport.HashReceptacle
. segmentof(component, portname)
- Returns the implementation of the port with name
portname
of componentcomponent
.
Component Packages
The component package that implements the standard model is loop.component.contained
. This package makes no assumption about the component's implementation objects, therefore these objects may be userdata or even non-indexable values like functions or threads. This is because the template provided by this package creates additional objects to represent the component's external interface, which is provided to clients to access the component ports, and also the context object, which is the component's internal interface. The figure bellow illustrates the schema of components created with templates of package loop.component.contained
.

Alternatively, LOOP also provides other component packages that provide templates that avoid creation of additional objects for each component like loop.component.wrapped
that creates only one additional object to represent the component external interface, but uses the component's implementation itself as the context object and therefore stores all ports values (facet implementation and receptacle connections) and the factory in the component's implementation object. This results that the component's implementation object can access all ports using the self
parameter and avoid an additional table index to get the context object. The only advantage of providing an additional object for the external interface of the component is to be able to provide an alternative external view like is used by ports with support for interception like is described in section Interception.
The component package loop.component.base
provides the lightest template implementation. Such templates use the component's object implementation both as the external and internal interface of the component, and therefore are very similar to ordinary objects, however they provide all the features described in the first section.
Port Interception
Package loop.component.intercepted
is a port package that provides implementation of component ports with support for interception. When a component template is created with ports from this package, interceptors may be registered in the component to be executed whenever an access is performed on its ports.
Interceptors may be defined for each component, or for all components created from a given factory, or even for all components of a given template. The interceptors are registered by the use of the following function provided by the loop.component.intercepted
package.
intercept(component|factory|template, portname, event [, interceptor])
- Sets
interceptor
as the interceptor to be invoked when eventevent
is triggered in port with nameportname
of the component or components defined by the first parameter. If theinterceptor
is omitted or isnil
then the interceptor registered for that event is removed. The possible values forevent
are:"method"
: a port's method is invoked."call"
: the port is invoked like a function."index"
: a port's field is indexed (this event always precede the"method"
event)."newindex"
: a port's field is set.
WARNING: There is a current limitation that intercepted ports cannot provide fields that contains function values. Such fields can only be methods, i.e. functions that are invoked immediately after they are indexed and take the self
parameter.
Interceptors
Interceptors are ordinary objects that may provide methods before
and after
. The semantics provided by these methods (the self
parameter is implict) are described below.
before(request, ...)
- Receives the
request
table that contains the following fields:context
: context object of the component.port
: name of the port being intercepted.object
: implementation object of the facet or object connected to the receptacle.event
: kind of the event that triggered the interception ("method"
,"call"
,"index"
, or"newindex"
).method
: ifevent
is"method"
then this field is provided with the function that implements the method being invoked.field
: ifevent
is"index"
or"newindex"
then this field is provided with the name of the field being accessed.
request
table this method also receive additional parameters that differ depending on the event that triggered the interception. In case of"method"
and"call"
these parameters are the parameters of the call, including theself
parameter in case of method invocations. In case of"index"
these parameters are only the name of the indexed field. In case of"newindex"
these parameters are the name of the indexed field and the new value being set to it.
This method must always return the values that must be used as the parameters of the actual event being intercepted. Therefore, to allow that the event be performed with unchanged parameters the method must return the additional parameters provided (i.e. the expression...
)
Finally, this method may cancel the event so it is not delegated to the component implementation. This is done by definition of fieldcancel
of tablerequest
provided as the first parameter of the method. In this case, the values returned by this method are used as the return values passed to the client of the event intercepted. after(request, ...)
- Receives the same
request
table that was provided to thebefore
method for the this triggered event. Therefore, if some value is stored in therequest
table in the execution of thebefore
method, such value is available in this table as well. The additional parameters are the return values of the triggered event. In case of"method"
and"call"
these parameters are the return values of the call. In case of"index"
these parameters are only the value of the indexed field. In case of"newindex"
no additional parameters are provided.
Similar to thebefore
method, this method must always return the values that must be used as the return values of the actual event being intercepted. Therefore, to allow that the event be performed with unchanged return values the method must return the additional parameters provided (i.e. the expression...
)
Example
ProfilerInterceptor = {} function ProfilerInterceptor:before(request, ...) if request.event == "index" then local field = ... self.lastindexed = field elseif request.event == "method" then -- should work because method index and invocation are atomic request.name = self.lastindexed request.start = os.time() end return ... end function ProfilerInterceptor:after(request, ...) if request.event == "method" then print(string.format("operation %s.%s(...) spent at least %d seconds.", request.port, request.name, os.difftime(os.time(), request.start) )) end return ... end local component = require "loop.component.base" local port = require "loop.component.intercepted" local MyTemplate = component.Template{ myfacet = port.Facet, myreceptacle = port.Receptacle, } port.intercept(MyTemplate, "myfacet", "index", ProfilerInterceptor) port.intercept(MyTemplate, "myfacet", "method", ProfilerInterceptor) port.intercept(MyTemplate, "myreceptacle", "index", ProfilerInterceptor) port.intercept(MyTemplate, "myreceptacle", "method", ProfilerInterceptor)
Dynamic Changes
Package loop.component.dynamic
is a component package that provides templates that can be changed dynamically and reflect such changes on all its instances. This package provides operations to add and remove ports of all components of a given template or factory or also from a single instance. When a new facet is added, it is possible to define the implementation of that facet that will be instantiated for each component instance on demand following the same implementation protocol used for ordinary facet implementations. The functions provided by the loop.component.dynamic
package are described below.
Functions
addport(component|factory|template, portname, portkind [, constructor])
- Defines a dynamic port with name
portname
of the kind specified by parameterportkind
that may be any port kind provided by packagesloop.component.base
orloop.component.intercepted
. The parameterconstructor
is used in addition of facets to define the constructor of their implementation objects. removeport(component|factory|template, portname)
- Removes a dynamic port that was added by function
addport
described above. It is not possible to remove ports that were not added dynamically.
Example
local component = require "loop.component.dynamic" local port = require "loop.component.base" local iport = require "loop.component.intercepted" local MyTemplate = component.Template() component.addport(MyTemplate, "inspector", port.Facet, oo.class{ context = function(self, context) self.context = context end, getfieldof = function(self, port, field) self.context[port][field] end, })
Copyright (C) 2004-2008 Tecgraf, PUC-RioThis project is currently being maintained by Tecgraf at PUC-Rio.