felix-dev mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From "Pierre De Rop (JIRA)" <j...@apache.org>
Subject [jira] [Commented] (FELIX-4689) Create a more fluent syntax for the dependency manager builder
Date Thu, 12 Nov 2015 23:04:11 GMT

    [ https://issues.apache.org/jira/browse/FELIX-4689?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=15003155#comment-15003155
] 

Pierre De Rop commented on FELIX-4689:
--------------------------------------

So, I have committed in my sandbox an experimental (it is just an attempt) prototype for a
java8 builder on top of DependencyManager API. All is committed in http://svn.apache.org/repos/asf/felix/sandbox/pderop/dependencymanager.builder.java/

The work is not finished, I will get back to it later, after FELIX-4955 is done.

Basically, this prototype is an attempt to implement your original idea, but in a separate
module
and also using some java8-isms (lambda expressions, type-safe method references, and other
java8-isms).

This builder is implemented outside the code of DependencyManager, in the form of a separate
module
(I called it org.apache.felix.dependencymanager.java).

In this mail, you will find the following parts:

- general comments on the prototype
- technical solutions used to manage method reference for DM callbacks
- prototype presentation
- use cases ranging from simple scenarios to more advances use cases (adapters, aspects)

General comments on the prototype:
============================

Before describing the prototype, I would like to say that on the one hand, I'm satisfied by
this
prototype because you can define nice activators using compact and (relatively) type-safe
lambda expression, method refs, new
java8 stuff, etc ... like in the following example:

{code}
public class Activator extends DependencyActivatorBase {
    @Override
    public void init() throws Exception {
        // Create a SpellChecker component with a LogService dependency
        component(comp -> comp
            .factory(() -> SpellCheckerImpl::new)
            .provides(SpellChecker.class)
            .properties(COMMAND_SCOPE, "dictionary", COMMAND_FUNCTION, new String[] {"spellcheck"})
            .withService(LogService.class, srv -> srv.onAdd(SpellCheckerImpl:bind)));
    }
}
{code}

However, on the other hand, if you have time to look into the implementation, I'm afraid that
you
will find it probably complex, and not easy to read. This is because dealing with method references
was actually challenging, and it was not easy to define all possible component and dependency
callbacks using functional interfaces.

The implementation was also hard to do, because the prototype is extensively based on generics
for the
support of automatic type inference. Maybe I excessively used generics. Initially I chose
to use
generics for the sake of strictness, but admittedly, coding with generics sometimes leads
to java
code which is difficult to read, but I think that this is the cost to pay for a type-safe
builder
API. However, I may rework later the code in order to possibly remove some generics where
it is
actually not really necessary.

I also faced an issue when trying to retrieve the actual type of generic parameters. Indeed,
Java
does not provide an API that allows to get actual type of generic parameters declared on classes,
interfaces, or lambda expressions. So, to preserve my soul, I used a lightweight and simple
tool
called "typetools" (https://github.com/jhalterman/typetools, a nice tool made by Jonathan
Halterman,
with Apache 2 License) which allows to easily get actual types of generic parameters (by looking
up
in java byte code).

Finally, the prototype requires latest Eclipse Mars, because Luna suffers from many java8
bugs,
especially regarding method references.

Technical solutions used to manage method reference callbacks:
=================================================

Before describing the new API, I'm now giving some simple use cases which will help to understand
the implementation of the prototype regarding method references management:

use case 1: Define a dependency callback on an already instantiated object, with a reference
to a
  bind method on that object:

{code}
class MyServiceImpl {
   void bind(LogService log) {}
}

MyServiceImpl impl = new MyServicImpl();
{code}

So, to define a method reference on the bind method, this is simply done using a
java.util.function.Consumer<LogService> method reference:

{code}
Consumer<LogService> callback = impl::bind;
{code}

and later, when we want to inject the service, we simply call the consumer like this:

{code}
callback.accept(logService);
{code}

so far so good.

Now something a bit more tricky:

use case 2: Define a method reference on a class method without the instance.

This is a more complex use case. Here we want to define a method reference on a class method,
but we
don"t have yet the instance. We'll instantiate the implementation class later, once all dependencies
are available, and at this point, we'll then inject the dependencies using the instance we
have just
created.

So, to define such a method reference on an object instance that we don't have yet, we need
to use
something like a BiConsumer functional interface.

Example:

{code}
class MyComponent {
   void bind(LogService log) {}
}
{code}

Now, to define a method reference on the bind method (but without a MyComponent instance),
we can do
this:

{code}
BiConsumer<MyComponent, LogService> callback = MyComponent::bind;
{code}

