Lecture 9: The Controller
Lecture on overdesigning and review on access specifiers
The Builder Pattern (same as from previous lecture).
9.1 Introduction
The controller is the glue of the system. It takes input from the user and decides what to do. First it is the client of the model, and so it controls how and when the model is used. It also controls what must be shown by the view and when. Some programs put unique responsibilities on the controller depending on what they do.
9.1.1 Synchronous controllers
The game of tic-tac-toe has a predictable, linear progression. The program (i.e. the controller) must follow a fixed sequence of “get input, pass input to model, get board and pass to view” operations until a player wins or the game ends in a stalemate. In general this behavior is observed in applications that run according to certain rules, making the sequence of operations predictable. Controllers that implement such a pre-defined sequence are called synchronous controllers. They are common in games and programs that do batch-processing (e.g. converting all images in a folder to .jpg format, etc.).
The design of such a controller is typically a method that goes through this sequence in a loop. The loop may have branches depending on the application, but the sequence is predictable.
A challenge for such controllers is to abstract the user input. The function of the controller does not change if the input is sought from the keyboard, from a file, or another input source. The controller must be designed so that this independence is achieved as much as possible.
9.1.2 Asynchronous controllers
Many programs are reactive in nature. The program does not execute a pre-defined sequence of operations, but rather executes depending on user input. Most programs with graphical user interfaces behave this way. Asynchronous controllers are suitable for this purpose.
Instead of a single method that executes a sequence of operations, the functionality of an asynchronous controller is usually divided into several methods. Each method is called in response to specific user action.
9.2 The role of the main method
The division of a program into model, view and controller has implications on what the main method is supposed to do (in case of standalone programs). In this context, the main method should not directly implement any functionality (the model does this), should not directly interact with the user (the controller does this) and should not directly show any results (the view does this). The main method (or in general, the “starter” of the application) is reduced to creating the model, view(s) and controller(s) and then giving control to the controller.
class ProgramRunner { public static void main(String[] args) { Model model = new Model(...); //set up before if needed View view = new View(...); //setup details of view if needed Controller controller = new Controller(model,view,...); //give controller the model and view controller.go(); //give control to the controller. Controller relinquishes only when program ends } }
9.3 The complexities of the controller
The two roles of a controller are to take inputs from the user, and to delegate to the model and the view. In a well-designed controller, the input from the user is handled and used in a manner that is independent of the method used to get that input from the user. For example, if the user enters a string then the functionality of the program should not change based on whether the user entered the string on the command line, through a text box on a form.
Another related challenge is to test the controller. If the controller is tightly coupled with the method of input suitable for an interactive program, it may not be amenable to automated unit testing. One may add specific methods to the controller with the specific purpose of testing it. However this is not good design, because there is otherwise no purpose for such methods. How do we design a controller that can be used and tested without changing its code?
In general we must find a way to abstract the input, so it can work with many data sources. Similarly the controller can abstract the output so that it can work with many data sinks. For testing purposes we can attach a data source that allows us to programmatically feed input (e.g. from a file or a string) and parse the output (e.g. output written to a file or a string) to verify the testing objectives. For the actual application we can attach the required data source (e.g. keyboard) and data sink (e.g. console).
9.4 A simple example
The following is a simple program:
/** * Demonstrates a simple command-line-based calculator */ public class SimpleCalc1 { public static void main(String[] args) { int num1, num2; Scanner scan = new Scanner(System.in); num1 = scan.nextInt(); num2 = scan.nextInt(); System.out.printf("%d", num1 + num2); } }
The above is a simple program. It has a monolithic design, with main doing all the work. The only thing good about this program is that it works. In fact there is no way to even test this program using automated testing, because the main method contains all the logic.
9.4.1 Factoring out the model
We begin improving the design of this program by factoring out a model. Recall that the model is the part of the program that implements all its features. In the above program, the part that adds two numbers can be thought of as the model. We factor it out in its own class, and modify the main method to use it.
/** * Demonstrates a simple command-line-based calculator with a separate model */ public class SimpleCalc2 { public static void main(String[] args) { int num1, num2; Scanner scan = new Scanner(System.in); num1 = scan.nextInt(); num2 = scan.nextInt(); System.out.printf("%d", new Calculator().add(num1, num2)); } } /** * The model of the calculator. */ class Calculator { public int add(int num1, int num2) { return num1 + num2; } }
The main method now uses the model by passing it two numbers. This design is marginally better, because adding two numbers and printing them are separated into two tasks. It is now possible to test the model using an automated test.
@Test public void testAdd() { assertEquals(7, new Calculator().add(3, 4)); }
However other parts of the program still cannot be tested, because they are embedded inside the main method.
9.4.2 Factoring out the controller
In the above iteration, the main method still takes input from the user. This is the job of the controller. The following refactoring carves out the controller.
/** * Demonstrates a simple command-line-based calculator. In this example, the * model and controller are factored out. */ public class SimpleCalc3 { public static void main(String[] args) { //create the model Calculator model = new Calculator(); //create the controller Controller3 controller = new Controller3(); //give the model to the controller, and give it control controller.go(model); } } /** * A controller for our calculator. This calculator is still hardwired to * System.in, making it difficult to test through JUnit */ class Controller3 implements CalcController { public void go(Calculator calc) { Objects.requireNonNull(calc); int num1, num2; Scanner scan = new Scanner(System.in); num1 = scan.nextInt(); num2 = scan.nextInt(); System.out.printf("%d", calc.add(num1, num2)); } }
The code of the main method now looks better: it creates the model and the controller, links the latter to the former and then relinquishes control to the controller (by calling its go method). Since we divide all tasks of a program amongst model, views and controllers, the main method should do nothing more than set up the main components and pass control to the controller.
However this controller is not testable, because it uses a specific way of accepting inputs (System.in) and a specific way to transmit output (System.out). Also, showing outputs is the responsibility of the view, so this controller is really a controller mixed with a specific, static view.
9.4.3 Abstracting the controller
Ideally the controller should be independent of the specific source of the input, and should be able to work with a general-purpose view. We change the controller design by replacing System.in with a higher-level, general-purpose input source, and System.out with a higher-level, general-purpose output source.
Looking at the documentation for System.in and System.out, we see that they are instances of InputStream and PrintStream respectively. These are two types of streams in Java (unrelated to the Stream API for functional programming constructs). A stream is a general-purpose source or sink of data. You may think of a stream as a “general pipe” that can be connected to a wide variety of actual data sources (keyboard, files, input buffers, etc.) and data sinks (console, files, output buffers, etc.).
Therefore we refactor the controller by allowing it to work with any InputStream and PrintStream objects.
Looking at the documentation for the InputStream class we see that it allows relatively low-level reading (reading a byte, for example). The advantage of the Scanner class is that it provides a higher-level abstraction for reading data (“read a number”, “read a word”, etc.). Fortunately, the Scanner object can be coupled with an InputStream object, so that we can continue to use a Scanner object in the controller.
/** * A controller for the calculator. The controller receives all its inputs * from an InputStream object and transmits all outputs to a PrintStream * object. The PrintStream object would be provided by a view (not shown in * this example). This design allows us to test. */ class Controller4 implements CalcController { final InputStream in; final PrintStream out; Controller4(InputStream in, PrintStream out) { this.in = in; this.out = out; } public void go(Calculator calc) { Objects.requireNonNull(calc); int num1, num2; Scanner scan = new Scanner(this.in); num1 = scan.nextInt(); num2 = scan.nextInt(); this.out.printf("%d", calc.add(num1, num2)); } }
How can we use this from our main method?
/** * Demonstrates a simple command-line-based calculator. The calculator is * factored out into a model and controller. */ public class SimpleCalc4 { public static void main(String[] args) { new Controller4(System.in, System.out).go(new Calculator()); } }
Since the controller can work with any InputStream and PrintStream objects, we can now write tests that provide it with suitable objects. Unlike the main method, we need an InputStream object that does not wait for the user to enter, but “pull” data from a pre-populated source.
Do Now!
Look at the documentation for the InputStream and PrintStream classes, and determine which subclasses can be helpful in the context of a non-interactive test.
We put our data in a simple String object, and convert it to an array of bytes. We then use the ByteArrayInputStream and connect it to the array of bytes.
String input = "3 4"; InputStream in = new ByteArrayInputStream(input.getBytes());
Similarly, we would like a PrintStream object that allows us to extract its contents, so that we can verify them with the expected output. This is easily done using strings as well. We connect a PrintStream object with a ByteArrayOutputStream object.
ByteArrayOutputStream bytes = new ByteArrayOutputStream(); PrintStream out = new PrintStream(bytes); ... String output = bytes.toString(); //extract contents
We use this to (finally) write an automated test for our controller.
@Test public void testGo() throws Exception { InputStream in = new ByteArrayInputStream("3 4".getBytes()); ByteArrayOutputStream bytes = new ByteArrayOutputStream(); PrintStream out = new PrintStream(bytes); CalcController controller4 = new Controller4(in, out); controller4.go(new Calculator()); assertEquals("7", new String(bytes.toByteArray())); }
9.4.4 Simplifying inputs and outputs even more
Streams are powerful abstractions, and contain many data-related features (such as the ability to buffer, for efficiency). However our controller does not need any such sophisticated features: it just needs to be able to read data one “token” at a time. Therefore we use an even simple abstraction of inputs: Readable objects. The documentation for the Readable interface shows that it allows a simpler API: a method that reads one or more characters. The corresponding interface for outputs is the Appendable interface.
We factor our controller to use these interfaces:
/** * A controller for the calculator. The controller receives all its inputs * from a Readable object and transmits all outputs to an Appendable * object. The Appendable object would be provided by a view (not shown in * this example). This design allows us to test. */ class Controller5 implements CalcController { final Readable in; final Appendable out; Controller5(Readable in, Appendable out) { this.in = in; this.out = out; } public void go(Calculator calc) throws IOException { Objects.requireNonNull(calc); int num1, num2; Scanner scan = new Scanner(this.in); num1 = scan.nextInt(); num2 = scan.nextInt(); this.out.append(String.format("%d\n", calc.add(num1, num2))); } }
How do we “hook up” our main method to such a controller? Since we already know that System.in is an InputStream object, we need something that will convert this into a Readable object. We use the InputStreamReader class for this purpose. Since a PrintStream is an Appendable we can pass System.out directly to this controller.
public static void main(String[] args) { try { new Controller5(new InputStreamReader(System.in), System.out).go(new Calculator()); } catch (IOException e) { e.printStackTrace(); } }
The Appendable object throws an IOException. In this example we merely catch it and print a message, but the controller should ideally throw a more appropriate exception.
Do Now!
Look at the documentation for the Readable and Appendable interfaces, and determine which of its implementations can be helpful in the context of a non-interactive test, as well as an interactive program.
The test looks similar to what we had before:
@Test public void testGo() throws Exception { StringBuffer out = new StringBuffer(); Reader in = new StringReader("3 4"); CalcController controller5 = new Controller5(in, out); controller5.go(new Calculator()); assertEquals("7\n", out.toString()); }
9.4.5 Receiving multiple inputs
Now that we have abstracted our controller, we can make its logic more complicated by having it receive several inputs and produce multiple outputs.
class Controller6 implements CalcController { final Readable in; final Appendable out; Controller6(Readable in, Appendable out) { this.in = in; this.out = out; } public void go(Calculator calc) throws IOException { Objects.requireNonNull(calc); int num1, num2; Scanner scan = new Scanner(this.in); while (true) { switch (scan.next()) { case "+": num1 = scan.nextInt(); num2 = scan.nextInt(); this.out.append(String.format("%d\n", calc.add(num1, num2))); break; case "q": return; } } } }
This calculator repeatedly accepts prefix-expressions with the operator +, until the input “q” is given. We can test this controller by giving it several inputs, and verify the sequence of outputs that it produces.
@Test public void testGo() throws Exception { StringBuffer out = new StringBuffer(); Reader in = new StringReader("+ 3 4 + 8 9 q"); CalcController controller6 = new Controller6(in, out); controller6.go(new Calculator()); assertEquals("7\n17\n", out.toString()); }
9.5 Testing the controller in isolation
All the above tests actually test two things: whether the model works correctly and whether the controller receives inputs and transmits the result produced by the model. A correct actual output transmitted by the controller reflects that both of them worked correctly. This is redundant, because we would have tests to separately verify the model’s correctness. The following (strategically buggy) implementation of a controller will still pass these tests:
class BuggyController6 implements CalcController { final Readable in; final Appendable out; BuggyController6(Readable in, Appendable out) { this.in = in; this.out = out; } public void go(Calculator calc) throws IOException { Objects.requireNonNull(calc); int num1, num2; Scanner scan = new Scanner(this.in); while (true) { switch (scan.next()) { case "+": num1 = scan.nextInt() + 10; //wrong input, bug num2 = scan.nextInt() - 10; //wrong input, but this bug nullifies above bug this.out.append(String.format("%d\n", calc.add(num1, num2))); break; case "q": return; } } } }
This bug appears contrived, but more such realistic cases will appear as the model becomes more complicated. As our tests pass, we will incorrectly conclude that our controller works correctly.
A better way to test the controller would be by isolating it. How do we know that the controller, in isolation, works correctly? It works correctly if it reads the inputs in the correct sequence, sends them to the model correctly, and transmits any outputs to the view correctly. This would mean our controller-model would work correctly, assuming that we have independently verified that the model works correctly when presented correct inputs.
In order to isolate the controller, we provide it with a “mock” model. A “mock” of an object is another object that “looks like the real object, but is simpler”. In particular, the simplicity here implies that it would allow us to verify what was passed to it correctly. We begin by creating an explicit interface for the model for the above example (we should have one for the model in general). We then make our model object implement this interface.
interface ICalculator { int add(int num1,int num2); } //Calculator model from above class Calculator implements ICalculator { public int add(int num1, int num2) { return num1 + num2; } }
We now change our controller and client code slightly.
class Controller6 implements CalcController { final Readable in; final Appendable out; Controller6(Readable in, Appendable out) { this.in = in; this.out = out; } public void go(ICalculator calc) throws IOException { Objects.requireNonNull(calc); int num1, num2; Scanner scan = new Scanner(this.in); while (true) { switch (scan.next()) { case "+": num1 = scan.nextInt(); num2 = scan.nextInt(); this.out.append(String.format("%d\n", calc.add(num1, num2))); break; case "q": return; } } } } ... public static void main(String[] args) { try { new Controller6(new InputStreamReader(System.in), System.out).go(new Calculator()); } catch (IOException e) { e.printStackTrace(); } }
Now we create a “mock” model class.
class MockModel implements ICalculator { private StringBuilder log; private final int uniqueCode; public MockModel(StringBuilder log,int uniqueCode) { this.log = log; this.uniqueCode = uniqueCode; } @Override public int add(int num1,int num2) { log.append("Input: " + num1 + " " + num2 + "\n"); return uniqueCode; } }
This mock does not actually add numbers: it merely logs the inputs provided to it, and returns a unique number provided to it at creation. We can now test the controller in isolation as follows:
@Test public void testGo() throws Exception { StringBuffer out = new StringBuffer(); Reader in = new StringReader("+ 3 4 + 8 9 q"); CalcController controller6 = new Controller6(in, out); StringBuilder log = new StringBuilder(); //log for mock model controller5.go(new MockModel(log,1234321)); assertEquals("Input: 3 4\nInput: 8 9\n", log.toString()); //inputs reached the model correctly assertEquals("1234321\n1234321\n",out.toString()); //output of model transmitted correctly }
What does this test do? It tests whether the inputs provided to the controller were correctly transmitted to the model, and the results from the model were correctly transmitted to the Appendable object by the controller. It does not test whether the controller-model combination produced the correct answer. However this test, along with those for the model, collectively verify the correctness of this combination. It is noteworthy that this test will correctly catch the contrived bug in BuggyController6 above.
As the complexity of the model and controller increases, testing using mock objects provides more substantial benefits. The tests are likely shorter and more focused.