Butterfly Container Script - Design Considerations
Jakob Jenkov |
As configuration mechanism I chose a simple script language I decided to call Butterfly Container Script. I will only explain the details of the language relevant to this text. If you are interested you can read more about it here: Butterfly Documentation.
The Script Language
The language consists of simple factory definitions. Here is an example:
bean = * com.jenkov.MyObject();
This script defines a factory called "bean" which produces new instances (marked by the *)
of the class com.jenkov.MyObject
. This is equivalent to this Spring configuration:
<bean id="bean" class="com.jenkov.MyObject" singleton="false" />
Already here the Butterfly Container Script is more dense than the corresponding Spring XML configuration, though only a little. The difference becomes more evident when you configure a method to have a few objects injected. Here is an example:
bean = * com.jenkov.MyObject("value") .setSecondValue("value2") .setThirdValue("value3");
Notice how it is possible to chain method calls even if the methods
return void
. This is a conscious design decision in the
Butterfly Container Script, to make configurations shorter to write.
Here is the corresponding Spring XML configuration:
<bean id="bean" class="com.jenkov.MyObject" singleton="false"> <constructor-arg type="java.lang.String" value="value"/> <property name="secondValue" value="value2"/> <property name="thirdValue" value="value2"/> </bean>
Already here you can see how much noise the Spring XML configurations contain. The Butterfly Container Script is both shorter and looks a lot more like Java. From here on, Spring XML only gets more and more unreadable.
Full Freedom in Life Cycle Phases
And now a few things you can't even do with Spring's XML configuration, though I see absolutely no reason why not:
beanCounter = 1 com.jenkov.BeanCounter(); bean = * com.jenkov.MyObject(); config{ beanCounter.increment(); }
Now what is going on here? First, a "beanCounter" factory is defined
as a singleton (marked by the 1). Second, a "bean" factory
is defined, with a configuration phase attached. The configuration
phase is executed after the object is instantiated. But lo and behold!
The config phase doesn't call any methods on the MyObject
instance the factory returns. Rather, it calls the increment()
method of the "beanCounter" singleton!!
This little trick is totally impossible with Spring XML or Pico. You may not use this feature so often, but when you do need it, it comes in quite handy.
Input Parameters
And here is something else that isn't possible with neither Spring XML, Pico or Guice:
bean = * com.jenkov.MyObject($0); bean1 = * bean("value1"); bean2 = * bean("value2"); bean3 = * bean("value3").setSecondValue("blabla");
Now what is going on here? First a "bean" factory is defined as a new instance factory. The $0 marks an input parameter passed to the factory, either from other factories, or from outside the dependency injection container.
Second, three factories are defined as calls to the "bean" factory,
with different input parameters. These input parameters will be injected
into the MyObject
constructor when the MyObject
instance is created in the "bean" factory. Notice how the "bean3"
factory even chains a call on the MyObject
instance
returned by the "bean" factory.
Using input parameters it is possible to create factories that are merely used as templates for other factories. Spring-lovers will claim that this is possible in Spring XML too, by letting one bean-definition extend another. While this gets you some of the way, you still cannot change what is injected into the constructor in the extended bean definition (unless I am totally mistaken).
Collections
Another area in which I had to make a decision was in the support for configuration of lists and maps. Here is now a List and a Map configuration would have looked without special support:
myList = * java.util.ArrayList(); config{ $myList.add("value1"); $myList.add("value2"); } myMap = * java.util.HashMap(); config{ $myMap.put("key1", "value2"); $myMap.put("key2", "value2"); }
With the builtin array, List and Map support, here is how it looks now:
myList = * ["value1", "value2"]; myMap = * <"key1" : "value1", "key2" : "value2">;
Much simpler, right? You can of course reference other factories
from inside List and Map definitions. For arrays you use the List
formatting. The parser will figure out whether to use an array
or List depending on what parameter you inject it into. If not
injected, it defaults to an ArrayList
.
In contrast, look at all the nasty XML code needed in Spring:
<map> <entry> <key><value>key1</value></key> <value>value1</value> </entry> <entry> <key><value>key2</value></key> <value>value2</value> </entry> </map>
Shift into Java When You Need to
Butterfly Container Script is designed to be shorter than the equivalent Java code. However, this brevity comes at a cost. Sometimes it is just easier to do more advanced object configuration from plain Java code. Butterfly Container Script is designed to allow you to do that.
Imagine you have a static method you would like to call from your Butterfly Container Script. Here is how you do it:
myBean = * com.jenkov.MyBean.staticFactoryMethod();
Now you can put whatever code into the static method staticFactoryMethod
you need to run.
You can also use the static method as a function in your script. Imagine you
have a static method that takes a date in string format, and returns it as
a java.util.Date
object. Here is an example of using that static
method as function:
toDate = * com.jenkov.DateUtil.toDate($0); timer = * com.jenkov.Timer(toDate("2008-24-12"));
Notice how the timer
factory uses the toDate
factory as a function to convert the date in string format to a
java.util.Date
object for injection into the Timer
constructor.
You can also use instance methods as functions, like this:
dateFormat = 1 java.text.SimpleDateFormat("yyyy-MM-dd"); toDate = * dateFormat.parse($0); timer = * com.jenkov.Timer(toDate("2008-24-12"));
This configuration creates a singleton factory called dateFormat
of the type
java.text.SimpleDateFormat
. Then the configuration defines a toDate
factory which calls the parse()
method of the singleton SimpleDateFormat
instance. The toDate
function is used in the timer
factory, exactly
like the in the previous configuration example.
As you can see, it is easy to shift into Java when you need to, and still weave it into the container. You can even plug real non-reflection, non-annotation Java factories into the container, and reference them from inside your scripts. This is outside the scope of this article though. You can read how to do that in the Butterfly Documentation.
Summary
There are several other, minor features that Butterfly Container Script has, that Spring XML lacks, but explaining them all is outside the scope of this text. I just wanted to underline the most important design goals of this script language:
- Dense
- Java-like
- Very Flexible
- Very Extensible
Tweet | |
Jakob Jenkov |