If you can't explain it to a six year old, you don't understand it yourself. (Albert Einstein)Despite release 1.0 of Spring was in 2003 the new wave of programmers seem to misuse the concept. Below we'll consider a small example to explain.
Application calls A to perform a task. A calls B, B in its turn calls C. Below is a naive approach to implement this. Full source code is available.
package test.naive; public class A { private Options options; private B b; public static class Options { } public A(Options options, B.Options optionsForB, C.Options optionsForC) { this.options = options; this.b = new B(optionsForB, optionsForC); } public void apply() { b.apply(); System.out.println(options); } } //------------------------------------------------------------------ package test.naive; public class B { private Options options; private C c; public static class Options { } public B(Options options, C.Options optionsForC) { this.options = options; this.c = new C(optionsForC); } public void apply() { c.apply(); System.out.println(options); } } //------------------------------------------------------------------ package test.naive; public class C { private Options options; public static class Options { } public C(Options options) { this.options = options; } public void apply() { System.out.println(options); } } //------------------------------------------------------------------ package test.naive; public class Application { /** * @param args */ public static void main(String[] args) { A a = new A(new A.Options(), new B.Options(), new C.Options()); a.apply(); } }'This code is bad', you say. Of course, but why? We'll do some math here kids. Money (cost) matters. As a dev the only thing you can do in minimizing the cost is to minimize the time to develop and support. The time to develop correct thing and incorrect one is almost the same (as you'll see below). The time to support matters (that means time to introduce changes due to new features or found bugs). How one can know that support of thing above will be costly? There are metrics in the field for this. The two for our case is Cohesion and Coupling. They are tightly bound together but not interchangeable. There are a lot formulas behind this, but the only thing you need to find out is what your class depends on (coupling) or does (cohesion) it can freely live without.
Consider class A. What unnecessary dependencies it has?
- B.Options
- C.Options
- Constructor B(optionsForB, optionsForC)
- B instance lifecycle management
So, the first obvious problem is Options forwarding hell. The second naive approach is below to help with this (classes B and C are omitted here and further while they are simplified versions of class A).
package test.config; public class A { private Options options; private B b; public static class Options { } public A(AbcConfiguration conf) { this.options = conf.getOptionsForA(); this.b = new B(conf); } public void apply() { b.apply(); System.out.println(options); } } //------------------------------------------------------------------ package test.config; public class AbcConfiguration { public A.Options getOptionsForA() { return new A.Options(); } public B.Options getOptionsForB() { return new B.Options(); } public C.Options getOptionsForC() { return new C.Options(); } } //------------------------------------------------------------------ package test.config; public class Application { /** * @param args */ public static void main(String[] args) { A a = new A(new AbcConfiguration()); a.apply(); } }Better. A little bit. What unnecessary dependencies class A has now?
- AbcConfiguration
- Constructor B(AbcConfiguration) but not as bad as in test.naive example
- B instance lifecycle management
package test.factory; public class A { private Options options; private B b; public static class Options { } public A(Options options, AbcFactory factory) { this.options = options; this.b = factory.getB(); } public void apply() { b.apply(); System.out.println(options); } } //------------------------------------------------------------------ package test.factory; public class AbcFactory { public A getA() { return new A(new A.Options(), this); } public B getB() { return new B(new B.Options(), this); } public C getC() { return new C(new C.Options(), this); } } //------------------------------------------------------------------ package test.factory; public class Application { /** * @param args */ public static void main(String[] args) { AbcFactory factory = new AbcFactory(); A a = factory.getA(); a.apply(); } }AbcFactory absorbs (incapsulates) constructor and lifecycle management dependencies. What unnecessary dependencies class A has now?
- AbcFactory
There is a hidden problem here. When client (our Application) uses class A it doesn't know what will be taken from the factory. One needs to traverse the code of A, B and C classes to get an idea about which instances factory should provide. Class A is coupled with class AbcFactory which is coupled with too many things in its turn (ok, and this is not for kids). Imagine you want to reuse (isolate) A, and A goes only with AbcFactory which is complicated to isolate.
Example above is still not an inversion of control, it is more a delegation of instance creation and destruction to the factory. Now lets take Spring 4 - a DI framework - and eliminate the last bad dependency. (See what is a relation between DI and IoC on StackOverflow.)
package test.spring; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @Component public class A { @Autowired private Options options; @Autowired private B b; public static class Options { } public A() { } public void apply() { b.apply(); System.out.println(options); } } //------------------------------------------------------------------ package test.spring; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; @Configuration @ComponentScan public class AbcConfiguration { @Bean public A.Options getOptionsForA() { return new A.Options(); } @Bean public B.Options getOptionsForB() { return new B.Options(); } @Bean public C.Options getOptionsForC() { return new C.Options(); } } //------------------------------------------------------------------ package test.spring; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; public class Application { /** * @param args */ public static void main(String[] args) { ApplicationContext applicationContext = new AnnotationConfigApplicationContext( AbcConfiguration.class); A a = applicationContext.getBean(A.class); a.apply(); } }No xml configuration is needed, Java-based container configuration is used. Now class A doesn't have some unwanted deps. But something is wrong here. Class A is bloated with annotations, and what is most important its contract is not clear. Class contract is
- Required dependencies are passed to constructor.
- Optional dependencies (which can be omitted or have default implementations) are injected via setters.
And now we'll take a CDI implementation Weld to eliminate last inconveniences. CDI stands for Contexts and Dependency Injection and is a standard implemented by all Java EE 6/7 containers (defined by JSR 299). Weld is used by Oracle GlassFish, JBoss AS, Oracle WebLogic and others. There are other production ready implementations of CDI, for instance, OpenWebBeans and CanDI. We'll use Weld in Java SE, see Application servers and environments supported by Weld. Results are below.
package test.cdi; public class A { private Options options; private B b; public static class Options { } public A(Options options, B b) { this.options = options; this.b = b; } public void apply() { b.apply(); System.out.println(options); } } //------------------------------------------------------------------ package test.cdi; import javax.enterprise.inject.Produces; public class AbcProducer { @Produces public A createA(B b) { return new A(new A.Options(), b); } @Produces public B createB(C c) { return new B(new B.Options(), c); } @Produces public C createC() { return new C(new C.Options()); } } //------------------------------------------------------------------ package test.cdi; import org.jboss.weld.environment.se.Weld; import org.jboss.weld.environment.se.WeldContainer; public class Application { /** * @param args */ public static void main(String[] args) { Weld weld = new Weld(); WeldContainer container = weld.initialize(); A a = container.instance().select(A.class).get(); a.apply(); weld.shutdown(); } }This is pretty much it. No unwanted dependencies. Clear contract. And CDI can do a lot more like AOP, handle ambiguities, runtime dependency resolution, etc. See more on CDI at Java EE 7 tutorial.
But most important is that the same result can be achieved in our small example without any framework.
package test.ioc; public class A { private Options options; private B b; public static class Options { } public A(Options options, B b) { this.options = options; this.b = b; } public void apply() { b.apply(); System.out.println(options); } } //------------------------------------------------------------------ package test.ioc; public class Application { /** * @param args */ public static void main(String[] args) { C c = new C(new C.Options()); B b = new B(new B.Options(), c); A a = new A(new A.Options(), b); a.apply(); } }I want you to understand, IoC is not a framework or a library, it is a way of doing, a way of being done.
A final word about cookies. I mean python. The same task
class A(object): def __init__(self, options, optionsForB, optionsForC): self.options = options self.b = B(optionsForB, optionsForC) def __call__(self): self.b() print(self.options) class B(object): def __init__(self, options, optionsForC): self.options = options self.c = C(optionsForC) def __call__(self): self.c() print(self.options) class C(object): def __init__(self, options): self.options = options def __call__(self): print(self.options) if __name__ == "__main__": A("A", "B", "C")()Despite python is a huge factory by itself - the module is like a dictionary with entries with objects that can be replaced in runtime and class definition is nothing more than some object with some name in some module - the implementation above is coupled like a hell (like test.naive java example). One can omit forwarding options by abstracting Configuration and will get the same result as in test.config. One can get similar results with test.factory. But one can also write something like test.ioc example using IoC principle and decrease coupling to the minimum.
class A(object): def __init__(self, options, b): self.options = options self.b = b def __call__(self): self.b() print(self.options) class B(object): def __init__(self, options, c): self.options = options self.c = c def __call__(self): self.c() print(self.options) class C(object): def __init__(self, options): self.options = options def __call__(self): print(self.options) if __name__ == "__main__": c = C("C") b = B("B", c) a = A("A", b) a()
No comments:
Post a Comment