Recently, I thought of a simple thought experiment.
With a Servlet
Let’s start with a servlet that just writes out “Hello World”. I’m going to use a pseudo API instead of something like javax.servlet
to facilitate the thought experiment.
public void service(Writer w) {
w.write("Hello World");
}
Simple enough. We can fake-out Writer
(use a mock Writer
or a StubWriter
) and put the service
method under test.
I’ll skip the test code as, regardless of whether you mock or stub, it’ll be pretty simple.
So, now let’s say our code needs access to the query parameters:
public void service(Writer w, Map<String, String> params) {
w.write("Hello World " + params.get("queryParam"));
}
We now have 2 method parameters. Fair enough.
But now let’s say we also want the HTTP headers:
public void service(
Writer w,
Map<String, String> params,
Map<String, String> headers) {
w.write("Hello World " +
params.get("queryParam") +
" using " +
headers.get("User-Agent"));
}
We have 3 method parameters. Still fine.
But then next we need the session:
public void service(
Writer w,
Map<String, String> params,
Map<String, String> headers,
Map<String, String> session) {
w.write("Hello World " +
params.get("queryParam") +
" using " +
headers.get("User-Agent") +
session.get("username"));
}
Oh, and now we also want to set the response Content-Type header:
public void service(
Writer w,
Map<String, String> params,
Map<String, String> headers,
Map<String, String> session,
Map<String, String> outHeaders) {
outHeaders.put("Content-Type", "text/html");
w.write("Hello World " +
params.get("queryParam") +
" using " +
headers.get("User-Agent") +
session.get("username"));
}
Eesh. Now we’re at 5 method parameters.
How about one last thing—so far our service
method has been stateless, which is fine, that’s how servlets work.
But, illustration purposes, let’s pretend our “servlet” component it actually stateful (e.g. one is created per-request):
public class Servlet {
private final Writer w;
private final Map<String, String> params;
private final Map<String, String> headers;
private final Map<String, String> session;
private final Map<String, String> outHeaders;
public Servlet(
final Writer w,
final Map<String, String> params,
final Map<String, String> headers,
final Map<String, String> session,
final Map<String, String> outHeaders) {
this.w = w;
this.params = params;
this.headers = headers;
this.session = session;
this.outHeaders = outHeaders;
}
public void service() {
outHeaders.put("Content-Type", "text/html");
w.write("Hello World " +
params.get("queryParam") +
" using " +
headers.get("User-Agent") +
session.get("username"));
}
}
At this point, passing around the parameter list is getting out of hand.
Which is fine, we can apply a well known refactoring, Introduce Parameter Object, and make Request
and Response
interfaces:
public interface Request {
Map<String, String> getParams();
Map<String, String> getHeaders();
Map<String, String> getSession();
}
public interface Response {
setHeader(String name, String value);
Writer getWriter();
}
And now have our Servlet
class use them:
public class Servlet {
private final Request request;
private final Response response;
public Servlet(Request request, Response response) {
this.request = request;
this.response = response;
}
public void service() {
request.setHeader("Content-Type", "text/html");
response.getWriter().write(
"Hello World" +
request.getParams().get("queryParam") +
" using " +
request.getHeaders().get("User-Agent") +
request.getSession().get("username"));
}
}
This looks a lot better.
We introduced a Request
abstraction that groups request-lifecycle attributes, and so instead of passing around lots of related parameters, we can pass around Request
and Response
instances.
So far, none of this should be new or different, because HTTP libraries in basically any OO language look this way.
Servlet Testing
After introducing the Request
interface, testing the Servlet
class involves a level of indirection.
Previously we could pass fake Writer
, Map
, etc. instances directly as parameters to our service
method—now we have to wrap a Request
around the test Writer
, Map
, etc. and pass the Request
instead.
If you’re mocking, you just mock out the parts of Request
you need:
private Request request = mock(Request.class);
private Response response = mock(Response.class);
public void testServletOne() {
// ServletOne only needs Writer
when(response.getWriter()).thenReturn(mockWriter);
servlet = new ServletOne(request, response);
// ...rest of test...
}
public void testServletTwo() {
// ServletTwo needs headers and Writer
when(request.getHeaders()).thenReturn(mockHeaders);
when(response.getWriter()).thenReturn(mockWriter);
servlet = new ServletTwo(request, response);
// ..rest of test ...
}
Personally, I prefer stubbing in this scenario, and would use something akin Spring’s misnamed MockHttpServletRequest (it’s really a stub):
private Request request = new StubRequest();
private Response response = new StubResponse();
public void testServletOne() {
servlet = new ServletOne(request, response);
// ...rest of test...
}
public void testServletTwo() {
servlet = new ServletTwo(request, response);
// ...rest of test ...
}
But, which ever style you prefer, this extra level of mocking/stubbing in tests is pretty standard for Request
interfaces.
Thoughts on Servlet Refactoring
So far, whether you prefer a stateful or stateless Servlet
class aside, I think this has been a pretty standard “best practice” refactoring.
But let’s be pedantic and focus on the API change of our constructor (using the stateful Servlet
class), both pre- and post-Request
refactoring:
// before
public Servlet(
final Writer w,
final Map<String, String> params,
final Map<String, String> headers,
final Map<String, String> session,
final Map<String, String> outHeaders) {
this.w = w;
this.params = params;
this.headers = headers;
this.session = session;
this.outHeaders = outHeaders;
}
// after
public Servlet(
final Request request,
final Response response) {
this.request = request;
this.response = response;
}
What are the pros and cons of the change?
- Pro: less parameters to pass around
- Pro: more decoupled—whoever instantiates our
Servlet
does not have to know exactly which parts of the request ourservice
method is going to use - Con: less explicit—previously we could see exactly what parts of the request we needed; if we didn’t need the
session
map, we could leave it out of our constructor, and it is immediately apparent to callers
Personally, I think the pros outweighs the cons here. And given that every servlet-type API I’ve seen lately has a Request
interface, I think its fair to say this is a well-accepted, non-controversial refactoring.
You might take a moment to note to yourself whether you do/do not agree with this refactoring, just for the purpose of the thought experiment.
Now with Services
Instead of a servlet, let’s now write a service, some sort of stateless “bean” (it’s 2014, I really should stop using that word) in your application that needs application-scoped dependencies:
public class Service {
private final EmailSender emailSender;
private final FooDao fooDao;
private final BlahService blahService
public Service(
final EmailSender emailSender,
final FooDao fooDao,
final BlahService blahService) {
this.emailSender = emailSender;
this.fooDao = fooDao;
this.blahService = blahService;
}
// ...service methods...
}
I’ve made up EmailSender
, FooDao
, and BlahService
as dependencies of Service
—what the actual dependencies are isn’t important. The main point is that they are all application-scoped dependencies.
Do you notice how similar this Service
code looks to the pre-Request
Servlet
code?
Most people look at the Service
constructor and think there’s no way they want to instantiate by hand all 10-50-whatever services they have in their project when the constructor is going to vary so widely from service to service.
So, in steps auto-wiring dependency injection (Spring, Guice, whatever) as the hero to solve this problem. They give up strongly-typed new Service
calls and say “here, let me use reflection to call all these nasty constructors for you”.
Which makes sense if you’re forcing yourself to stick with the Service(EmailSender, FooDao, BlahService)
constructor.
But what if we apply the same refactoring that we just applied to Servlet
? We applied Introduce Parameter Object and made a Request
interface—let’s apply it here and make an AppContext
interface:
public interface AppContext {
EmailSender getEmailSender();
FooDao getFooDao();
BlahService getBlahService();
}
And now let’s use it:
// style 1
public class Service {
private final AppContext appContext;
public Service(final AppContext appContext) {
this.appContext=appContext;
}
// service method calls appContext.getBlahService().xxx();
}
// style 2
public class Service {
private final EmailSender emailSender;
private final FooDao fooDao;
private final BlahService blahService
public Service(final AppContext appContext) {
this.emailSender = appContext.getEmailSender();
this.fooDao = appContext.getFooDao();
this.blahService = appContext.getBlahService();
}
// service method calls blahService.xxx();
}
Whichever style you prefer, we’ve drastically simplified our constructor—instead of saying “I need X, Y, Z, …”, it’s “I need some of my application’s app-scoped dependencies”.
Service Testing
Much like Servlet Testing earlier, adding an AppContext
adds a level of indirection to our tests that we must mock/stub out.
You can either mock:
private AppContext appContext = mock(AppContext.class);
public void testServiceOne() {
// ServiceOne only needs FooDao
when(appContext.getFooDao()).thenReturn(mockFooDao);
service = new ServiceOne(appContext);
// ...rest of test...
}
public void testServiceTwo() {
// ServiceTwo needs needs FooDao and EmailSender
when(appContext.getFooDao()).thenReturn(mockFooDao);
when(appContext.getEmailSender()).thenReturn(mockEmailSender);
service = new ServiceTwo(appContext);
// ...rest of test ...
}
Or, stub:
private StubAppContext appContext = new StubAppContext();
public void testServiceOne() {
service = new ServiceOne(appContext);
// ...rest of test...
}
public void testServiceTwo() {
service = new ServiceTwo(appContext);
// ...rest of test ...
}
Thoughts on Service Refactoring
Okay, so what are the pros/cons of the AppContext
approach for Service
?
To me, they are the same as the pros/cons of the Servlet
refactoring:
- Pro: less parameters to pass around
- Pro: more decoupled—whoever instantiates our
Service
does not have to know exactly which parts of the application context we need - Con: less explicit—less obvious what our
Service
’s application-scoped dependencies are
Either way, our instantiation of Service
is now trivial. We can just call new Service(appContext)
.
I assert that our constructor is now simple enough that mere mortals can instantiate services—we don’t need Spring, Guice, or anything fancy to call new
for us.
We still have Dependency Injection:
- The
AppContext
methods return interfaces whose implementation is determined at runtime - No statics are involved that would complicate testing
- No
XxxFactory
classes are involved (which is a popular strawman in auto-wiring DI tutorials)
And, most importantly, it’s simple. Anyone who understands interfaces can understand AppContext
. And not just how to use it, but how it actually works. There is no magic.
I think this is a stark contrast to auto-wiring DI, where only a handful of experts on a project truly understand what is happening behind the scenes. The rest of the team, or even entire teams if they lack a true expert, cargo cult their way through auto-wiring DI because it’s best practice.
Are Request
and AppContext
that Different?
So, the Request
object groups request-related/request-scoped parameters.
The AppContext
object groups application-related/application-scoped services.
My assertion is that they are based on the same underlying principles.
Which is why, when debating pros/cons of an AppContext
approach vs. an auto-wiring approach, I find it frustrating when the admitted cons of AppContext
come up (like less explicit which services are actually being used).
I will not disagree that they are not cons, but I feel they are generally outweighed by the pros (simple, type-safe, no need to rely on a framework), that, in other “Introduce Parameter Object” instances (like Request, etc.), are taken as perfectly fine and a worthy trade-off.
But something about these parameters being “dependencies” (usually application-scoped, but also custom-scoped, given frameworks support that), means developers end up treating/thinking of them differently. I’m not sure why.
Personally, I’ve been using this AppContext
style for quite awhile in various codebases, since I wrote Changing my style (where I referred to it as a registry), and it is still working well. (Note that I make no claim for originality.)
Perhaps there are cases where an AppContext
approach becomes very degenerate, say with hundreds of services. Hundreds of services would be awkward, but I would be tempted at that point to apply the GooS philosophy that the awkwardness is pointing out a real smell (a huge codebase with many unstructured dependencies), and not just sweep it under the rug by throwing a framework at it.
But, even if it does become degenerate in some scenarios (extremely large system/something/something), I can somewhat selfishly assert that, that’s fine, those are not my scenarios.
(And vice versa, if other developers assert that an AppContext
approach would be degenerate for their demonstrably different scenarios, and auto-wiring DI is awesome, I’m willing to trust them on on that.)
That said, I suppose I will go a bit further and assert that most systems that are using auto-wiring DI because it’s “best practice”, would likely be just fine, and in fact easier/simpler to understand, with a “standard OO” solution to the problem.
As a final note, this was sitting in my “drafts” directory for ages, so I’m somewhat past the whole DI/AppContext debate; I’m not sure why, perhaps because I’ve been away from any DI-using systems for awhile now. Definitely at Bizo, but also in the wider Scala community, it seems that Scala systems have eschewed adopting auto-wiring DI as idiomatic.
But I thought I’d still go ahead and publish it, if anything so that I can have a link to point people to in the future.