[ Team LiB ] |
12.3 Presentation Tier AntipatternsThe Model-View-Controller pattern, covered in Chapter 3, is the fundamental organizing principle for the presentation tier. The building blocks of the MVC pattern create an overall map of the application, making it easier to understand and extend. Preserving the separation of model, view, and controller is essential to maintaining these advantages. It should be no surprise, then, that the presentation tier antipatterns have to do with the breakdown of model-view-controller separation. While these antipatterns are specific to the presentation tier, be aware that other antipatterns, such as Leak Collection and Excessive Layering, can affect the presentation tier as well. 12.3.1 The Magic ServletWhen servlets were first introduced, developers immediately saw the potential of combining the robust Java environment with an efficient mechanism for serving dynamic content. Server-side Java's killer app was JDBC, a powerful and high-level mechanism for communicating with databases. Over time, technologies like JNDI and JMS were added, allowing J2EE to talk easily and directly to a large number of enterprise information systems. Because it was suddenly so easy to talk to databases, directory servers, and messaging systems, many developers were tricked into thinking that their applications would be simple, too. Who needed a complicated design when reading a row from a database was a one-line operation? Unfortunately, as applications grew in scope, complexity crept back in—but the design to handle it did not. The typical symptom of complexity outpacing design is the Magic Servlet antipattern, in which a single servlet handles all aspects of a given request. On the surface, the magic servlet seems like a reasonable encapsulation. It captures all the logic needed to handle a request in a single, convenient class. But a magic servlet is also large, complex, difficult to maintain, and impossible to reuse. Typically, these servlets have a huge amount of code in a single doGet( ) or similar method. Imagine you are given the task of putting an LDAP-based corporate address book on the web. Example 12-2 shows a typical magic servlet solution, using the servlet to read all the names and phone numbers in the LDAP directory. Example 12-2. A magic servletimport java.io.*; import javax.servlet.*; import javax.servlet.http.*; import javax.naming.*; import javax.naming.directory.*; import java.util.*; public class MagicServlet extends HttpServlet { private DirContext peopleContext; // setup LDAP access public void init(ServletConfig config) throws ServletException { super.init(config); Properties env = new Properties( ); env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); env.put(Context.PROVIDER_URL, "ldap://localhost/o=jndiTest"); env.put(Context.SECURITY_PRINCIPAL, "cn=Manager, o=jndiTest"); env.put(Context.SECURITY_CREDENTIALS, "secret"); try { DirContext initalContext = new InitialDirContext(env); peopleContext = (DirContext)initalContext.lookup("ou=people"); } catch (NamingException ne) 2{ ne.printStackTrace( ); throw new UnavailableException("Error inializing LDAP", ne); } } // close LDAP public void destroy( ) { try { peopleContext.close( ); } catch(NamingException ne) { ne.printStackTrace( ); } } // "magic" function is model, view and controller protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // view logic response.setContentType("text/html"); java.io.PrintWriter out = response.getWriter( ); out.println("<html>"); out.println("<head>"); out.println("<title> Servlet</title>"); out.println("</head>"); out.println("<body>"); out.println("<table>");2 // model logic try { NamingEnumeration people = peopleContext.list(""); while(people.hasMore( )) { NameClassPair personName = (NameClassPair)people.next( ); Attributes personAttrs = peopleContext.getAttributes(personName.getName( )); Attribute cn = personAttrs.get("cn"); Attribute sn = personAttrs.get("sn"); Attribute phone = personAttrs.get("telephoneNumber"); out.println("<tr>< td>" + cn.get( ) + " " + sn.get( ) + "</td>" + "<td>" + phone.get( ) + "</td></ tr>"); } } catch(Exception ex) { out.println("Error " + ex + " getting data!"); } // back to view logic out.println("</table>"); out.println("</body>"); out.println("</html>"); } } The doGet( ) method talks directly to the data source, LDAP. It also writes directly to the output. This example is relatively simple, since it doesn't handle input validation or any kind of error handling, for starters. Adding those functions would make the servlet even more complicated and specialized. The problem with a magic servlet is not a lack of encapsulation, but rather improper encapsulation. The first rule of objects is that they should perform a single function well. The scratch test is whether you can describe what an object does in one phrase. For our magic servlet, the best we could say is "it reads phone numbers from a database and formats the results as HTML." Two phrases at least, and awkward ones at that. 12.3.1.1 Refactoring the magic servletTo fix the Magic Servlet antipattern, we need to break our servlet into multiple objects, each with a specific task. Fortunately, we already have a pattern for solving just this type of problem: the Model-View-Controller. MVC is covered extensively in Chapter 3, so we won't go into too much detail here. The salient point is the separation of the interaction into (at least) three pieces. The model is responsible for all interactions with persistent storage. We will create a simple JavaBean, Person, with accessor methods for fields firstName, lastName, and phoneNumber. Then we create a class, LdapPersonCommand, to read from LDAP and expose the values as a collection of class Person. The command implements a generic PersonCommand interface: public interface PersonCommand { // initialize the command public void initialize(HttpSession session) throws NamingException; // execute the query public void runCommand( ); // get the result of the query as a List of class Person public List getPeople( ); } The view, a simple JSP page, uses an instance of class PersonCommand placed in request scope and the Java Standard Tag Libraries to generate a table of names and phone numbers. PersonView.jsp is shown in Example 12-3. Example 12-3. PersonView.jsp<%@page contentType="text/html"%> <%@taglib uri="/jstl/core" prefix="c"%> <html> <head>< title>Addresses</title> </head> <body> <jsp:useBean id="personCommand" scope="request" class="PersonCommand" /> <table> <c:forEach var="person" items="${personCommand.people}" > <tr>< td><c:out value="${person.firstName}"/ ></td> <td><c:out value="${person.lastName}"/></ td> <td><c:out value="${person.phoneNumber}"/></ td> </tr> </c:forEach> </table> </body> </html> As you can see, our JSP is entirely focused on view management. It contains only the minimal amount of JSTL code needed to handle looping and output. All this abstraction leaves us with a very simple servlet, as shown in Example 12-4. Example 12-4. PersonServlet.javaimport javax.servlet.*; import javax.servlet.http.*; import java.IO.* import javax.naming.*; public class PersonServlet extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { try { PersonCommand personCommand = new LdapPersonCommand( ); personCommand.initialize( ); personCommand.runCommand( ); request.setAttribute("personCommand", personCommand); } catch(NamingException ne) { throw new ServletException("Error executing " + "command", ne); } RequestDispatcher dispatch = getServletContext( ).getRequestDispatcher("/PersonView.jsp"); dispatch.forward(request, response); } } This servlet is responsible for coordinating the response but not for performing the dirty work of reading LDAP or generating HTML. The major advantages of this solution are in extensibility. We could easily replace LdapPersonCommand with a class that reads from a database or legacy system. Writing a separate JSP to output to WAP or WML or a different language would also be easy and require no modification of the servlet. All of this should be familiar from previous chapters. The point is that the problem can be solved. You can resolve this antipattern a piece at a time. In this case, you might want to isolate the view first, and come back and separate the model and controller when you have time. 12.3.2 Monolithic/Compound JSPsWhen JSPs were first introduced, some developers recognized their potential to replace the awkwardness of embedding HTML output in a servlet with the convenience of scripting. Many decided that since JSPs were parsed into servlets internally, they were just a new syntax for servlets. So they built servlets—including access to models, control logic, and output—as JSPs. Example 12-5 shows a worst-case JSP design. Example 12-5. LoginPage.jsp<%@page contentType="text/html"%> <%@page import="antipatterns.LoginManager" %> <html> <% LoginManager lm = new LoginManager( ); ServletRequest req = pageContext.getRequest( ); if (!lm.doLogin(req.getParameter("username"), req.getParameter("password"))) { %> <head>< title>Login Page</title> </head> <body> <form action="/LoginPage.jsp" method="post"> User name: <input type="text" name="username"> <br> Password: <input type="password" name="password" ><br> <input type="submit"> </form> </body> <% } else { %> <head>< title>First Page</title> </head> <body> Welcome to the page! <% } %> </body> </html> This example has the same problems as a magic servlet. It performs operations belonging to the model, view, and controller. The example above combines the functionality of JSP and servlets, but represents the worst of both worlds from the point of view of maintenance. The HTML is confusing, often duplicated, and spread throughout the file in an opaque way. The Java code is embedded awkwardly—again, hard to find and follow. There are actually two common antipatterns in Example 12-5. The Monolithic JSP antipattern occurs when JSP pages do more than their fair share of the work, often acting as both view and controller. While this example doesn't interact directly with the model via JNDI or JDBC, it would be a logical extension, further destroying the separation of model and controller. A closely related antipattern is the Compound JSP antipattern, in which multiple JSP files are crammed into one using Java conditionals. In the example above, our code tests the return value of a call to doLogin( ) and displays different pages based on the result. 12.3.2.1 Fixing monolithic and compound JSPsJSP's advantage is its simplicity. As a tag-based language, it should be familiar to web designers who no longer need to understand the complexities of Java development in order to build web pages. It is also meant to integrate easily into existing tools, such as WYSIWYG HTML editors. Monolithic and compound JSPs take away those advantages. It would take a pretty sophisticated editor to separate the two pages contained in Example 12-5. And web designers working with it would have to understand the embedded code, at least enough so as not to break it. Since monolithic JSPs are really just another type of magic servlet, the solution is pretty much the same. The JSP should be refactored into three separate pieces, with a servlet acting as the controller, the JSP as the view and JavaBeans as the model. We won't go through the whole exercise of converting our monolithic JSP to MVC, since the result is so similar to the previous example and those in Chapter 3. Compound JSPs present a slightly different problem, but again a similar solution. In Example 12-5 we effectively have two pages: the login page and the welcome page. We should separate these into two separate JSP pages (in this case, HTML pages would work as well). The selection logic should go in the controller servlet, which contains code like: if (lm.doLogin(username, password)) { nextPage = "/welcome.jsp"; } else { nextPage = "/login.jsp"; } RequestDispatcher dispatch = getServletContext( ).getRequestDispatcher(nextPage); dispatch.forward(request, response); 12.3.3 Overstuffed SessionThe session, which tracks users as they navigate a web site, is a major feature of the J2EE presentation tier. Objects like the user's security permissions can be stored in the session when a user first logs in, and used several pages later to determine what the user can access. While maintaining session information is quite convenient, it is not without its pitfalls. Putting too much data or the wrong kind of data into the session leads to the Overstuffed Session antipattern. The first danger of this antipattern is from data with a short lifespan. Since the session is implemented as a collection, we have to be on the lookout for a variant of the Leak Collection antipattern. Putting objects in the wrong scope, as we have done in Example 12-6, can lead to a pseudo memory leak. Example 12-6. The LeakyServlet's doGet( ) methodprotected void doGet(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
HttpSession session = request.getSession( );
// create the bean
AddressBean address = new AddressBean( );
address.setFirst(request.getParameter("first"));
address.setLast(request.getParameter("last"));
// pass the bean to the view
session.setAttribute("antipatterns.address", address);
// instantiate the view
RequestDispatcher dispatcher =
getServletContext( ).getRequestDispatcher("/View.jsp");
dispatcher.forward(request, response);
}
As the snippet shows, we have placed a bean called AddressBean (which contains state data that is only used in this request) in session scope. This bean, however, will be kept around in the user's session for as long as the user is connected. It's hard to see this kind of scenario as a real memory leak. The user's session expires when they log out, and the memory is reclaimed. But each object adds up, and if lots of users store lots of irrelevant beans in session scope, the total number of users that can connect at any one time will be reduced.[1]
The second danger is from data with a long lifespan. It is very common to store complex state data—such as shopping carts or user preferences—in the user's session, assuming the session will stay alive for as long as the user is using the site. Unfortunately, this is not always the case: if the user takes a break to respond to email or read another web page, or if the web server crashes, the user may unexpectedly lose all the data. This can be a real problem: just ask anyone who has painstakingly selected all the components for his new computer system only to have them disappear while he is looking for his credit card. 12.3.3.1 Unstuffing the sessionFortunately, as far as antipatterns go, fixing an overstuffed session isn't too difficult. For short-lived data, make sure you default to request scope for all your objects, and only use session scope when it is explicitly required. Also, remember that it is possible to remove items from the session if you know they will no longer be used. Long-lived data should generally be migrated to the business tier. This precaution has a number of advantages, including persistence between logins and across server restarts and transactions. Recently, there has been a trend toward applications with a database solely used by the presentation tier. While it may be overkill, this method provides a convenient (and usually high-speed) place to store user and session information without affecting the design of the business tier. |
[ Team LiB ] |