What does Yan Container do?
- Dependency Injection. Business objects that are dependent on each other can be assembled using Yan Container.
- Yan Container is non-intrusive. Components do not need to implement any interface, extend any class. They can be Pojo's.
- There's no constraint on injection method. No requirement for a Java Bean setter, or a public constructor or any constraint on how you design or expose your business objects. You may use Yan Container with a static factory method, with an abstract factory, a constructor, a java bean, anything that the Java language supports.
- Lifecycle support is flexible. The default lifecycle is easy (very similar to that of Spring) but extensible and replaceable.
How do I use Yan Container?
- Business objects are designed with no explict dependency (no implementation/subclassing of any proprietary interface/class) and no implicit dependency (like conform to a coding convention such as Java Bean or constructor-based IOC) on Yan Container.
- Components are registered in the container using simple and declarative API that is similar to a Java hashtable.
- Parameters and properties can be customized at component level.
- Lifecycle is done by declaring the method name of "initializer", "starter", "stopper" and "disposer", very similar to the lifecycle support in Spring framework. Business objects don't need to implement any interface.
- More advanced lifecycle support can be implemented and plugged in easily.
How is Yan Container different than other IOC containers?
- Yan Conainer is non-intrusive.
- Business objects do not need to depend on any proprietary interface/class.
- Business objects do not need to conform to any coding convention. Not have to have public constructor; not have to be Java Bean.
- Yan Container is combinator-oriented, which enables unlimited flexibility. Real sophisticated component can be constructed by combining simple components in a declarative syntax.
- Simple and modular design makes it easy for extension.
Write two simple components
public class Foo {
private final String hi;
public Foo(String hi){
this.hi = hi;
}
public void greet(Object obj) {
System.out.println(hi + " " + obj);
}
}
public class Bar {
Foo foo;
public Bar(Foo foo) {
this.foo = foo;
}
public void greeting() {
foo.greet(this);
}
public String toString(){return "bar";}
}
Assemble components
Container yan = new DefaultContainer();
yan.registerValue("hello");
yan.registerConstructor(Foo.class);
yan.registerConstructor(Bar.class);
Instantiate and use component
Bar bar = (Bar) yan.getInstanceOfType(Bar.class); bar.greeting();
getInstanceOfType(Bar.class) will look at the constructor of Bar class and determine that it needs a Foo instance.
Container then constructs this Foo instance using the constructor of class Foo. Since the constructor of Foo requires a String as a parameter, container will search a component with type String, and that is resolved to the valule "hello".
Finally the value "hello" is passed into constructor of Foo, and the Foo instance is passed into the constructor of Bar.
In an IOC (Inversionn of Control) design, the Bar class does not bother finding itself a Foo but instead just wait for someone to provide it.
Introduce an interface for the dependency
Change the Foo class to implement a Greeter interface and change the Bar class to depend on Greeter instead.
public interface Greeter {
void greet(Object whom);
}
public class Foo implements Greeter {
private final String hi;
public Foo(String hi) {
this.hi = hi;
}
public void greet(Object whom) {
System.out.println(hi + " " + whom);
}
}
public class Bar {
Greeter greeter;
public Bar(Greeter greeter) {
this.greeter = greeter;
}
public void greeting() {
greeter.greet("bar");
}
}
Assemble and use components just as before:
Container yan = new DefaultContainer();
yan.registerValue("hello");
yan.registerConstructor(Foo.class);
yan.registerConstructor(Bar.class);
Bar bar = (Bar) yan.getInstanceOfType(Bar.class);
bar.greeting();
The Bar will be given a Foo, because Container understands that a Foo is a Greeter.
The Bar and the Foo no longer depend on each other, this is called the Dependency Inversion Principle since both components depend on the interface and no longer directly on each other.
Simple Lifecycle
Suppose the Foo object has to be initialized before being used by class Bar. This can be done with the default lifecycle support.
public class Foo {
private final String hi;
public Foo(String hi) {
this.hi = hi;
}
public void initialize() {
}
public void greet(Object whom) {
System.out.println(hi + " " + whom);
}
public void dispose() {
}
}
public class Bar {
Foo foo;
public Bar(Foo foo) {
this.foo = foo;
}
public void greeting() {
foo.greet(this);
}
public String toString(){
return "bar";
}
}
Now, assemble the components and set the lifecycle method.
Container yan = new DefaultContainer();
yan.registerValue("hello");
Component foo = Components.ctor(Foo.class);
DefaultLifecycleManager manager = new DefaultLifecycleManager();
Lifecycle lifecycle = manager.newLifecycle()
.initializer("initialize")
.disposer("dispose");
Component foo_with_lifecycle = manager.withLifecycle(foo, lifecycle);
yan.registerComponent(foo_with_lifecycle);
yan.registerConstructor(Bar.class);
- The Components.ctor() method creates a Component object that uses the public constructor in class Foo.
- The DefaultLifecycleManager object manages the default lifecycle for the component instances.
- manager.newLifecycle() creates a life cycle object so that we can populate the life-cycle methods.
- initializer() specifies a method name for intialization. Similar to the "init-method" in Spring.
- disposer() specifies a method name for disposing the object. Similar to the "destroy-method" in Spring.
- manager.withLifecycle(foo, lifecycle) creates a new Component object by binding the lifecycle to the foo component.
- registerComponent() registers any Component object to the container.
To run the initializer and the disposer:
final Bar bar = (Bar)yan.getInstanceOfType(Bar.class); manager.init(); bar.greeting(); manager.dispose();
- manager.init() invokes the Foo.initialize() for all the managed component instances in the creation order.
- manager.dispose() invokes the Foo.dispose() for all the managed component instances in the reverse order of creation.
- Since Bar is not registered with any lifecycle method, it is not touched.
Customize component parameter
Change the Bar class by adding a member "name".
public class Bar {
Foo foo;
private final String name;
public Bar(Foo foo, String name) {
this.foo = foo;
this.name = name;
}
public void greeting() {
foo.greet(name);
}
}
Now we can control the name of any Bar object.
However, if we assemble and run the components just as before, we will get "hello hello" rather than the expected "hello bar". This is because container resolves both the second parameter of Bar and the first parameter of Foo to the same String object: "hello".
We can register another value "bar" into the container by:
yan.registerValue("bar component", Components.value("bar"));
The string "bar component" is used as a key for the value bar. This is because the container is a hash table, two components with the same key cannot exist in the container at the same time. We have to give it a different key.
To use this new value, we need to register Foo and Bar slightly different:
Container yan = new DefaultContainer();
yan.registerValue("greeting", "hello");
yan.registerValue("bar component", "bar");
yan.registerComponent(Components.ctor(Foo.class).withArgument(0, Components.useKey("greeting")));
yan.registerComponent(Components.ctor(Bar.class).withArgument(1, Components.useKey("bar component")));
- Components.ctor(Bar.class) creates a component that will call the constructor of Bar.
- Components.useKey("greeting") acts as a forwarding point. It fowards all request to the component identified by key "greeting" in the same container.
- withArgument(0, Components.useKey("greeting")) decorates the previous component by customizing the first parameter with the value of the component identified by "greeting".
- withArgument(1, Components.useKey("bar component")) decorates the previous component by customizing the second parameter to use the value of the "bar component" component.
Thus, by explicitly informing the container which component to use for the otherwise ambiguous parameter, we can get the expected result: "hello bar".
(By the way, did you find that Components.useKey() is very similar to the "beanref" available in the Spring framework?)
Customize component parameter locally
In the previous example, two auxiliary components with distinct keys are registered into the container. The component parameter can then explicitly reference these components for customization.
This might not be desirable in certain cases because it also makes visible the two auxiliary components to other irrelevant components in the container. It doesn't look like a serious problem for our tiny example, but for bigger application, it tends to convolute things together. In fact, it is very similar to introducing global variables in a program, which is normally discouraged.
The solution is simple. For a component to be used as a parameter of another one, Yan does not require a component to be registered in a container at all.We can replace the Components.useKey() directly with Components.value().
Container yan = new DefaultContainer();
yan.registerComponent(Components.ctor(Foo.class).withArgument(0, Components.value("hello")));
yan.registerComponent(Components.ctor(Bar.class).withArgument(1, Components.value("bar")));
- Components.value("hello") creates a component that always returns value "hello".
- A global component of type String is no longer needed because each component expecting a String parameter is already configured with a String value.
In fact, if the only component we care is Bar, Foo does not need to be registered at all. We can make Foo local using withArgument().
Container yan = new DefaultContainer();
Component foo = Components.ctor(Foo.class)
.withArgument(0, Components.value("hello"));
yan.registerComponent(
Components.ctor(Bar.class)
.withArgument(0, foo)
.withArgument(1, Components.value("bar"))
);
Thus, only Bar is registered and made visible to other components in the container. Foo and the two string values are just local dependents. They are private to Foo and invisible to any other components.
Singleton Component
Although singleton pattern is considered harmful by some people, it is undoubtedly useful in real life when you simply want a _single_ object all the time. Call it "singleton" or not, this is sometimes a hard requirement that has to be fulfilled.
Yan allows any component to be made singleton so that it always returns the same instance..
Component foo = Components.ctor(Foo.class)
.withArgument(0, Components.value("hello"));
Component singleton_foo = foo.singleton();
- foo.singleton() returns a new Component that will use singleton pattern to create Foo object so that it is guaranteed to always return the same instance each time invoked.
Java Bean components
So far, we've seen components using public constructors. You may get an impression that Yan exclusively uses public constructor for dependency injection. No, Yan has no requirement about how the dependency should be injected into your business ojbect.
If you read on, you can see how Java Bean setters can be used (as in Spring Framework).
First, let's re-write the class Foo and Bar to follow Java Bean convention:
public class Foo {
private String hi;
public Foo() {
}
public void setHi(String h) {
this.hi = h;
}
public void greet(Object whom) {
System.out.println(hi + " " + whom);
}
}
public class Bar {
private Foo foo;
private String name;
public Bar() {
}
public void setFoo(Foo f) {
this.foo = f;
}
public void setName(String n) {
this.name = n;
}
public void greeting() {
foo.greet(name);
}
}
Now if we register and assemble components as before, we'll get a Foo object and a Bar object with all member variables set to null.
This is because container does not know that you want it to call Java Bean setters after the Foo and Bar objects are constructed.
In order to use Java Bean setters to inject dependency, we need to use the Components.bean() function to create a Component that uses Java Bean setters.
Container yan = new DefaultContainer();
yan.registerValue("hello");
yan.registerComponent(Components.bean(Foo.class));
yan.registerComponent(Components.bean(Bar.class));
- Components.bean(Foo.class) tells the container to instantiate instance of class Foo in a Java Bean convention. i.e. Call the default constructor and then call all java bean setters.
And still, we get "hello hello" out of this. Solution is very similar to what we have done, except we need to replace "withArgument" with "withProperty":
Container yan = new DefaultContainer();
Component foo = Components.bean(Foo.class)
.withProperty("hi", Components.value("hello"));
yan.registerComponent(Components.bean(Bar.class)
.withProperty("foo", foo)
.withProperty("name", Components.value("bar"))
);
- withProperty() customizes a property of a Java Bean component.
Java Bean with non-default constructor
Although Java Bean Specification requires you to always use a parameter-less constructor, Yan allows you to use a non-default constructor with parameters when you want to.
First of all, you may ask "why?". If Java Bean Specification requires so, why do I ever want to use a non-default constructor?
Well, the simple answer is: You may or you may not want to. Whether to conform to Java Bean Specification is your call, not Yan's.
Some thought we had is: the concept of Java Bean is borrowed into dependency injection. What dependency injection needs is not real Java bean, but just some standard setters.
If we re-write the Bar class as follows:
public class Bar {
private Foo foo;
private String name;
public Bar(Foo f) {
this.foo = f;
}
public void setFoo(Foo f) {
this.foo = f;
}
public void setName(String n) {
this.name = n;
}
public void greeting() {
foo.greet(name);
}
}
We've got a non-standard Java Bean with a parametered constructor and public setters.
To use such non-standard Java Bean, the syntax for the component is slightly different:
Container yan = new DefaultContainer();
Component foo = Components.bean(Foo.class)
.withProperty("hi", Components.value("hello"));
yan.registerComponent(foo);
yan.registerComponent(Components.bean(Components.ctor(Bar.class))
.withProperty("foo", foo)
.withProperty("name", Components.value("bar"))
);
- Components.bean(Components.ctor(Bar.class)) tells container to use the result from Components.ctor(Bar.class) and call the Java Bean setters against this object. And, Components.ctor(Bar.class) is not restricted to only default constructor. It uses any constructor present in the class.
- The constructor of Bar will use the Foo object registered in the container.
- The parameter of function Components.bean() can be any component. It is not limited to a Components.ctor(). This means, any component, including a simple value, constructor based component, java bean based component, method based component (we'll cover that soon), can be used to create the object that has the setters.
Selecting Java Bean setters
In the previous example, class Bar accepts a Foo object from both the constructor and the setFoo() method.
When component Bar is assembled, the Foo object will be injected twice by the constructor and the setter.
Although it did not cause us any trouble, sometimes we may want to tell the container not to call setFoo() because the foo object is already injected in constructor.
There are many ways in Yan to do it. The most straight-forward one is to explicitly specify the properties to inject.
yan.registerComponent(
Components.bean(Components.ctor(Bar.class), new String[]{"name"})
.withProperty("name", Components.value("bar"))
);
- Components.bean(some_bean, new String[]{"name"}) tells container that we are only interested in the property "name", please ignore other properties.
Optional Java Bean Property
If we did not use withProperty() to specify the value of property name in the previous example, an exception will be thrown indicating that the property name cannot be resolved.
Yan fails when any dependency cannot be satisfied.
This behavior can be customized though. For example, we may tell the container that: skip property "name" when it cannot be resolved.
This is done by calling optionalProperty() function.
yan.registerComponent(
Components.bean(Components.ctor(Bar.class), new String[]{"name"})
.optionalProperty("name")
);
- optionalProperty("name") tells the container that property "name" is optional. It can be skipped if a value of this property is not resolved.
Configure the default value for a property
We can further customize the defaulting policy by giving a default value.
For example, we may say: if property "name" is not found, please do not fail, do not skip it, but use value "bar" instead.
yan.registerComponent(
Components.bean(Components.ctor(Bar.class), new String[]{"name"})
.withDefaultProperty("name", Components.value("bar"))
);
- withDefaultProperty("name", Components.value("bar")) tells the container to use the value "bar" when the property "name" cannot be resolved.
Optional Parameter and Default Parameter value
Not just Java Bean properties, parameters of constructor or any other kind of component can be made optional and can be given a default value.
Suppose we did not register component foo into the container, the above code will fail to instantiate Bar because constructor of Bar requires a Foo object, which cannot be resolved.
We could make this parameter optional by saying:
yan.registerComponent(
Components.bean(Components.ctor(Bar.class).optionalParameter(0),
new String[]{"name"})
.withDefaultProperty("name", Components.value("bar"))
);
- optionalParameter(0) tells container that the first parameter is optional. If it cannot be resolved, please use whatever the default value to the best of your knowledge.
- By default, default value for a parameter is null.
This way, we can successfully instantiate a Bar object with its "foo" member set to null. Of course, it will still fail when greeting() is invoked.
To provide a different default value for a parameter, withDefaultArgument() can be used.
yan.registerComponent(
Components.bean(Components.ctor(Bar.class)
.withDefaultArgument(0, Components.bean(Foo.class)),
new String[]{"name"})
.withDefaultProperty("name", Components.value("bar"))
);
- withDefaultArgument(0, Components.bean(Foo.class)) tells container to instantiate class Foo as a Java Bean when the first argument of Bar cannot be resolved.
Explicitly ask for a defaut value
withDefaultProperty(), withDefaultArgument() both rely on whether the property/parameter can be resolved in a certain container. In addition, one can also explicitly ask for the default value.
In the example where we want to ignore the property "foo" for component Bar, we listed all the properties that we are interested and just left "foo" out. It was not a very straight-forward solution.
Using useDefault(), we can achieve the same result, and the code might be more readable to some:
yan.registerComponent(
Components.bean(Components.ctor(Bar.class)
.withDefaultArgument(0, Components.bean(Foo.class)),)
.withProperty("foo", Components.useDefault())
.withDefaultProperty("name", Components.value("bar"))
);
- Components.useDefault() is a component that does nothing but signals that a default action is required.
- withProperty("foo", Components.useDefault()) tells the container that for property "foo", please take the default action to the best of your knowledge. And that default action for a property happens to be "skip it". ;->
Factory based injection
So far, we've covered constructor-based and java bean based injections. Now we want to introduce a more flexible way of injection: factory.
First, what do we do most frequently in our every-day Java program? calling constructor? setting Java Bean properties? No, we invoke methods! Based on their high level semantics in the application domain, we call some methods "factory" if their responsibility is spelled as "creating products".
We believe it is a pity if an IOC container has no support for this most general form of object instantiation.
In Yan, using factory is as easy as using constructor or Java Bean.
Imagine now our business object design changes such that the constructor of Foo is hidden, and a static factory method is exposed to create Greeter object:
public interface Greeter {
void greet(Object whom);
}
public class Foo implements Greeter {
private final String hi;
private Foo(String hi) {
this.hi = hi;
}
public static Greeter instance(String hi) {
return new Foo(hi);
}
public void greet(Object whom) {
System.out.println(hi + " " + whom);
}
}
public class Bar {
Greeter greeter;
public Bar(Greeter greeter) {
this.greeter = greeter;
}
public void greeting() {
greeter.greet("bar");
}
}
No more. we cannot use constructor-based injection any more because the constructor is hidden.
But don't worry. You won't hear any arrogant interference like "No! This design is bad. It does not work with the container!". Yan is a polite container. It won't say "no" to any of your own business object design decision. It follows your rule, not the opposite. And here is how Yan work with this new design:
Container yan = new DefaultContainer();
yan.registerValue("hello");
yan.registerComponent(Components.static_method(Foo.class, "instance"));
yan.registerConstructor(Bar.class);
Bar bar = (Bar) yan.getInstanceOfType(Bar.class);
bar.greeting();
- Components.static_method(Foo.class, "instance") creates a Component that invokes a static method named "instance" in class Foo.
Yan also supports invoking an instance method to instantiate a component:
Components.method(obj, "method_name") can be used for this purpose. It invokes obj.method_name(. . .) to instantiate component. It is more often combined with dynamic component. We'll cover them in the advanced sections.
Instantiate a list
Suppose the class Bar is changed as:
public class Foo {
private String hi;
public Foo() {
}
public void setHi(String h) {
this.hi = h;
}
public void greet(Object whom) {
System.out.println(hi + " " + whom);
}
}
public class Bar {
private java.util.List foos;
private String name;
public Bar() {
}
public void setFoo(java.util.List f) {
this.foos = f;
}
public void setName(String n) {
this.name = n;
}
public void greeting() {
for (java.util.Iterator it = foos.iterator(); it.hasNext();) {
((Foo) it.next()).greet(name);
}
}
}
Now, to instantiate a Bar, we need to give it a list of Foo objects instead of just one.
Suppose we want to create two Foo objects, one will say "hello", one will say "goodbye". And we want to pass these two Foo objects in a list to Bar.
Function Components.list() can be used to do this job:
Component foo = Components.bean(Foo.class);
Component foos = Components.list(new Component[]{
foo.withProperty("hi", "hello"),
foo.withProeprty("hi", "goodbye")
});
Component bar = Components.bean(Bar.class).withProperty("foos", foos);
- Objects of class Component are immutable. foo.withProperty(. . .) does not change the state of object foo. Instead, it returns a new Component object.
- Components.list(. . .) creates a Component object that will sequentially invoke all the components stored in the array and push the results into a java.util.List object.
Instantiate an array
What if instead of a list, Bar expects an array of Foo? The API is very similar: We just need to switch from Components.list(. . .) to Components.array(. . .).
Component foo = Components.bean(Foo.class);
Component foos = Components.arraynew Component[]{
foo.withProperty("hi", "hello"),
foo.withProeprty("hi", "goodbye")
});
Component bar = Components.bean(Bar.class).withProperty("foos", foos);
- Components.array(. . .) creates a Component object that will invoke all the components stored in the array and push the results into a result array.
Instantiate a java.util.LinkedHashMap
What if instead of list or array, Bar expects a java.util.Map object that contains Foo objects?
public class Foo {
private String hi;
public Foo() {
}
public void setHi(String h) {
this.hi = h;
}
public void greet(Object whom) {
System.out.println(hi + " " + whom);
}
}
public class Bar {
private java.util.Map foos;
private String name;
public Bar() {
}
public void setFoo(java.util.Map f) {
this.foos = f;
}
public void setName(String n) {
this.name = n;
}
public void greeting(String key) {
((Foo) foos.get(key)).greet(name);
}
}
We want to be able to get the following result:
bar.greeting("morning") yields "hello bar";
while bar.greeting("evening") yields "goodbye bar".
Well. Components.hashmap() is your friend.
Component foo = Components.bean(Foo.class);
final java.util.Map foos = new java.util.HashMap();
foos.put("morning",foo.withProperty("hi", Components.value("hello")));
foos.put("evening",foo.withProperty("hi", Components.value("goodbye")));
Component foos = Components.hashmap(foos);
- Components.hashmap(foos) creates a Component object that will create a java.util.LinkedHashMap object when invoked.
If the java.util.Map object contains: {name1=Component1; name2=component2}, the result will be a java.util.LinkedHashMap with value {name1=result from component1; name2=result from component2}
Instantiate a mylist
Now we know how to create a component for standard java.util.List for sequential collection; how to create a component for standard java.util.Map for associative map. We also showed how to create an array component.
But Java collection framework has more classes and interfaces. What if we want to create a java.util.Set? A java.util.LinkedList? A java.util.TreeMap?
What about a List of List? An Array of HashMap? A List of List of Array of HashMap?
What about collection classes outside of java.util package?
Suppose we have a MyList class that we want to use instead of java.util.List (for whatever the reason). The Bar class becomes:
public class Bar {
private MyList foos;
private String name;
public Bar() {
}
public void setFoo(MyList f) {
this.foos = f;
}
public void setName(String n) {
this.name = n;
}
public void greeting() {
for (int i = 0; i < foos.size(); i++) {
((Foo) foos.getAt(i)).greet(name);
}
}
}
Obviously Yan will not have a mylist() function that instantiates MyList for you because Yan is not aware of MyList.
The idea is, you can first use Components.list() or Components.array() to get a Component that creates an array or a list. Then use a jfun.yan.Map object to convert this array or list to MyList. (It is your list, you know how to populate it, do you?)
Creating an array or a list is easy, let's assume we are gonna use array. The next thing is how you use jfun.yan.Map to convert an array to MyList.
jfun.yan.Map m = new jfun.yan.Map(){
public Object map(Object obj){
final Object[] arr = (Object[])obj;//the object is an array.
return MyList.fromArray(arr);
//assume fromArray is tbe function to convert an array to a MyList.
}};
This jfun.yan.Map object is responsible for converting an object (which should be of type Object[]) to a MyList object.
With this jfun.yan.Map object, we are ready for the main-course:
Component foo = Components.bean(Foo.class);
Component foos = Components.array(new Component[]{
foo.withProperty("hi", "hello"),
foo.withProeprty("hi", "goodbye")
});
Component mylist = foos.map(m);
- foos.map(m) function creates a new Component object that will first invoke foos to instantiate an array instance. This instance is then passed to the jfun.yan.Map object to be converted to the target MyList object.
Instantiate a MyHashMap
map() opens a new window. All kinds of component instances can be instantiated by mapping from instance of another component.
Suppose we have a MyHashMap object, we could use map() to convert a java.util.Map to it.
jfun.yan.Map convert_to_myhashmap = new jfun.yan.Map(){
public Object map(Object obj){
final java.util.Map m = (java.util.Map)obj;
return populate_myhashmap_with_map(m);
}};
...
Component myhashmap = Components.hashmap(. . .).map(convert_to_myhashmap);
Instantiate a Pair
map() enables transformation from one component to another. Sometimes, however, we may want to transform more than one components to another component.
For example, componentA creates an object of type A, componentB creates an object of type B. How do I get a component that creates a Pair object which contains both the result from componentA and componentB?
Similar to jfun.yan.Map, there are jfun.yan.Map2, jfun.yan.Map3, jfun.yan.Map4, jfun.yan.Map5 interfaces that represent transformation from 2-5 objects to another object.
To create a Pair, we need to use jfun.yan.Map2 interface:
jfun.yan.Map2 m2 = new jfun.yan.Map2(){
public Object map(Object o1, Object o2){
return new Pair(o1, o2);
}
}
Component foo1 = . . .;
Component foo2 = . . .;
Component pair = Monad.map(foo1, foo2, m2);
- Monad.map(foo1, foo2, m2) will invoke foo1 and foo2 to create two instances. These two objects are then passed to the Map2 object and converted to a third instance.
- Monad.map() function has several variants. It supports transformation for up to 5 components.
Instantiate MyBean.
At the beginning when I showed Yan Container to my friends, my friend Joe challenged the "no-intrusion" concept:
"The 'no-intrusion' doctrine is just over-emphasized.
There has to be some coding standard or convention to make things work. We should just go ahead and follow that convention, whatever it is.
For an extreme example, what if somebody uses 'giveXXX' rather than 'setXXX' for injecting dependency? If you don't support it, should I call that an 'intrusion' or what?"
And I said: "Hold it there, buddy. Yan does support it. When I said no intrusion, I meant it."
Now, let's imagine that Moon Microsystems defines a Java Nuts Specification which uses giveXXX for setting property value and shareXXX for getting property value. We have no interest in the sharer (getter), but let's take a look at how the "giver" is handled.
Before starting though, a very important interface Binder needs to be brought to your attention.
This is how Binder is defined:
public interface Binder{
Component bind(Object obj);
}
Looks very similar to jfun.yan.Map interface. It is also responsible for mapping an instance created from another component.
The difference is the return type. Rather than directly converting the instance to another object, Binder returns another Component which in turn can be used to create the target object.(In the real API, return type of bind is Creator, a super interface of Component, but for now let's not worry about that minor detail.):
- Binder enables staged components where a component is constituted with different stages. Each stage only does part of the job. The stages are combined together using Binder objects, where the result of the previous stage is passed in to the Binder object and a next stage Component is calculated dynamically based on the result of the previous stage.
Pseudo code for staging:
instance1 <- component_stage1.create();
component_stage2 <- binder.bind(instance1);
instance2 <- component_stage2.create();
If you still don't see the value of such a simple interface, it is fine, you will see it once we start doing MyBean.
Alright, armed with the knowledge of Binder, let's buckle up and go!
First, Let's design class MyBean that follows the Java Nuts Specification:
public class MyBean {
private double money;
private long time;
private String name;
public MyBean() {
}
public void giveMoney(double amount) {
this.money = amount;
}
public void giveTime(long ms) {
this.time = ms;
}
public void giveName(String n) {
this.name = n;
}
}
Our target function should look like this:
Component mybean(Class cls, String[] props);
mybean() should create a Component that creates an object of class "cls" and set the properties following the Java Nuts Specification.
So that mybean(MyBean.class, new String[]{"money", "time", "name"}) will create a Component that instantiates a MyBean object using all the properties.
And heres' how we implement this function:
Component mybean(Class cls, final String[] props){
final Component stage0 = Components.ctor(cls, null);
final Binder binder = new Binder(){
public Component bind(Object instance){
Component r = Components.value(instance);
for(int i=0; i<props.length;i++){
final String method_name = "give"+props[i];
final Component invoke_method = Components.method(instance, method_name);
r = r.seq(invoke_method);
}
return r;
}
}
return stage0.followedBy(binder);
}
- stage0 is responsible for invoking the default constructor of the class.
- In Components.ctor(cls, null), null is used to explicitly pick the default constructor. This avoids ambiguity when there are more than one public constructors.
- Components.method(instance, method_name) is responsible for invoking a method against object instance. The method name is specified in the parameter method_name.
- r.seq(invoke_method) is a sequencing operation. It creates a new Component object that will call r first and then call invoke_method. This allows us to sequentially invoke all the "givers".
- stage0.followedBy(binder)
tells container to run stage0 first,
pass the result to binder but discard the result from it in favor of the result from stage0.
Using pseudo code, it is like:r <- stage0.create(); binder.bind(r).create(); return r;
In summary, stage0 is responsible for invoking default constructor;
binder is responsible for invoking "givers" against the MyBean object.
stage0.followedBy(binder) combines these two together and does the entire job.
How to disable auto-wiring?
In auto-wiring, each parameter and property gets injected automatically by the container, which saves us quite some work.
It works just fine in an application with small number of components to be laced up.
It may not be desirable, however, if the application scales up. Configuration using auto-wiring in an application with large number of components tends to be less predictable and less maintainable. It is like walking in a maze to figure out who exactly is injected to whom.
Using combinators like withArgument(), withProperty(), one can already manual-wire some dependencies. The container is only responsible for dependencies not explicitly specified in manual-wiring. In case when you absolutely don't want auto-wiring stand in your way, Yan provides two ways of disabling it.
- At container level, instead of DefaultContainer,
use ManualContainer that does not resolve dependency for its components at all. This disables auto-wiring at a global level:
Container yan = new ManualContainer(); //instead of new DefaultContainer(); - At component level, use combinator seal() to prevent auto-wiring for a specific component:
Component sealed = Components.ctor(Integer.class) withArgument(0, Components.value(10)) .seal();seal() indicates that the component is self-contained. Its dependency is totally resolved by itself, which is true because the constructor of class Integer only expects one parameter and that parameter is already set by withArgument().
About Abstract Factory Pattern
Some say that IOC container invalidates the need for factory or abstract factory.
We strongly disagree with this view because we believe an IOC container should be non-intrusive and should remain invisible to the application domain.
We claim that it is an anti-pattern to have the application code depend on IOC container which should only be used by the person responsible for assemblying various business components.
For most application developers, factory and abstract factory are still necessary patterns when some object creation logic needs to be encapsulated or abstracted.
For example, our Bar class may be refactored to require an abstract factory instead of Foo or Greeter:
public interface GreeterFactory {
Greeter createGreeter(String hi);
}
public class Bar {
GreeterFactory factory;
private final String name;
private final String hi;
public Bar(GreeterFactory factory, String hi, String name) {
this.factory = factory;
this.hi = hi;
this.name = name;
}
public void greeting() {
factory.createGreeter(hi).greet(name);
}
}
This new Bar class uses an abstract factory to create Greeter object on the fly, with no dependency on Container.
Abstract factory pattern does have one awkward drawback: implementations of abstract factory typically depend on concrete classes of its product.
For example, if we have two implementations of interface Greeter:
public class Foo1 implements Greeter{
. . .
}
public class Foo2 implements Greeter{
. . .
}
We will need two implementation classes of GreeterFactory that creates Foo1 and Foo2 respectively:
public class Foo1Factory implements GreeterFactory {
Greeter createGreeter(String hi) {
return new Foo1(hi);
}
}
public class Foo2Factory implements GreeterFactory {
Greeter createGreeter(String hi) {
return new Foo2(hi);
}
}
Very slight difference, but we need two classes.
Imagine that we have 100 difference implementations of interface Greeter, we will need 100 extra implementation classes for GreeterFactory!
This thought could easily scare away us lazy programmers and drive us to use less Object-Oriented design such as reflection.
For example:
public class Bar {
Class greeter_class;
private final String name;
private final String hi;
public Bar(Class greeter_class, String hi, String name) {
this.greeter_class = greeter_class;
this.hi = hi;
this.name = name;
}
public void greeting() {
Constructor ctor = greeter_class.getConstructor(new Class[] { String.class });
Greeter greeter = (Greeter) ctor.invoke(new Object[] { hi });
greeter.greet(name);
}
}
The design is less elegant but it does save programmers from having to create many implementation classes of GreeterFactory. java.lang.Class is used instead as a factory free of charge.
Java anonymous class can make things a little bit better but the syntax for anonymous class is still cumbersome.
Dilemma? As a big fan of "programming-against-interface", I strongly recommend you to stick to the first deisgn where class Bar depends on an abstract GreeterFactory interface.
In order to liberate pogrammers from creating the 100 extra GreeterFactory implementation classes, Yan offers a factory() function that will automatically implement GreeterFactory.
Component foo1 = Components.ctor(Foo1.class); Component foo2 = Component.ctor(Foo2.class); Component factory1 = foo1.factory(GreeterFactory.class); Component factory2 = foo2.factory(GreeterFactory.class); Component bar = Components.ctor(Bar.class).withArgument(0, factory1);
- foo1 and foo2 are two components that instantiate Foo1 and Foo2 respectively.
- foo1.factory(GreeterFactory.class) creates a Component object that instantiate a GreeterFactory object. This GreeterFactory object, when invoked, will in turn call foo1 to instantiate a Foo1 object.
- factory1.createGreeter("hello") will forward the parameter "hello" to the constructor of Foo1.
This saves programmers lot of work. It is typically not necessary to create one factory implementation for each and every different product. When Yan is assemblying components, it uses the factory() to automatically generate a factory class.
Application programmers can now follow the "programming-against-interface" principle and be rest-assured that they don't need to do boring extra work like creating trivial factory classes.
Container Inheritance
Components can be stored in different containers to be kept separate. Components from container 1 is not visible to those from container 2 and vice versa. However, if we wish, we can let them communicate in a controlled manner.
For example, if we create two conainers and register our Foo and Bar separately:
Component foo = Components.ctor(Foo.class); Component bar = Components.ctor(Bar.class); Container yan1 = new DefaultContainer(); Container yan2 = new DefaultContainer(); yan1.registerComponent(foo); yan2.registerComponent(bar);
Now if we try to instantiate Bar from yan2, we will get an error because it depends on Foo while Foo cannot be found in yan2.Now if we try to instantiate Bar from yan2, we will get an error because it depends on Foo while Foo cannot be found in yan2.
We can create a new virtual container that can make components in yan1 visible to yan2.
Container yan = yan2.inherit(yan1);
- yan2.inherit(yan1) creates a virtual container where all the components in yan1 are visible to yan2 while those from yan2 are not visible to yan1.
- inherit() is an immutable function. i.e. neither yan1 nor yan2 is modified by this function. It only creates a new virtual container that honors the inheritance relationship between these two containers.
If we now try to instantiate Bar from container yan, Foo will be visible to Bar and therefore the Bar component can be instantiated successfully. While since neither yan1 nor yan2 was changed by inherit(), trying to instantiate Bar from either of them will still result in an exception.
The immutable nature of inherit() enables real flexible thing. For example, we could also create another virtual container that makes components in yan2 visible to yan1:
Container another_yan = yan1.inherit(yan2);
And YET another conainer that makes yan1 inherit yan3 (some other container):
Container yet_another_yan = yan1.inherit(yan3);
These virtual containers can all exist in the program at the same time and we programmers can flexibly choose between them or even use them all.
"Hey, so yan1 is parent of yan2 and yan2 is parent of yan1? What the heck is that?" Well, they may or may not make sense to your application, or they may make sense to one but not the other. To understand it better, we can think of this as a "what-if" question.
If the question is "what if yan1 is a parent of yan2?", we go from the virtual container yan; and if the question is "what if yan2 is a parent of yan1?", we start from the virtual container another_yan.
If you know relational database such as Oracle or SQL Server, containers are like tables that hold physical data, while the virtual containers created by inherit() are like views that you can join the tables at will. In view 1, you can make Tom parent of Jack, while in view 2, you can make Jack parent of Tom. However you play this logical trick, the physical tables are not changed a bit.
This relational model of managing data has been proved more flexible and easier to maintain than if the "Tom is parent of Jack" relationship were hard-wired. We believe container management can benefit from this model too.