Seeing objects as services
- A central idea in this book is that -
of an object acting as a service. Take an email sending service for example. The email itself can be viewed as a message
composition service, the internet relays that are routing the email to its destination as delivery agents, and the receiver's inbox as a receiving service.
- Consider the following architecture for the service:
- Let the three objects responsible for these actions be:
Emailer
,
InternetRelay
,
RecipientInbox
. Each object is a
client
of the next.
Emailer
uses the
InternetRelay
as a service to send email, and in turn, the
InternetRelay
uses the
RecipientInbox
as a service to deliver sent mail.
- There's value in decomposing our services into objects. Take the
Emailer
for example. The
Emailer
will require a text editor to type-in the
email. This logic could be added in the
Emailer
class itself, but it is better to create a new
TextEditor
. Pulling this functionality into its
own class has the following advantages:
a) Our
Emailer
is not cluttered with distracting code meant for text manipulation.
b) We can reuse the
TextEditor
component in other scenarios (say, a calendar or note-taking application) without much additional coding.
c) If someone else has written a general-purpose text-editing component, we can make use of it rather than writing one from scratch
- This principle is important because it highlights the relationship between one object and other objects it uses to perform its function: an object depends on its
services in order to perform its function. In this example that we are talking about, the
Emailer
class depends upon a
TextEditor
,
SpellChecker
, and a
Dictionary
to perform its functions. Hence these 3 classes are the
dependencies of the
Emailer
class.
In other words, the
Emailer
class is a
client of its
dependencies.
- The dependencies of the
Emailer
class may have dependencies of their own and this relationship carries on transitively. This composite system of
dependencies is known as the
object graph.
- Hence we get the following definitions:
a)
Service
: An object that performs a
well-defined function when it is called.
b)
Client
: Any consumer of a service. A client calls a service to perform a
well-understood function.
c)
Dependency
: A specific service that is
required by another object to fulfill its function.
d)
Dependent
: A client object that needs a dependency (or dependencies) in order to perform its function.
e)
Dependency Injection
: DI is the study of reliably and efficiently building such object graphs and the strategies, patterns, and best
practices therein.
Pre DI Solutions
- Suppose you were asked to create an
Emailer
which is composed of a
SpellChecker
, this might be the first thing that you come up with:
Expand Gist
- But what is the problem with this approach? Suppose you wanted to test that the
SpellChecker
was in-fact being called when you used the
send
method to send an email. One way to do this would be to create a
MockSpellChecker
and make
Emailer
use that:
Expand Gist
- But now you cannot use this mock because you cannot substitute the internal spellchecker that the
Emailer
class has. This effectively makes the
Emailer
class untestable.
- There is another problem with this approach. You cannot create objects of the same
Emailer
class with different functionality. For instance, suppose you
were writing an email client for the english language, then you would do something like:
Expand Gist
- Now if you wanted to use the same
Emailer
class for the french language, you would have to create a completely new
Emailer
class. We need
another system where instead of the dependent creating its own dependencies, it has them provided externally.
Construction by hand
- Instead of the constructor of the
Emailer
creating its own
SpellChecker
, we can add a method that
accepts a
SpellChecker
.
Expand Gist
- This allows us to substitute the
SpellChecker
with a version of our own, and thus we can use the
MockSpellChecker
that we created above to
now test the functionality of the
Emailer
class. Similarly, we can also substitute the
SpellChecker
with another spell checker easily now.
Expand Gist
- In the above example, we are setting the
SpellChecker
manually. At the time of construction of the
Emailer
class, we are manually setting the
dependency. Hence, this is also sometimes referred to as
construction by hand.
- The above code was an example of
setter injection. You can also do a
constructor injection by passing in the dependency during the construction of the
Emailer
class itself. The advantage of following this approach is that it makes the dependency between the two classes explicit. You cannot create an
Emailer
class without creating a
SpellChecker
class. And thus it reduces the chances of someone forgetting to set the dependency on the object.
Expand Gist
Problems with construction-by-hand
:
The problem with this technique is that the burden of knowing how to create the object graph is placed on the client of the service. In this case, it is the
Emailer
class that must set up its dependency,
SpellChecker
, either through constructor or through setter injection. Also, if the same object is
being
used in multiple places, then you need to repeat the code of wiring together the object in all the places. If you decide to change the dependency graph in the future, you
will have to go to all the places and make the change yourself.
- Another problem is the fact that users need to know how object graphs are wired internally. This violates the principle of encapsulation and becomes problematic when
dealing with code that is used by many clients, who really shouldn't have to care about the internals of their dependencies in order to use them.
Factory Pattern
- One way of getting around the problem of having to construct the objects in an object graph yourself is to use a Factory Pattern.
Expand Gist
- The most important thing to notice here is that the client code has no reference to spellchecking, address books, or any of the other internals of
Emailer
.
By adding a level of abstraction (the Factory pattern), we have separated the code using the
Emailer
from the code that creates the
Emailer
. This
leaves client code clean and concise. This becomes even more useful when the object graph of the created object is even more complex.
Expand Gist
- Talking about testing, the
Emailer
code is still using the same constructor injection it was using previously. So we can still test the
Emailer
class as we were previously.
Problems with using Factory Pattern
:
Testing the internals of the object returned from the factory becomes difficult unless the factory has explicitly provided capabilities to test it. What do I mean by that?
This client does not know anything about
Emailer
's internals; instead, it depends on a Factory. In one of the previous scenarios, we were able to swap out the
SpellChecker
being injected into the class with a
MockSpellChecker
of our own. But that will not be possible in this case.
- Another issue is that if you are going with Factory Pattern then you will have to create a new method for
every variation of
every service. That is a lot of
code to test and maintain. If you decided that you wanted to replace the
AddressBook
with
PhoneAndAddressBook
, you will have to go through all the
methods in the factory and make the substitution. Plus there might be many dependencies that are common across different types of
Emailer objects but which you still
have to specify separately for each new method that you add to the factory. One way to get around this is to use a 'general' factory that first creates a common version of
the object and then use that intermediate object to set any specific fields on it. But this again leads to code that is difficult to test.
Service Locator Pattern
- A Service Locator is also a kind of factory. The way that it works is that you pass in a
key to the service locator, and the service locator replies with a
fully-formed object that you can use in your code.
- This is what it would look like:
Emailer emailer = (Emailer) new ServiceLocator().get("Emailer");
- Since all the locator needs is a key, a Service Locator acts as a factory that can return
any kind of service. Compare this to the
EmailerFactory class where
it was returning only one type of object - namely
Emailer.
- The Java Naming and Directory Interface (JNDI) is a good example of a Service Locator pattern. It is often used by application servers to register resources at start time
and later by deployed applications to look them up. Web applications typically use JNDI to look up data sources in this manner.
- An example of gettig a different object:
Emailer emailer = (Emailer) new ServiceLocator().get("JapaneseEmailerWithPhoneAndEmail");
Problems with using Service Locator Pattern
:
Being a kind of Factory, Service Locators suffer from the same problems of testability and shared state. The keys used to identify a service are opaque and
can be confusing to work with. The practice of embedding information about the service within its identifier (namely, "JapaneseEmailerWithPhoneAndEmail") is also verbose and
places too much emphasis on arbitrary conventions.
Embracing Dependency Injection
- DI is a magical way that takes all the good parts of the above solutions and leaves behind all the bad parts. DI enables testability in the same way as construction
by hand, via a setter method or constructor injection. DI removes the need for clients to know about their dependencies and how to create them, just as factories do. It
leaves behind the problems of shared state and repetitive clutter, by moving construction responsibility to a library.
- In case of DI, the task of creating, assembling, and wiring the dependencies into an object graph is performed by an external framework known as a
dependency
injection framework or simply a
dependency injector. The control over the construction, wiring, and assembly of an object graph no loger resides with the clients
or services themselves. This is also known as the
Inversion of Control.
- Notice that
EmailClient knows nothing about what kind of
Emailer it needs or is using to send a message. All it knows is that it accepts
some kind of
Emailer, and this dependency is used when needed. Also notice that the client code is now starting to resemble service code; both are free of logic to create or locate
dependencies.
- This highlights the separation of
infrastructure code (meant for wiring and construction) from
application code (the core purpose of a service).
Expand Gist
- Footnote about Gin: Guice is also available inside the Google Web Toolkit (GWT), via its cousin Gin. Google Gin is a port of Guice for the GWT, a JavaScript web framework.
Find out more
here.
Constructing objects with dependency injection
- The dependency injector is itself a service like any other. And like any service, it must be constructed and prepared before it can be used. Once the injector has been
bootstrapped, we can obtain an object from it for use.
- This is an example of using Guice for injection:
Expand Gist
- Think of Guice as a factory class for Dependency Injectors. Note that in the above example, you are asking Guice directly for an
Emailer class. The object graph of
the
Emailer class requires dependencies
SpellChecker and
Dict. But you don't have to worry about providing these. Guice can wire-up the fully-formed
object for you when you request it. Also note that you had to use the
@Inject
annotation on the
Emailer and the
SpellChecker classes in order to
get the code to compile properly. If you remove this annotation, from either of the classes, you get the following error: "Could not find a suitable constructor in
Chapter2._1_Basics.SpellChecker. Classes must have either one (and only one) constructor annotated with @Inject or a zero-argument constructor that is not private."
- The
@Inject
annotation is supposed to tell Guice which constructor to use when creating the object, and consequently what it's dependencies are.
- In this particular line:
Emailer emailer = Guice.createInjector().getInstance(Emailer.class);
you are doing something similar to what we did in the Service
Locator pattern, the difference being now we are using a class type to get an object as the key, instead of directly using a string. In a sense the injector is an automated,
all-purpose service locator prepackaged for convenience.
Metadata and injector configuration
- In essence what you did by using the @Inject
annotation was configure the injector by telling it which constructor to use and hence what the
dependencies of the object are. If you had been using Spring, you would do the same thing by using @Component
and @Autowired
combination.
- There are multiple ways in which you can configure the injector:
a) Using an XML Configuration File
b) Invoking into a programmatic API
c) Using an injection-oriented DSL
d) By inference; that is, implicitly from the structure of classes
e) By inference from annotated source code
XML Injection in Spring
- When using injection in Spring, we do the following (the other stuff remains common):
Expand Gist
- Here,
BeanFactory
refers to the Spring dependency injector. The call to
getBean()
is analogous to the original call to
getInstance()
.
Both return an instance of
Emailer. The
FileSystemXmlApplicationContext
is a special type of
BeanFactory
. There are other kinds of
application contexts available for use as well. They are mentioned on
this link on Baeldung.
- The file email.xml contains the configuration required for Spring's injector to provide us with an emailer correctly wired with its dependencies.
Expand Gist
XML to in-code configuration in Guice
- Using an XML configuration is advantageous in the sense that all the configuration can be easily seen in one place which becomes easier when you are working on a large
project. You can do the same thing with Guice as well. In the case of Guice, you can do it in two ways. Either by implementing the
Module
interface, or by
extending the
AbstractModule
class.
- Guice is able to automatically inject Concrete Class with either a public no argument constructor or a constructor with @Inject without any specific defined binding in your
module but when it comes to Interfaces you have to define the necessary bindings.
Source on SO. This is what the
module class would look like with the bindings defined:
Expand Gist
- Note that the config on the main class now changes slightly. Note that we are passing in the module class that we created to the
createInjector
method. Without
this it does not work.
Expand Gist
- If you were doing the same thing using the
AbstractModule
, this is what the module class would look like. And instead of passing in the
new EmailModule()
as you did above, you would now be passing in
new EmailAbstractModule()
.
Expand Gist
- Note that you can also pass in multiple modules as args:
Expand Gist
Revisiting Spring and autowiring
- You can use autowire in the config files to configure the Spring injections as well. The config file in that case will look like this. But note that Intellij does not like
this and tries to revert to the method above, ie. make the constructor injection explicit.
Expand Gist
- Of course, you can also just entirely ignore the XML configuration, and go directly with annotations. In the setup that you are using, you still included the xml file as
well.
Expand Gist
- And the context that we are using to get the beans from is defined as follows:
Expand Gist
- Note that now we have to use the
ClassPathXmlApplicationContext
, instead of the
FileSystemXmlApplicationContext
that we used previously above.
Note that you also had to add this in Facets in Intellij, because it was giving you a warning. Note the folder structure as well. The reason for highlighting the folder
structure is so that you can notice the usage of class path related stuff. See how paths are being used in
new ClassPathXmlApplicationContext("email-actual-autowire-config.xml")
and
<context:component-scan base-package="_1_Basics"/>
and relate it to
the folder structure being used. The same warning that you get when you add an XML configuration file, you also get when you annotate a java class with
@Configuration
. Both, XML files and
@Configuration
are different but equivalent ways of telling Spring how you want to setup the beans.
- The three application contexts that were introduced above can be seen here:
Identifying dependencies for injection
- A
flat object is an object that has no dependencies of its own.
- In the Guice example that we saw above, there is a single implementation of the
EmailerIF that we are using to inject in as a dependency, namely the
Emailer
class. But we might have multiple implemnetations of the same interface and we may want to conditionally inject different implementations depending on our requirements. For
example, on the right side, we have two implementations of the
EmailerIF,
Emailer and
AdvancedEmailer. So when we create an
EmailClient, we might
might want to choose either of the implementations,
Emailer or
AdvancedEmailer. So how do you tell your injector which of these two dependencies you want to
use? This is the question we will be trying to answer here.
Identifying by String keys
- Suppose that we are using Spring, and we are trying to create the following dependency graph.
Emailer requires a
SpellChecker, and there are two possible
implementations of it -
EnglishSpellChecker and
FrenchSpellChecker.
- Then depending on the type of
Emailer that we are trying to create, we can configure the xml as follows:
Expand Gist
- And we create instances of the
Emailer as follows: and this will give us a fully-formed french emailer.
Expand Gist
Drawbacks of this method
:
a) A misspelled key that maps to no valid service can be difficult to detect until well into the runtime of the program. If you are working with a statically typed language
like Java, this is a poor sacrifice to have to make. These languages are supposed to offer guarantees around type validity at compile time. Ideally, one should not have to
wait until runtime to detect problems in key bindings. Intellij does not warn you about these kinds of errors, and they appear only during runtime.
b) In your injector configuration there is no way to determine the type of a binding if all you have is a string key. Without starting up the injector and inspecting the
specific instance for a key, it is hard to determine what type the key is bound to. But do note that Intellij does provide you with some level of static checking in order to
minimize this kind of issue. For example suppose you passed in an incorrect class
Dict_2 instead of
Dict, Intellij would give you a compile error.
Expand Gist
Identifying by type
- Since Guice configuration file requires you to explicitly specify the class that you are going to inject, the compile time checking of the classes being injected can be
done in Guice configuration as well (just like Spring). This takes care of the point 2 of the drawback we discussed above. Also since there is no name in the form of String
being associated with the dependency, there is no scope of mistyping the name and getting a run time error either. This takes care of the point 1 of the drawback.
- All the stuff with the XML configuration that we did in the above section using Spring, we can also do using simple annotations. We make use of the
@Configuration
and
@Bean
annotations for this. The objective is still the same - pass in different implementations of the
SpellChecker class
for the specific implementations for the
Emailer. But this time without using XML.
- First we need to define a configuration class that would be equivalent to defining the XML file in the previous approach. The
@Configuration
annotation
indicates that the class can be used by the Spring IoC container as a source of bean definitions. It indicates that a class declares one or more
@Bean
methods
and may be processed by the Spring container to generate bean definitions and service requests for those beans at runtime.
- The
@Bean
annotation tells Spring that a method annotated with
@Bean
will return an object that should be registered as a bean in the Spring
application context. See
this link on Tutorials Point for a good explanation.
We aren't doing anything special, all we have done is convert the XML bindings into code.
Expand Gist
- And then we can use these beans as follows. We need to register and refresh the context for
this reason explained on SO.
- If you had just a single object of a particular type, you could have fetched the object using the type of the object, as we are doing with the
EnglishSpellChecker in
the below example. For other cases, you would have to fall back to using the String keys instead.
Expand Gist
Drawbacks of this method
:
a) Like we saw, if we use types, we cannot distinguish between different implementations of the same service.
Combinatorial Keys
- Ideally we want something like this:
[Emailer.class, "english"]. The type key identifies the service that dependents rely on, in a safe and consistent manner. Its
counterpart, the string key, identifies the specific variant that the key is bound to but does so in an abstract manner. However, the String part of the key is still
susceptible to typos and errors that we saw previously. We get around this issue by using an annotation in place of a String.
- Guice
TM embodies the use of type/annotation combinatorial keys and was probably the first library to use this strategy.
- Suppose this is the object graph that we are going to create. As we can see in the diagram,
FrenchEmailer and
EnglishEmailer both require the dependency
SpellChecker. But there are two different implementations available:
FrenchSpellChecker and
EnglishSpellChecker. We want the
EnglishSpellChecker
to be injected into the
EnglishEmailer class and the
FrenchSpellChecker into the
FrenchEmailer class.
- The way we are going to do this is through the use of annotations. At the point where we are injecting
SpellChecker type into the
EnglishEmailer for eg., we
will annotate that injection with a custom annotation
@English
that we have created. Then we are going to tell Guice that - hey, whenever you see a dependency
that requires the
SpellChecker object, and has been annotated with
@English, you should inject the
EnglishSpellChecker exclusively. In the same manner,
we are going to do for the french side as well.
Step 1) We create custom annotations that we are going to make use of.
Expand Gist
Step 2) Annotate the injections with the respective annotations:
Expand Gist
Step 3) Tell Guice to inject the dependencies based off of the annotation that we are using:
Expand Gist
Step 4) Get the object that you are interested in from the injector and makes use as required:
Expand Gist
- This might strike you as odd - while the
EnglishEmailer does not depend directly on a specific implementation, it seems to couple via the use of
@English
annotation. Doesn't this mean
EnglishEmailer is tightly coupled to English spellcheckers? The answer is a surprising no. To understand why,
consider the following altered injector configuration:
bind(SpellChecker.class).annotatedWith(English.class).to(FrenchSpellChecker.class);
. Here, we've changed
the service implementation bound to the combinatorial key
[SpellChecker.class, English.class] so that any dependent referring to an
@English annotated
SpellChecker will actually receive an implementation of type
FrenchSpellChecker. There are no errors, and
EnglishEmailer is unaware of
any change and more importantly
unaffected by any change. No client code needs to change, and we were able to alter configuration transparently. So they aren't really
tightly coupled.
- In this chapter we will look at different injection idioms. We will see why we should favor constructor injection. We will also see when setter injection is favorable and how
to decide between the two. Understanding injection idioms and the nuances behind them is central to a good grasp of dependency injection and architecture in general.
Injection idioms
- The key to understanding the differences between setter and constructor injection is to understand the differences between methods and constructors. The advantages and
limitations arise from these essential language constructs.
1) Constructor Injection
:
- A constructor's purpose is to perform initial setup work on the instance being constructed, using provided arguments as necessary. This setup work may be wiring of
dependencies (what we are typically interested in) or some computation that is necessary prior to the object's use.
- A constructor has restrictions that differentiate it from a normal method:
a) Cannot (does not) return anything
b) Must
complete before an instance is considered fully constructed
c) Can initialize
final
fields, whereas a method cannot
d) Can be called only once per instance
e) Cannot be given an arbitrary name - in general, it is named after the class
2) Setter Injection
:
- In case of setter injection, the wiring takes place
after the instance has been fully constructed.
- The setter injection differs based on the DI framework that you are using. In the case of Spring, a setter directly refers to a property on the object and is set via the
<property>
XML element.
- This is what the class in the case of Setter Injection would look like. Also note that the setters in this case need to be declared as
public
.
Expand Gist
- And the XML configuration file would look like this. Note that if you misspell the name of the setter, Intellij is smart enough to give you a compile error. Also note that
the order in which these setters are called usually matches the order of the
<property>
tags. This ordering is unique to the XML configuration
mechanism and is not available with autowiring.
Expand Gist
- Doing the same setter injection in the case of Guice would look something like this. Recall that for concrete classes dependency in the case of Guice, we do not need to
bind them in the AbstractModule. So for this example, we can leave the AbstractModule class empty. Note that we have annotated the methods with
@Inject
.
Interestingly, even if you remove the annotation from the method, the code still works. So the annotations are not mandatory?
- The order in which these setters are called is undefined, unlike with Spring's XML configuration.
- Another thing to keep in mind is that these setters are called by Guice itself, immediately after the constructor has been executed completely. So when you use this object,
the object that you get finally is the fully-formed object. At no point do you have to call the setters manually yourself in order to set these dependencies on the object.
The framework does that for you automatically like magic. Of course, nothing is stopping you from calling the setters yourself if you want to and set in a different object
for these dependencies, but you do not HAVE to. And that is the important thing to keep in mind. All of the injection is happening behind the scenes, without you having to
do things like:
Amplifier amp = new Amplifier(); amp.setGuitar(new Guitar()); amp.setSpeaker(new Speaker())
etc. before having to use the amp object.
Expand Gist
- But one nice thing is that you do not need the setter naming convention in Guice, so you can rewrite this class in a more compact way.
Expand Gist
- Notice that this new setter now looks very much like a constructor, as in, it does not return anything, it accepts a bunch of dependencies, and it is called only once when
the object is created by the injector.
- Guice does not place any restrictions on the name or visibility of setters either. So you can hide them from accidental misuse. In the above example, you could have
declared the set up method as
@Inject private void setUp(...)
, and the code would STILL have worked. This is because Guice takes use of reflection, and
reflection can do very weird things. But is this a good thing to do, declaring methods as
private
and using
@Inject
on top of that? I don't know.
3) Interface Injection
:
- Interface injection is the same as setter injection except that each setter is housed in its own interface. This design is not commonly used. However, some of the ideas
behind interface injection form the basis of important design patterns and solutions to several knotty design problems.
4) Method Decoration
:
- When they are called, these methods return an injector-provided value instead of their normal value. This process is called
decorating the method and gets its name
from the Decorator pattern. It is useful if you want to make a Factory out of an
arbitrary method on your object.
- The problem that we are trying to solve is this. We want the
Candy class to be wired and created by the injector. Hence, the way that we are manually creating and
returning the object is not sufficient.
Expand Gist
- An alternative would be to have the
Sugar dependency injected into the
Dispenser class, and use that to create the
Candy class. But this is also not
optimal because we are reusing the SAME
Sugar instance for every new
Candy that we are creating. What we want is a
new Sugar for a
new
Candy. Besides,
Dispenser must know how to construct
Candy and consequently is
tightly coupled to its internals. Also,
Dispenser is now
cluttered with the dependencies of
Candy.
Expand Gist
- Method Decoration gets around this problem in the following way. First we provide a
dummy implementation of
dispense in the code.
Expand Gist
- Then the actual impl of the
Candy class is done in the XML files as follows. Read more about this approach
here on SO. What we have basically ended up doing is
injecting a prototype-scoped bean (
Candy) into a singleton-scoped bean (
Dispenser).
Expand Gist
5) Field Injection
:
- You can also wire dependencies directly into the fields.
Expand Gist
- Annotating fields
barley and
yeast with
@Inject
tells the injector to wire dependencies directly to them when
BrewingVat is requested.
This happens after the class's constructor has completed and before any annotated setters are called for setter injection.
- Without the ability to set dependencies (whether mocked or real) for testing, unit tests cannot be relied on to indicate the validity of code. It is also not possible to
declare field-injected fields immutable, since they are set post construction. So, while field injection is nice and compact (and often good for examples), it has little
worth in practice.
Choosing an Injection Idiom
- We are going to be comparing the constructor injection and the setter injection idioms as they are the ones that are the most frequently used.
1) Constructor vs Setter Injection
:
- The merits and demerits of using either constructor or setter injection are the same as those for setting
fields by either constructor or setters.
- An important practice regards the
state of fields once set. In most cases we want fields to be set once (at the time the object is created) and never modified again.
This means not only that the dependent can rely on its dependencies throughout its life but also that they are ready for use right away. Such fields are said to be
immutable. The basic idea is that once the dependency object graph has been created, the graph is fixed, and no changes can be done to the graph whatsoever.
- If you think of private member variables as a
spatial form of encapsulation, then immutability is a form of
temporal encapsulation, that is, unchanging with
respect to time. With respect to dependency injection, this can be thought of as
freezing a completely formed object graph. Constructor injection affords us the
ability to create immutable dependencies by declaring fields as
final
.
- Immutability is essential in application programming. Unfortunately, it is not available using setter injection. Because setter methods are no different from ordinary
methods, a compiler can make no guarantees about the one-call-per-instance restriction that is needed to ensure field immutability.
- A second advantage to using constructor injection is that the object becomes fully available for use immediately. I.e. if a dependency were not provided, either in the
configuration, or in a unit test, a compiler error would have alerted us to a misconfiguration immediately. Contrast this with a setter injection. Suppose you forgot to add
@Inject
on a setter, you wouldn't realize that you missed it unless you tried to execute some method on the injected object, ultimately resulting in a NPE
(because the dependency was never set by the injector). Similarly, if you were writing tests for a class using constructor injection, you would just have to call the
constructor, and would be enough for the
setUp()
. But in the case of using setter injection, you need to call the constructor, and then separately have to call
each of the setters on the object to set the required dependencies before being able to run any of the tests. Not cool.
- One problem with the constructor injection though is that if your object requires multiple dependencies which are of the same type, it is easy to get the order of the
dependencies mixed up and end up with a malformed object. The error goes
completely undetected. Imagine a case where you have like 8 string args in a constructor.
Compare this to if we were using setter injection, and it would have been very easy to give meaningful names to the setters, thus minimizing any chances of such errors.
2) The Constructor Pyramid Problem
:
- When you have different object graph permutations of the same object, the number of constructors can get out of hand, which becomes confusing when it comes to deciding
which constructor to make use of.
- Consider the following
Amphibian class:
Expand Gist
- Here we require two mutually exclusive constructors that match either profile but not both. Were you to require more such profiles, there would be more constructors, one
for each case. All these constructors differ by is the type of argument they take; unlike setters, they can't have different names. This makes them hard to read and to
distinguish from one another. Where an object requires only a partial set of dependencies, additional, smaller constructors need to be written, further adding to the
confusion. This issue is called the
constructor pyramid problem, because the collection of constructors resembles a rising pyramid. On the other hand, with setters,
you can wire any permutation of an object's dependencies without writing any additional code.
- If you find yourself encountering the pyramid problem often, you should ask serious questions about your design befor pronouncing setter injection as a mitigant.
3) The Circular Reference Problem
:
- The problem becomes when you are trying to create an object graph that looks like this. The constructor for a
Symbiote requires a
Host, and the constructor
for
Host requires a
Symbiote. Both of them refer to the
same instances of each other. Syntactically, there is no conceivable way to construct these
objects so that circularity is satisfied with constructor wiring alone. If you decide to construct
Host first, it requires a
Symbiote as dependency to be valid.
So you are forced to construct
Symbiote first; however, the same issue resides with
Symbiote.
- This classic chicken-and-egg scenario is called the
circular reference problem. The triangle one is more commonly seen in the wild.
- There are two possible solutions to this, using setter or constructor injection.
This link on
baeldung explains how to solve this issue in the case of Spring.
- Refer
this page on Guice docs to read about cyclical dependencies and how to get around them.
This SO answer goes into detail about how Guice works to
resolve circular dependency issues.
This answer on SO explains
the scenarios in which Spring creates proxies.
Spring - using setter injection to solve the issue
:
- The setter injection solution is to just switch to setter injection. When the injector starts up, both host and symbiote object graphs are constructed via their nullary
(zero-argument) constructors and then wired by setter to reference each other. The drawback of this approach is that the dependencies can now no longer be declared as
final
.
Expand Gist
- And the XML Configuration file would look like this:
Expand Gist
Spring - using constructor injection to solve the issue
:
- Solving this by using constructor injection requires us to use a
proxy object. First we decouple the
Host and the
Symbiote so that they use interfaces.
The concrete classes
HostImpl and
SymbioteImpl now have dependency on the interfaces instead of the concrete classes. We have introduced a new class
HostProxy that implements the same interface that the
HostImpl implements. Unlike
HostImpl,
HostProxy has a zero-dependency constructor. This
means we can now carry out the following sequence of steps in order to form the object graph:
a) Construct
HostProxy
b) Construct
SymbioteImpl using the instance of
HostProxy created in step 1
c) Construct
HostImpl using the
SymbioteImpl created in step 2
d) Call the setter
setDelegate on
HostProxy by passing in the
HostImpl created in step 3
- (From the gang of four book) Delegation is basically defined as follows: Delegation is a way of making composition as powerful for reuse as inheritance. In delegation, two
objects are involved in handling a request: a receiving object delegates operations to its
delegate. This is analogous to subclasses deferring requests to parent
classes.
- For example, instead of making class Window a subclass of Rectangle (because windows happen to be rectangular), the Window class might reuse the behavior of
Rectangle by keeping a Rectangle instance variable and
delegating Rectangle-specific behavior to it. In other words, instead of a Window
being a Rectangle, it
would
have a Rectangle. Window must now forward requests to its Rectangle instance explicitly, whereas before it would have inherited those operations.
- This is the same concept that we are also using over here. The
HostProxy is supposed to simply delegate the requests that it gets for performing operations on the
Host object to the actual implementation of the
Host type (
HostImpl) that it has saved.
- Question: Well even though the
Host hostProxy field on
SymbioteImpl has been marked as
final
, the
setDelegate
method allows you
to change the underlying reference of
Host as and when you like. So where is the immutability that was promised? Hmm?
- This is what the code looks like when we are using Constructor Injection in order to solve the circular dependency issue:
Expand Gist
- And this is what the XML Config corresponding to the code looks like:
Expand Gist
Spring - using annotations to solve the issue
:
- You can get Spring to create the proxies automatically by using the
@Lazy
annotation.
- This is what the code looks like in this scenario:
Expand Gist
- Sice we are using annotations, the XML files are going to look pretty vanilla.
Expand Gist
Guice - using annotations to solve the issue
:
- In the case of Guice, all you need to do is extract the classes to an interface of its own (a step that we have already done) and then bind the classes to their resp.
implementation in the Guice module. The Guice injector automatically provides the proxy in the middle. We can also trust the injector to work out the correct order of
construction. We don't deal directly with proxies or setter injection or any other infrastructure concern.
Expand Gist
- And the module itself looks something like this:
Expand Gist
4) The In-Construction Problem
:
- One close relative of the circular reference problem that we saw above is where initialization logic requires a dependency that is not yet ready for use. What this
typically means is that a circular reference was solved with a proxy, which has not yet been wired with its delegate.
- This example code is building up on the example that we discussed in the circular dependency problem. Since Host has been proxied but does not yet hold a valid reference to
its delegate, it has no way of passing through calls to the dependency. This is the in-construction problem in a nutshell. The only recourse in such cases is setter
injection.
- Example code here is in Spring using the same config xml file that we used above.
Expand Gist
- The above problem can be solved by just using setter injection instead of constructor injection as follows:
Expand Gist
- But even the setter injection cannot solve this problem if instead of just the symbiote making use of the host, we now have the host making use of the symbiote as well.
Idk man. This seems to be working for some reason. I have no idea what is going on. This code is present
here on Github. Figure out what is happening I
guess...
5) Constructor Injection and Object Validity
:
- If you use setter injection, you require a lot of upfront work in order to make sure that all the dependencies have been injected properly. For instance, suppose we were
trying to set up Spring to wire in the following objects:
Expand Gist
- The XML file would look like this:
Expand Gist
- We would have to make sure of a bunch of things:
a) We have to remember to set both of the setters manually by using the
property
tags.
b) We have an
init method defined in the class, that needs to be manually set up as well by using the
init-method
tag.
c) Objects in Spring are Singletons by default. This means that other multiple threads are concurrently accessing
unsafeObject. While the injector is itself safe to
concurrent accesses of this sort,
UnsafeObject may not be. The Java Memory Model makes no guarantees about the visibility of non-
final
,
non-
volatile
fields across threads. To fix this, you can remove the concept of shared visibility, so an instance is exposed to only a single thread. We do this
by scoping each bean to a
prototype
. This tells the injector to create a new instance of
UnsafeObject each time it is needed. Now there are no concurrent
accesses to
UnsafeObject. Notice that you are forced to mark both dependencies with
scope="prototype"
as well. Otherwise they may be shared
underneath.
- You were also confused about the order in which the methods would be called:
Inside the UnsafeObject constructor ->
Inside the constructor for Shady ->
Inside the constructor for Slippery ->
Inside the setShady method ->
Inside the setSlippery method ->
Inside the init method (init method is called only after the setter dependencies have been set)
- We can solve all of these issues in one go by simply using Constructor Injection. There is only one way to construct it, since it exposes only the one constructor.
Both dependencies are declared
final
, making them safely published and thus guaranteed to be visible to all threads. Initialization is done at the end of the
constructor. And
init() is now private, so it can't accidentally be called after construction.
Expand Gist
The Reinjection Problem
- This problem is about injecting an object that has already been injected once earlier. Reinjection is typical in cases where you have a long-lived dependent with
short-lived dependencies. For instance, consider an example of a dialog box that opens up when a button is clicked and accepts some user text. Every time that the user clicks
on the button, you would want a new dialog box to be created, so that the text that was input the previous time does not show.
- Consider the following example.
Granny is given one apple when she comes into existence. Upon being told to
eat(), she consumes that apple but is still
hungry. The apple cannot be consumed again since it is already gone. In other words, the
state of the dependency has changed and it is no longer usable.
Apple
is short-lived, while
Granny is long-lived, and on each use (every time she eats), she needs a new
Apple.
Expand Gist
- One solution to the problem would be the
method decoration solution that we saw above. There we were also returning a prototype-scoped
bean from a singleton-scoped bean. We could use the same thing here as follows. But this belies our original object graph (of
Granny depending on an
Apple) and
more importantly is difficult to test. One alternative to this solution is using the
Provider
pattern that we are going to look at next.
Expand Gist
Reinjection with the Provider Pattern - Using Provider<T>
- Provider pattern is a Factory that the injector creates, wires, and manages. It contains a single method, which provides new instances. It is particularly useful
for solving the reinjection problem, since
a dependent can be wired with a Provider rather than the dependency itself and can obtain instances from it as necessary.
Using Provider pattern with Guice
:
- This is an example of how we would set the Provider pattern up. Note that no injector configuration apart from this was required in order to set this up. Guice is clever
enough to work out how to give you an
Apple provider.
Expand Gist
- There a couple of things that we can say about the Providers:
a) A Provider is unique (and specific) to the type of object it provides. This means you don't do any downcasting on a
get()
as you might do with a Service
Locator.
b) A Provider's only purpose is to provide scoped instances of a dependency. Therefore, it has just the one method,
get()
. Guice provides Providers out of the
box for all bound types.
- This is what the
Provider
interface in Guice looks like. Method
get()
is fairly straightforward: it says get me an instance of
T
. This may or may not be a new instance depending on the scope of
T
. For example, if the key is bound as a singleton, the same instance is returned
every time.
Expand Gist
Using Provider pattern with Spring
:
- Spring does not have a Provider class out of the box. But we can create a Provider of our own and use it pretty much in the same way that we did with Guice.
Expand Gist
- And the XML Config will look something like this. Note that we also had to declare the
beans.apple as
prototype
in order for a new instance of
Apple to be returned everytime. Note that for the
AppleProvider class, we did not have to explicitly tell Spring to wire-in the injector. Spring was able to
look at the
BeanFactoryAware
interface and able to inject it automatically.
BeanFactoryAware
is an example of the Interface Injection that we
looked at above in
Interface Injection section.
Expand Gist
The Contextual Injection Problem - Using Custom Factory and @Assisted
- There are some cases where we might want to do partial injection, i.e. have the injector wire up some dependencies for us and provide some dependencies ourselves (for eg. a
text that has been entered by the user).
-
This SO Link explains Assisted Inject.
- Consider the following example. The
Deliverer instance that we want to create has 3 dependencies. Suppose
EmailService is supposed to be injected by the
injector, whereas the other two dependencies,
NewsLetter and
date need to supplied by the instance's creator.
- In such a case, the standard solution is to write a factory that helps Guice build the objects.
NewsLetterManager needs an instance of
Deliverer in order to
send the newsletter to its list of recipients. We cannot create the
Deliverer object in the way that we have been doing so far because we need to pass in two args to
the object ourselves. We are doing this by creating a factory,
DelivererFactory.
NewsLetterManger makes the use of
DelivererFactoryImpl in order to
obtain an instance of
Deliverer.
Expand Gist
- Instead of creating a Factory ourselves, we can get Guice to create a factory for
Deliverer for us, and then we can just inject the factory whenever we need an
instance of
Deliverer. This is what the UML looks like in the case when we do use Annotation. The
Deliverer class is going to contain the
Factory
interface. The params passed to the factory interface will be the same params that we want to pass in manually. The corresponding args on the
Deliverer constructor
itself will have to be annotated with
@Assisted
and the constructor will have to be annotated with
@Inject
. This
Factory will have to be
injected into the
NewsLetterManager constructor because that is where we are originally making use of the
Deliverer class. We call
create there, which
goes to the
Deliverer.Factory interface, from where Guice automatically creates an implementation of the Factory for us. This is by telling Guice to do so in the
GuiceModule class. Guice creates the implementation of the
Factory and returns back a fully-formed
Deliverer object for us.
- So much talking. Does it even help clear things up? This is what the code looks like.
Expand Gist
Using @Assisted
with multiple args that are of the same type - Using @Named
- Suppose in the above example, we needed to pass an additional
String
param to the constructor of the
Deliverer class called
name that would also
have to be provided by the instance's creator (just like the
date param for exmaple). We would then need to add this param to the
Factory interface. But this
won't work. If we try to run the code, we will get an error.
-
This SO post explains the problem and the
solution.
Java Doc for
FactoryModuleBuilder
.
- You get the following error when you do this:
String annotated with @Assisted(value="") was bound multiple times
. Learn more at
this link.
- The types of the factory method's parameters must be distinct. To use multiple parameters of the same type, use a named
@Assisted
annotation to disambiguate
the parameters. The names must be applied to the factory method's parameters and to the concrete type's constructor parameters. Hence the class would now look something like
this. The places where we changed the code are commented with // changed here.
Expand Gist
Similar functionality in Spring as @Assisted
?
@Provides
annotation vs the Provider
class
Flexible partial injection using the Builder Pattern
- // TODO:
- Refer the Gang of Four book for the Builder pattern. Refer the text for this.
Injecting objects in sealed code
- // TODO:
- Goes over how you can use the Adapter Pattern to inject dependencies into third-party code??
Understanding the role of an object
- An object is:
a) A logical grouping of data and related operations
b) An instance of a class of things
c) A component with specific responsibilities
- An object is all these things, but it is also a building block for programs. And as such, the design of objects is paramount to the design of programs themselves. Classes
should have a specific, well-defined purpose and stay within clearly defined boundaries. Each object has its area of concern. Good design keeps those concerns separated.
Separation of concerns
- Earlier we laid down the principle: separating infrastructure from application logic so that logic that dealt with construction, organization, and bootstrapping was housed
independently from core business logic. If you think of infrastructure as being orthogonal to application logic, then separating the two can be seen as dividing horizontally.
- Similarly, logic dealing with different business areas can be separated vertically. Checking spelling and editing text are two core parts of any email application. Both
deal with application logic and are thus focused on business purpose. However, neither is especially related to the other. In other words, separating logic by area of concern
is a good practice.
Perils of tight coupling
- Recall the very first incarnation of
Emailer that we wrote:
Expand Gist
- This email service was poorly written because it encapsulated not only its dependencies but also the
creation of its dependencies. Following are the problems with
this:
a) It prevents any external agent (like the injector) from reaching its dependencies. By preventing any external agent from reaching its dependencies, not only does it
prevent an injector from creating and wiring them, it prevents a unit test from substituting mock objects in their place.
b) It allows for only one
particular structure of its object graph (created in its constructor). By deciding its own dependencies, Emailer prevents any variant of
them from being created. This makes for poor reuse and a potential explosion of similar code, since a new kind of Emailer would have to be written for each permutation
c) It is forced to know how to construct and assemble its dependencies.
d) It is forced to know how to construct and assemble dependencies of its dependencies, and so on ad infinitum.
- When a dependent is inextricably bound to its dependencies, code is no longer testable. This, in a nutshell, is
tight coupling. Tight coupling also means that we
have opened the door in the iron wall separating infrastructure from application logic - something that should raise alarm bells on its own.
-
What is the difference between loose
coupling and tight coupling in the object oriented paradigm? on SO.
-
What is the point of having every
service class have an interface? on SE.
Programming to a contract
- An example of a class with tight coupling. In the following example, if we wanted to search for the
startsWith in an
ArrayList
instead of a
HashSet
, we would have to change the type of the dependency being passed in into the constructor.
Expand Gist
- An example of implementation of the class with loose coupling. By placing an abstraction between dependent and dependency (the interface
Collection
), the code
loses any awareness of the underlying data structure and interacts with it only through an interface.
Expand Gist
Rebinding Dependencies
- The basic idea is altering dependency bindings at runtime. It refers purely to changing the association of a key to an object graph. This is what enables hot deployment of
changes. But there are multiple pitfalls that need to be kept in mind:
a) Once bound, a key provides instances of its related object until rebound.
b) When you rebind a key, all objects already referencing the old binding retain the old instance(s).
c) Rebinding is very closely tied to scope. A longer-lived object holding an instance of a key that has been rebound will need to be reinjected or discarded altogether.
d) Rebinding is also tied to lifecycle. When significant dependencies are changed, relevant modules may need to be notified.
e) Not all injectors support rebinding, but there are alternative design patterns in such cases.
- It encapsulates any infrastructure logic and can be used in place of the original dependency with no impact to client code. A change in the binding is signaled to the
adapter(s) via a rebinding notification, and the adapter shifts to the new binding. While this is a robust and workable solution, it is fraught with potential pitfalls and
should be weighed carefully before being embarked upon.
- There is a difference between hot-deploy and hot-swap as mentioned in
this blog post.
This SO answer goes into details about how hot-deploy is done.
-
This is a SO answer about how hot-swap works.
Rebinding Dependencies - The Simple Way
- Here we are considering a case where we are using an injector that does not support dynamic rebinding. The problem that we are trying to solve is that there isn't enough
knowledge while coding to bind all the dependencies appropriately. In other words, the structure of the object graph might change over the course of the life of the
application.
- One very simple solution is to maintain both object graphs and simply flip the switch when you need to move from one object graph to another. Here the method
rebind() controls which dependency
LongLived uses. At some stage in its life, when the rebinding is called for, you need to make a call to
rebind(). The
problem with this approach is that we end up mixing infrastructure logic with our application logic.
Expand Gist
- What's really needed is an abstraction, an intermediary to which we can move the rebinding logic and still remain totally transparent to clients. We can achieve this by
using the adapter pattern. An
adapter is supposed to inherit its
adaptee.
Adapter Pattern
- Adapter pattern is used to convert the interface of a class into one that the client is expecting. We achieve this by wrapping the object that has an incompatible interface
with an object that implements the correct one.
- // TODO: Add text in detail about what Adapter Pattern is, and how it actually works.
-
Adapter pattern on Baeldung
-
Adapter pattern on Refactoring
Rebinding Dependencies - Adapter Pattern
- This is how you would use the Adapter Pattern to achieve the same result:
Expand Gist
- And the Guice module would look something like this. Note that we had to declare the
DependencyAdapter as a
Singleton
. This was to make sure that the
same
DependencyAdapter that is being injected into the
LongLived class is the same one on which we are calling the
rebind() method in the test class.
If you remove the
Singleton
declaration, then the
rebind is called on a different instance of
DependencyAdapter, and hence both calls of the
compute() method end up being called on the
DependencyA instance.
Expand Gist
- Now most of the infrastructure code has been moved to
DependencyAdapter. When the rebinding occurs, flag useA is set to false and the adapter changes, now delegating
calls to
DependencyB instead.
- One interesting feature of this is the use of a
Rebindable role interface. The control logic for dynamic rebinding is thus itself decoupled from the "rebinding"
adapters. All it needs to do is maintain a list of
Rebindables, go through them, and signal each one to
rebind() when appropriate. (Question: What?). Rebinding
of the key associated with
Dependency is now completely transparent to
LongLived - and any other client code for that matter.
- Picture, because why not:
What is Scope?
- Scope is a way of specifying a context under which a key refers to the same instance. When the scope context ends, the instance which is bound to the particular key is
discarded, and is no longer injected in other instances. You are basically telling the injector that a particular key is bound to an object only within a specific scope.
- This has some benefits:
a) It lets the injector manage the latent state of your objects
b) It ensures that your services get new instances of dependencies as needed
c) It implicitly separates state by context (for eg. two HTTP requests specifies two different contexts). This means that code working in a context is oblivious to that
context. It is the injector's responsibility to manage these contexts.
d) It reduces the need for state-aware application logic
What is the importance of 'Context' in Scope?
- Consider the following scenario: there are three people in a family. There is one bathroom in which there can be exactly one toothpaste. Each of the three members of the
family need to use the toothpaste. Since the bathroom is shared by all of the family members, all the family members end up using the same instance of the
Toothpaste.
Then in this example, the bathroom is an example of a
context. Ignore the details currently, and just try to think on a high-level. In this case, we would say that the
Toothpaste is
Singleton scoped.
Expand Gist
- In contrast, if each family member had their own bathroom (each with it's own toothpaste), then a
new instance of
Toothpaste would be available to each family
member. In this case, the context under which each object operates is unique (that is, its own bathroom). This is like the
Toothpaste having no scope at all. This is
referred to as
no-scope or
prototype scope.
Expand Gist
- No code changes were done between the above two cases. All we did was change the context.
No-scope/Prototype
- In a sense, no-scope fulfills the functions of scope, as it:
a) Provides new instances transparently
b) Is managed by the injector
c) Associates a service (key) with some context
- The first two points are self evident. But the last one is not. It's not even true. It is difficult to point out what is the scope that no-scope represents.
- Consider the example that we looked at above. We saw that we could alter the scopes of the objects without making any changes to the objects themselves. To be more precise,
the
family.give() sequence looked exactly the same for both the singleton as well as the no-scope setup. In that example, we equated the scope of a
toothpaste
with a
bathroom context, and said that each instance of
bathroom gets its own instance of
toothpaste. But that is not entirely correct.
- Suppose if Charlie wanted to brush his teeth twice daily. In that case, when we asked for a
toothpaste, he would have received a completely new instance of it. So
now we would have had three instances of
bathrooms, but four instances of
toothpastes.
- This means that no scope cannot be relied on to adhere to any conceptual context. No scope means that every time an injector looks for a given key (one bound under no
scope),
it will construct and wire a new instance.
- You can think of no-scope as a split-second scope where the context is entirely tied to referents of a key.
- This becomes very useful when you are trying to inject dependencies in a long-lived object, like we did in the Granny eating example using Provider Pattern
here. There,
Apple was no-scoped.
- Guice and Spring differ in nomenclature with regard to the no scope. Spring calls it as the
prototype scope, the idea being that a key (and binding) is a kind of
template (or prototype) for creating new objects.
- Recall in
this section in Chapter 3 where we used prototype instances of the objects.
Singleton Scope
- A Singleton's context is the injector itself. The life of a singleton is tied to the life of the injector. Therefore, only one instance of a singleton is ever created per
injector.
- It is important to emphasize this last point, since it is possible for multiple injectors to exist in the same application. In such a scenario, each injector will hold a
different instance of the singleton-scoped object. This is important to understand because many people mistakenly believe that a singleton means one instance for the entire
life of an application. In dependency injection, this is not the case.
Diagram explanation for Singleton vs Prototype Scope
- In the case of a prototype scoped binding, a new instance is returned by the injector everytime that a new object is requested by using its binding. Because of this reason,
prototype does not lend itself to a context.
- On the other hand, in the case of a Singleton scoped binding, we can establish a context quite easily. A singleton's context is the injector itself. As long as the injector
exists, the singleton object will exist as well. There is a small subtlety here that the scope is one singleton per
injector, not per
application. If your
application has multiple injectors,
each of those injectors will hold a separate/distinct reference of the singleton object. Refer the section on Singleton scope
here for details.
How to decide when to use a Singleton
- If a service represents a conceptual nexus for clients, then it is a likely candidate. For instance, a database connection pool is a central port of call for any service
that wants to connect to a database. Thus, connection pools are good candidates for singleton scoping.
- Services that are stateless (in other words, objects that have no dependencies or whose dependencies are immutable) are good candidates.
- In the scenario where there are no states to manage, either of prototype or singleton scopes can be used. Singleton does have some advantages over prototype in these
scenarios:
a) Objects can be created at startup (sometimes called eager instantiation), saving on construction and wiring overhead.
b) There is a single point of reference when stepping through a debugger.
c) Maintaining the lone instance saves on memory (memory complexity is constant as opposed to linear in the case of no scoping)
- Remember the singleton litmus test: Should the object exist for the same duration as the injector?
Singletons in Practice: Keys are bound under scope, not object or class (Spring example)
- The important thing to keep in mind about scoping is that a key is bound under a scope, not an object or class. This is true of any scope, not just the singleton.
- This is how we would create Singleton scoped beans in the case of Spring. Note that we can also use two different bean names for the same class. (Using
camera.basement and
camera.penthouse for the same class
SimpleCamera).
Expand Gist
- And then we can make use of the beans as follows.
camera.basement and
camera.penthouse are declared as prototype, hence we get different instances of the
SimpleCamera class that is associated with the beans. But the
MasterTerminal class that is injected into the
SimpleCamera class is declared as a
Singleton, and hence the instance injected into the two classes is the same.
Expand Gist
- But now, what would happen if we defined the two beans,
camera.basement and
camera.penthouse, as singletons? Note that both beans are supposed to create
instances of the
SAME class, i.e.
SimpleCamera. So would the calls to
getBean return the
same instance of the
SimpleClass?
- We changed the xml so that the two beans are now defined as singletons.
Expand Gist
- And we are running the following test. As you can see, the instance of the
SimpleClass that is returned from the injector are different. Despite the
SimpleClass being associated with a
Singleton key. What does this tell us? This tells us that in the case of an injector, the scope is a concept that is
associated COMPLETELY with the key. Not with the class. Not with the object. But with a key. It is the
KEY that is bound under a scope. If you ask the injector for an
object via it's key, the injector will look at the key, see if it is a singleton or prototype, and based off of that, return either the same object, or a new object. There is
nothing stopping you from having a completely different key for the same class. The injector does not care about the class. If a key is bound to Singleton, injector can allow
multiple instances of the class. But it will allow only ONE instance associated with the key that will be shared with all the dependents.
Expand Gist
- In fact, the same class can also be bound to both Singleton and Prototype. Consider the following xml configuration.
Expand Gist
- And the test class becomes this:
Expand Gist
Singletons in Practice: Keys are bound under scope, not object or class (Guice example)
- The same thing we can do in Guice as well. Consider the simple way first where we have the following Guice Module setup.
Expand Gist
- And the classes making use of the module would look like this.
BasementCamera and
PenthouseCamera require instances of
Camera that is declared as
prototype in the Guice Module. Hence, everytime that we construct a new
BasementCamera and
PenthouseCamera, a new instance of
Camera would be injected
into it.
Expand Gist
- When two instances of prototype key are being injected into the same constructor, they are still different instances. Consider the following case.
BasementFloor
requires two instances of the object bound to the
[Camera, Basement] key. Both the instances will be different since the
[Camera, Basement] key is bound to the
prototype scope.
Expand Gist
- Similarly, in this case all four fields of
Building receive different instances of
Camera even though only two keys are present.
Expand Gist
- This is opposed to the following class,
ControlRoom, which has four fields that refer to the same instance of
MasterTerminal but via four references.
Expand Gist
- But now, (to recreate the example we saw in Spring above), let us consider the following Guice Module setup. We have two keys defined:
[MasterTerminal, RoofMaster]
and
[MasterTerminal, BasementMaster]. Both of these keys will return an instance of
MasterTerminalImpl when injected. Both of these keys are also defined as
Singleton
. This means that the object associated with
each of
[MasterTerminal, RoofMaster] and
[MasterTerminal, BasementMaster] keys will be
unique.
Expand Gist
- Now we make use of the keys in the following test.
[MasterTerminal, RoofMaster] and
[MasterTerminal, BasementMaster] keys are bound to the same class declared
as Singleton. But the instances that are returned by either of the keys are different. Any dependents of
[MasterTerminal, RoofMaster] see the SAME SHARED instance. But
any dependents of
[MasterTerminal, BasementMaster] see a DIFFERENT SHARED instance.
Expand Gist
- To reiterate, it is very important to distinguish that a key, rather than an object, is bound under a scope. Singleton scoping allows many instances of the same class to
exist and be used by dependents; it allows only one shared instance of a key.
- This is different from the conventional "Singleton Design Pattern" that we see being used in books which defines a more absolute singleton in the sense that there can
always be exactly one instance of a class in an application. This is often considered as an anti-pattern.
The Singleton Anti-Pattern
- As mentioned above, there is a difference between
singleton scope, a feature of DI, and
singleton objects (or singleton anti-patterns).
- Singleton objects are shared even between injectors and can cause a real headache if you are trying to separate modules by walling them off in their own injectors.
- Singleton objects bad, singleton scoping good. Singleton objects make testing difficult if not impossible and are antithetical to good design with dependency injection.
Singleton scoping, on the other hand, is a purely injector-driven feature and completely removed from the class in question.
- Singleton scope is thus flexible and grants the usefulness of the Singleton antipattern without all of its nasty problems.
Domain-Specific Scopes - The Web
- Goes over request-scoped objects and session-scoped objects.
- TODO.