This declaration is similar to:

{code}
BiConsumer<Test, LogService> callback = (myComponent, param) -> myComponent.bind(param);
{code}

So, when we'll instantiate later MyComponent class, we'll then simply call the BiConsumer.accept
method with the MyComponent instance, as well as the logService that we want to inject to
the
myComponent.bind method:

{code}
MyComponent comp = new MyComponent();
...
// now inject the logService in the comp.bind method:
callback.accept(comp, logService);
{code}

One last remark about the implementation; it concerns the ConfigurationDependencyBuilder:

When you specify a method reference on a component instance "updated" callback, a proxy callback
object is used by the ConfigurationDependencyBuilder; and when the proxy object is called
in its
"updated" callback, then the proxy calls "component.getInstances()" method in order to call
the
method reference on the component instances.

But with the current DM API, we have a problem here, because when you add a callback
instance on a configuration dependency, then the confDependency.needsInstance() returns false,
and
the component instances won't be available at the time the proxy.updated method is invoked.
The component instances is not instantiated when using instance callbacks, mainly for the
support of
factories that needs to get configuration before being able to instantiate the component
implementation from the factory.create method (see the compositefactory in the sample code,
in order
to fully understand all this).

So, in order to work around, I used a simple solution: I exposed the "instantiateComponent"
method
in the ComponentContext interface, in order to let the ConfigurationBuilder force the instantiation
of the component instances before invoking the component.getInstances() method.

Prototype presentation
======================

Mainly, the builder design pattern is reused, and is inspired from your initial work (using
a builder
API in order to create real DependencyManager objects).

The new things concerns java8. Essentially, you can now define method references for dependency
injections, as well as lambdas when initializing components and dependencies.

The prototype contains three bundles:

org.apache.felix.dependencymanager.builder.java: this bundle contains the builder API + the
implementation.
org.apache.felix.dependencymanager.builder.java.itest: integration tests. (to be finished)
org.apache.felix.dependencymanager.builder.java.samples: some of the original samples available
from DependencyManager, but adapted to the new builder API

When using the builder API, you have to write an activator, as before, but this time you extend
the
"org.apache.felix.dm.builder.java.DependencyActivatorBase" class. This class provides an init()
method that you have to extend, and also some functions that allows to create components,
adapters, as well as some static functions that allow to create components outside of the
Activator.

The components are auto-added to the DependencyManager object that is created in the
DependencyActivatorBase class, however you can create components and use the "autoAdd(false)"
method
in the ComponentBuilder. This will make sure the component is not automatically added to the
dm
object (sometimes, this is useful). 

let's take a look at a simple example: here we have one component that depends on a log service
injected by reflection (autoconfig):

{code}
 public class Activator extends DependencyActivatorBase {
    @Override
    public void init() throws Exception {
       component(comp -> comp
            .impl(ServiceImpl.class)
            .withService(LogService.class));
    }
 }
{code}

