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 parameter id 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 or port.HashReceptacle.
segmentof(component, portname)
Returns the implementation of the port with name portname of component component.

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 event event is triggered in port with name portname of the component or components defined by the first parameter. If the interceptor is omitted or is nil then the interceptor registered for that event is removed. The possible values for event 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: if event is "method" then this field is provided with the function that implements the method being invoked.
  • field: if event is "index" or "newindex" then this field is provided with the name of the field being accessed.
In addition to the 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 the self 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 field cancel of table request 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 the before method for the this triggered event. Therefore, if some value is stored in the request table in the execution of the before 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 the before 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 parameter portkind that may be any port kind provided by packages loop.component.base or loop.component.intercepted. The parameter constructor 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.