(the other title: Your Application has a Wiring Problem)
In My main() Method Is Better Than Yours we looked into what a main() method should look like. There we introduced a clear separation between (1) the responsibility of constructing the object graph and (2) the responsibility of running the application. The reason that this separation is important was outlined in How to Think About the "new" Operator. So let us look at where have all of the new operators gone...
Before we go further I want you to visualize your application in your mind. Think of the components of your application as physical boxes which need to be wired together to work. The wires are the references one component has to another. In an ideal application you can change the behavior of the application just by wiring the components differently. For example instead of instantiating LDAPAuthenticator you instantiate KerberosAuthenticator and you wire the KerberosAuthenticator to appropriate components which need to know about Authenticator. That is the basic idea. By removing the new operators from the application logic you have separated the responsibility of wiring the components from the application logic, and this is highly desirable. So now the problem becomes, where have all the new operators gone?
First lets look at a manual wiring process. In the main() method we asked the ServerFactory to build us a Server (in our case a Jetty Web Server) Now, server needs to be wired together with servlets. The servlets, in turn, need to be wired with their services and so on. Notice that the factory bellow is full of "new" operators. We are new-ing the components and we are passing the references of one component to another to create the wiring. This is the instantiation and wiring activity I asked you to visualize above. (Full source):
public Server buildServer() {
Server server = new Server();
SocketConnector socketConnector
= new SocketConnector();
socketConnector.setPort(8080);
server.addConnector(socketConnector);
new ServletBuilder(server)
.addServlet("/calc", new CalculatorServlet(
new Calculator()))
.addServlet("/time", new TimeServlet(
new Provider() {
public Date get() {
return new Date();
}
}));
return server;
}
When I first suggest to people that application logic should not instantiate its own dependencies, I get two common objections which are myths:
- "So now each class needs a factory, therefore I have twice as many classes!" Heavens No! Notice how our ServerFactory acted as a factory for many different classes. Looking at it I counted 7 or so classes which we instantiated in order to wire up our application. So it is not true that we have one to one correspondence. In theory you only need one Factory per object lifetime. You need one factory for all long-lived objects (your singletons) and one for all request-lifetime objects and so on. Now in practice we further split those by related concepts. (But that is a discussion for a separate blog article.) The important thing to realize is that: yes, you will have few more classes, but it will be no where close to doubling your load.
- "If each object asks for its dependencies, than I will have to pass those dependencies through all of the callers. This will make it really hard to add new dependencies to the classes." The myth here is that call-graph and instantiation-graph are one and the same. We looked into this myth in Where have all the Singletons Gone. Notice that the Jetty server calls the TimeServlet which calls the Date. If the constructor of Date or TimeServlet all of a sudden needed a new argument it would not effect any of the callers. The only code which would have to change is factory class above. This is because we have isolated the instantiation/wiring problem into this factory class. So in reality this makes it easier to add dependencies not harder.
Now there are few important things to remember. Factories should have no logic! Just instantiation/wiring (so you will probably not have any conditionals or loops). I should be able to call the factory to create a server in a unit test without any access to the file-system, threads or any other expensive CPU or I/O operations. Factory creates the server, but does not run it. The other thing you want to keep in mind is that the wiring process is often controlled by the command line arguments. This makes is so that your application can behave differently depending what you pass in on a command line. The difference in behavior is not conditionals sprinkled throughout your code-base but rather a different way of wiring your application up.
Finally, here are few thoughts on my love/hate of Singletons (mentioned here and here) First a little review of singletons. A singleton with a lower case 's' is a good singleton and simply means a single instance of some class. A Singleton with an upper case 'S' is a design pattern which is a singleton (one instance of some class) with a global "instance" variable which makes it accessible from anywhere. It is the global instance variable which makes it globally accessible , which turns a singleton into a Singleton. So singleton is acceptable, and sometimes very helpful for a design, but Singleton relies on mutable global state, which inhibits testability and makes a brittle, hard to test design. Now notice that our factory created a whole bunch of singletons as in a single instance of something . Also notice how those singletons got explicitly passed into the services that needed them. So if you need a singleton you simply create a single instance of it in the factory and than pass that instance into all of the components which need them. There is no need for the global variable.
For example a common use of Singleton is for a DB connection pool. In our example you would simply instantiate a new DBConnectionPool class in the top-most factory (above) which is responsible for creating the long-lived objects. Now lets say that both CalculatorServlet and TimeServlet would need a connection pool. In that case we would simply pass the same instance of the DBConnectionPool into each of the places where it is needed. Notice we have a singleton (DBConnectionPool) but we don't have any global variables associated with that singleton.
public Server buildServer() {
Server server = new Server();
SocketConnector socketConnector
= new SocketConnector();
socketConnector.setPort(8080);
server.addConnector(socketConnector);
DBConnectionPool pool = new DBConnectionPool();
new ServletBuilder(server)
.addServlet("/calc", new CalculatorServlet(
pool,
new Calculator()))
.addServlet("/time", new TimeServlet(
pool,
new Provider() {
public Date get() {
return new Date();
}
}));
return server;
}
Add to:
0 comments
Post a Comment