The ServiceImpl class will be injected with the LogService on any field having a LogService
type.
Notice that the component function takes as parameter a lambda expression. The corresponding
functional interface is a Consumer<ComponentBuilder> function that accepts a ComponentBuilder
(comp -> comp). Now, using that builder, you can call the builder methods
(comp.impl(..).withService(..) 

If you want to configure a service dependency with more informations (like callbacks), you
can also
use another version of the "withService" method which also takes a lambda that accepts a
ServiceDependencyBuilder object: 

 {code}
public class Activator extends DependencyActivatorBase {
     @Override
    public void init() throws Exception {
        component(comp -> comp
            .impl(ServiceImpl.class)
            .withService(LogService.class, srv -> srv.filter("(foo=bar)").onAdd(this::bind).required(false)));
    }
 }
 {code}

The withService you see above takes the service dependency type (LogService.class), as well
as a
lambda that is a Consumer<ServiceDependencyBuilder>. And the lambda can then use the
"srv" builder
and call proper ServiceDependencyBuilder methods: 

 {code}
    withService(LogService.class, srv -> srv.filter("(foo=bar)").onAdd(this::bind).required(false)
 {code}

Notice that, unlike in the original DependencyManager API, dependencies are required by default.

Currently, the prototype supports the following components:

Component
Aspects
Adapters
Factory Configuration Adapters

and the following dependencies are supported:

ServiceDependency
ConfigurationDependency

So, the following things still need to be done:

- add support for ResourceDependency
- add support for BundleDependency
- add support for ResourceAdapter
- add support for BundleAdapter

All the remaining things to be done are described in the TODO file

Use cases ranging from simple scenarios to more advances use cases (adapters, aspects)
======================================================================================

1) simple component with a service consumer and a service provider:

In the samples, you will find a simple example with a ServiceConsumer, a ServiceProvider,
and
Configurator service:

org.apache.felix.dependencymanager.builder.java.samples/src/org/apache/felix/dependencymanager/samples/hello:

 {code}
public class Activator extends DependencyActivatorBase {
    void bind(ServiceProvider provider) {
	System.out.println("Activator.bind(" + provider + ")");
    }
	
    @Override
    public void init() throws Exception {
        component(comp -> comp
            .provides(ServiceProvider.class)
            .onStart(ServiceProviderImpl::activate)
            .properties("foo", "bar", "gabu", "zo") // foo=bar, gabu=zo
            .impl(ServiceProviderImpl.class)
            .withService(LogService.class, srv -> srv.onAdd(ServiceProviderImpl::bind)));

        component(comp -> comp
            .impl(ServiceConsumer.class)
            .withService(LogService.class)
            .withService(ServiceProvider.class, srv -> srv.filter("(foo=bar)").onAdd(this::bind))
 
            .withConfiguration(conf -> conf.pid(ServiceConsumer.class).onUpdate(ServiceConsumer::updated)));
 
        
        component(comp -> comp.impl(Configurator.class).withService(ConfigurationAdmin.class));
    }
}
 {code}

The first component "ServiceProvider" has a special start method (activate), so we are using
the
onStart(ServiceProviderImpl::activate) method. The properties can now be provided using the
one
liner properties(...) method, which takes an even number of parameters, each pair of parameters
consisting of a key-value params: 

   properties("foo", "bar", "gabu", "zo") // foo=bar, gabu=zo

the provider depends on a LogService (required), which will be injected in the ServiceProviderImpl::bind
method.
The second component is the ServiceConsumer that depends on the ServiceProvider, and also
on a configuration.
The last component is a Configurator component that populates the configuration into ConfigAdmin
for the ServiceConsumer component.

2) A component which is created using a factory object.

(org.apache.felix.dependencymanager.builder.java.samples/src/org/apache/felix/dependencymanager/samples/factory/)

 {code}
public class Activator extends DependencyActivatorBase {    
    @Override
    public void init() throws Exception {
        component(comp -> comp
            .provides(Provider.class)
            .factory(ProviderFactory::new, ProviderFactory::create)       
            .withService(LogService.class, srv -> srv.required().onAdd(ProviderImpl::set))
            .onStart(ProviderImpl::start));                      
    }
}
 {code}

Here, the Provider class is instantiated using the ProviderFactory that is instantiated using
"ProviderFactory::new" constructor reference, and the ProviderFactory::create method. 

3) Object composition:

The example from
org.apache.felix.dependencymanager.builder.java.samples/src/org/apache/felix/dependencymanager/samples/compositefactory/
contains a ProviderImpl component that is instantiated from a CompositionManager object, and
the
configuration is injected in the CompositionManager.

 {code}
/**
 * Defines a factory that also returns a composition.
 * The LogService in only injected to the ProviderImpl and the ProviderParticipant1.
 */
public class Activator extends DependencyActivatorBase {
    @Override
    public void init() throws Exception {
        CompositionManager compositionMngr = new CompositionManager();
        
        component(comp -> comp
            .factory(compositionMngr::create, compositionMngr::getComposition)
            .withService(LogService.class, srv -> srv.onAdd(ProviderImpl::bind).onAdd(ProviderParticipant1::bind))
            .withConfiguration(conf -> conf.pid(CompositionManager.class).onUpdate(compositionMngr::updated)));
                
        component(comp -> comp
            .impl(Configurator.class).withService(ConfigurationAdmin.class));
    }
}
 {code}

4) Adapter example

org.apache.felix.dependencymanager.builder.java.samples/src/org/apache/felix/dependencymanager/samples/device/

This is an example showing a Dependency Manager "Adapter" in action. Two kinds of services
are
registered in the registry: some Device, and some DeviceParameter services. For each Device
(having
a given id), there is also a corresponding "DeviceParameter" service, having the same id.

Then a "DeviceAccessImpl" adapter service is defined: it is used to "adapt" the "Device" service
to
a "DeviceAccess" service, which provides the union of each pair of Device/DeviceParameter
having the
same device.id . The adapter also dynamically propagate the service properties of the adapted
Device
service.

Here is the activator 

 {code}
public class Activator extends DependencyActivatorBase {
    @Override
    public void init() throws Exception { 
        createDeviceAndParameter(1);
        createDeviceAndParameter(2);

        // Adapts a Device service to a DeviceAccess service
        adapter(Device.class, comp -> comp.provides(DeviceAccess.class).impl(DeviceAccessImpl.class));
            
        component(comp -> comp
            .impl(DeviceAccessConsumer.class)
            .withService(LogService.class)
            .withService(DeviceAccess.class, srv -> srv.onAdd(DeviceAccessConsumer::add)));
      
    }
    
    private void createDeviceAndParameter(int id) {
        component(buicomplder -> buicomplder
            .provides(Device.class).properties("device.id", id)
            .factory(() -> new DeviceImpl(id))); // lazily create DeviceImpl
                       
        component(comp -> comp
            .provides(DeviceParameter.class).properties("device.id", id)
            .factory(() -> new DeviceParameterImpl(id))); // lazily create DeviceParameterImpl
    }
}
 {code}

This example is interesting because it uses an adapter and also a factory that takes a lazy
Supplier
lambda that is used when instantiating components.

also, the example shows how to add dynamic dependencies from component's init method. For
example,
when the DeviceAccessImpl component is initialized, it is passed the (real) DependencyManager
Component that is then modified in order to add a dynamic dependency:

 {code}
public class DeviceAccessImpl implements DeviceAccess {
    volatile Device device; // injected
    volatile DeviceParameter deviceParameter; // injected

    void init(Component c) {
        // Dynamically add an extra dependency on a DeviceParameter (using the builder API).
    	// Notice that we also add a "device.access.id" service property dynamically.
        component(c, builder -> builder
        	.properties("device.access.id", device.getDeviceId())
        	.withService(DeviceParameter.class, srv -> srv.filter("(device.id=" + device.getDeviceId()
+ ")")));
    }
}
 {code}



> Create a more fluent syntax for the dependency manager builder
> --------------------------------------------------------------
>
>                 Key: FELIX-4689
>                 URL: https://issues.apache.org/jira/browse/FELIX-4689
>             Project: Felix
>          Issue Type: Improvement
>          Components: Dependency Manager
>            Reporter: Christian Schneider
>         Attachments: FELIX-4689-1.patch
>
>
> I wonder if the DependencyManager API could be made a bit more fluent.
> Technically it already uses the fluent builder pattern
> but all the builder verbs still look a lot like traditional setters.
> I know what I propose is mostly syntactic sugar but I think the result
> looks more readable and crisp. See below for some ideas.
> There is the concern about auto adding the component() to manager as it would acrivate
the not fully configured component. We could perhaps overcome this by adding the component
to a list of pending components first and then moving them to the active components after
the init method.
> The camel DSL solves this similarly.
> This is from samples.dependonservice:
>     public void init(BundleContext context, DependencyManager manager)
> throws Exception {
>         manager.add(createComponent()
>             .setImplementation(DataGenerator.class)
>             .add(createServiceDependency()
>                 .setService(Store.class)
>                 .setRequired(true)
>             )
>             .add(createServiceDependency()
>                 .setService(LogService.class)
>                 .setRequired(false)
>             )
>         );
>     }
> Why not make it look like this:
>     public void init(BundleContext context, DependencyManager manager)
> throws Exception {
>         component()
>             .implementation(DataGenerator.class)
>             .add(serviceDependency(Store.class).required())
>             .add(serviceDependency(LogService.class))
>             );
>         );
>     }
> component() could create and add the component.
> Or for configuration:
>     public void init(BundleContext context, DependencyManager manager)
> throws Exception {
>         manager.add(createComponent()
>             .setImplementation(Task.class)
>             .add(createConfigurationDependency()
>                 .setPid("config.pid")
>                 // The following is optional and allows to display our
> configuration from webconsole
>                 .setHeading("Task Configuration")
>                 .setDescription("Configuration for the Task Service")
>                 .add(createPropertyMetaData()
>                      .setCardinality(0)
>                      .setType(String.class)
>                      .setHeading("Task Interval")
>                      .setDescription("Declare here the interval used to
> trigger the Task")
>                      .setDefaults(new String[] {"10"})
>                      .setId("interval"))));
>     }
> could be:
>     public void init(BundleContext context, DependencyManager manager)
> throws Exception {
>         component().implementation(Task.class)
>             .configuration("config.pid")
>                 .add(meta("Task Configuration)
>                     .description("Configuration for the Task Service")
>                     .add(property("interval")
>                             .cardinality(0)
>                             .type(String.class)
>                             .heading("Task Interval")
>                             .description("Declare here the interval used
> to trigger the Task")
>                             .default("10"))
>     }



--
This message was sent by Atlassian JIRA
(v6.3.4#6332)

Mime
View raw message