Structuring Applications with Graphical Interfaces
- 9.1 The Core: Model-View Separation
- 9.2 The Model-View-Controller Pattern
- 9.3 The JFace Layer
- 9.4 The MVC Pattern at the Application Level
- 9.5 Undo/Redo
- 9.6 Wrapping Up
Chapter 7 introduced the technical and conceptual basis for building user interfaces using the SWT framework that comes with Eclipse. At the core, development comprises two aspects: setting up a widget tree with layout information to create the visual appearance, and attaching event-listeners to the individual widgets to implement the application’s reaction to user input. Although this seems simple enough, this basis alone is too weak for building larger applications: Since the application’s functionality tends to be scattered throughout event-listeners, one will almost certainly end up with a code base that cannot be maintained, extended, and ported to different platforms—in other words, software that must be thrown away and redeveloped from scratch.
This chapter investigates the architectural building block that keeps applications with user interfaces maintainable and portable: In the code, one always separates the application’s business logic strictly from its graphical interface. Section 9.1 introduces this approach, called model-view separation, and traces it through different examples within the Eclipse platform. Next, Section 9.2 discusses its conceptual and technical basis, the classical MODEL-VIEW-CONTROLLER pattern. Section 9.3 introduces the JFace framework, which complements the basic SWT widgets by connecting them to the application’s data structures. Section 9.4 uses a running example MiniXcel, a minimal spreadsheet implementation, to give a self-contained overview and to explore several implementation details of model-view separation that must be mastered to create truly professional applications. Finally, Section 9.5 adds the aspect of making edits undoable, which is indispensable for achieving usability.
Throughout the presentation, we will pay particular attention to the fact that model-view separation is deceptively simple: While the concept itself is rather straightforward, its rendering in concrete code involves many pitfalls. We will discuss particularly those aspects that have often been treated incorrectly in the work of novices to the field.
Before we start to delve into these depths of software design and implementation, there is one general piece of advice to set them into perspective:
Always gear the application toward the end users’ requirements.
The reason for placing this point so prominently is that it is neglected so 258 often. As developers, we often get swept away by our enthusiasm for the technically possible and the elegance of our own solutions. However, software development is not a modern form of l’art pour l’art, but a means of solving other people’s pressing problems. These people, called “users,” do 229 not care about the software’s internals; they care about their own workflows. So before you even start to think about the software’s view and model and 28 the elegance of their separation, talk to the end users: What are their expectations of the software’s concrete behavior? How do they wish to interact with the software? Which particular tasks must the software support? The conscientious professional software engineer starts application development by learning about the users’ work—in other words, by learning about the software’s application domain. Everything said subquently must be subject to this overall guideline.
9.1 The Core: Model-View Separation
Every application has a purpose for which it is built and which provides its unique value to its users. Correspondingly, the application contains code that implements the business logic to fulfill that purpose. Apart from that, most applications need a graphical user interface, simply because they have nontechnical users who do not appreciate command-line tools too much.
Apart from all of the strategic considerations related to software quality 9.2.2 and maintenance, to be discussed later, it is useful to keep the code implementing the business logic and the user interface separate simply because they have different characteristics (Fig. 9.1). Users buy, for instance, CAD software because its business logic can do CAD and nifty computations, but they accept it into their working routine because they like the way they can interact with it. The business logic of a CAD system must be extremely reliable to prevent bridges from collapsing, and it must be stable enough through different software releases, for instance, to read the same files correctly throughout projects running for several years. The interface, in contrast, must be visually appealing and must adapt to the changing working habits of its users so that they can, for instance, exploit new input methods such as 3D interaction devices. To achieve stability, the business logic must adhere to rigorous contracts and must be tested comprehensively, 7.11 5.3.5 while the interface is event-based and cannot be tested easily, especially if it is liable to frequent changes. Finally, the business logic must deal with internal data structures and basic services such as file I/O, which are easily ported to different platforms. The API of graphical interfaces, in contrast, varies dramatically between platforms, and user interface code is usually not portable at all—for instance, from SWT to Swing. Keeping business logic and user interface separate is therefore first of all a matter of separation of concerns.
Figure 9.1 Characteristics of Business Logic and the User Interface
Keep the user interface and the business logic in different modules.
Accepting the goal of this separation, we have to investigate how it can be accomplished in the concrete software. Fig. 9.2 gives an overview, whose aspects we will explore in the remainder of this section. As a first step, one places the user interface and the business logic into separate modules, as indicated by the dashed horizontal dividing line in the figure. Referring 9.2 to their roles in the MODEL-VIEW-CONTROLLER pattern, the business logic and the user interface are also called the model and the view, respectively, which explains the term model-view separation as a summary of the principle.
Figure 9.2 Overview of Model-View Separation
A.1 In Eclipse, modules are implemented as plugins. Throughout the Eclipse code base, plugins with suffix .ui access the functionality provided by the corresponding plugins without that suffix. For instance, org.eclipse.jdt.ui accesses the Java Development Tools, whose logic comes in plugin org.eclipse.jdt.core, as well as org.eclipse.jdt.launching, org.eclipse.debug.core, and others.
Introducing separate plugins will at first appear as a somewhat large A.1.2 overhead for small applications. However, the sophisticated support for plugin development in Eclipse removes any technical complexity and exhibits the benefits of the split: The functionality can be linked into different applications to enable reuse; unit tests run much faster on plugins that do A.1 not require the user interface to come up; the OSGi class loader ensures that the logic code cannot inadvertently access interface classes; the logic module remains small and focused on its task; and several more. And, finally, successful small applications have a tendency to grow quickly into successful large applications; the split into different plugins ensures that they will also grow gracefully.
The model contains the application’s core functionality.
From the users’ perspective, an application is all about the user interface, since they are not and should not be aware of any other part. The interface creates simplifications and abstractions that keep all the technical complexity 229 under the hood. When writing a letter with a word processor, for example, one certainly does not want to think about linear optimization problems for line and page breaking. 142
The software engineer, in contrast, focuses on the business logic, or the model, in Fig. 9.2. That component contains the data structures and algorithms that solve the problems that the application is built for. Its objects constitute the machinery that the whole project relies on. Its answers to the 11.1 technical, conceptual, and maybe scientific challenges make up the team’s and the company’s competitive advantage. The user interface from this perspective is merely a thin, albeit commercially all-important, wrapper that enables nontechnical users to take full advantage of the functionality.
We have chosen the term “core functionality” rather than just “functionality” in this summary because the user interface does provide its own nontrivial behavior. Visual highlights and effects, reactions to drag-and-drop 9.4.4 gestures, and wizards to guide the user—they all require careful engineering in themselves. Yet, they do not belong to the “core,” because they would need to be rebuilt from scratch on a new platform.
Never mention user interface classes in the logic.
The goal of the proposed division is to keep the business logic independent of the user interface, because this will establish precisely the separation of concerns indicated in Fig. 9.2. This can, however, be accomplished only if the code implementing the business logic never mentions user interface classes, such as widgets, images, or other resources: A single reference to a specific user interface library destroys portability and testability. At the level of modules, this means that the user interface module will reference the logic module, but not the reverse.
Connect the user interface to the logic using OBSERVER.
The question is then how logic objects can ever communicate with interface objects at all. The key insight here is that the OBSERVER pattern enables precisely this communication: The subject in the pattern accesses its observers 2.1.2 only through an interface that is defined from the perspective of the subject and is independent of the concrete observers.
In the case of model-view separation, the observer interface is contained in the business logic module, and that module sends change messages to observers in the interface module (see Fig. 9.2). These observers will translate the generic change notifications into concrete updates of the widgets.
Let us look at the example of Eclipse’s management of background jobs, which also exhibits several interesting facets beyond the bare fundamentals. 2.1.1 We have already seen that the platform’s JobManager allows observers to register for change notifications:
org.eclipse.core.internal.jobs.JobManager
public void addJobChangeListener(IJobChangeListener listener) public void removeJobChangeListener(IJobChangeListener listener)
The interface IJobChangeListener is contained in the same package as the job manager itself, in org.eclipse.core.runtime.jobs. Neither that interface nor the IJobChangeEvent is connected in any way to possible user interfaces.
org.eclipse.core.runtime.jobs.IJobChangeListener
public interface IJobChangeListener { public void scheduled(IJobChangeEvent event); public void aboutToRun(IJobChangeEvent event); public void running(IJobChangeEvent event); public void done(IJobChangeEvent event); ... }
The standard user interface for jobs is the Progress view, implemented in class ProgressView and several helpers. They reside in the user interface package org.eclipse.ui.internal.progress. The central class is the 1.3.8 (singleton) ProgressManager, which registers to observe the (singleton) JobManager.
org.eclipse.ui.internal.progress.ProgressManager.JobMonitor
ProgressManager() { ... Job.getJobManager().addJobChangeListener(this.changeListener); }
org.eclipse.ui.internal.progress.ProgressManager
private void shutdown() { ... Job.getJobManager().removeJobChangeListener( this.changeListener); }
Construct view-related information at the view level.
The example of the Progress view also illustrates a typical aspect that accounts for a lot of the complexity involved in presenting the business logic adequately to the user: the need to create intermediate view-related data structures.
The model of jobs is essentially a flat list, where each job provides progress reports through progress monitors. Usability, however, is improved 7.10.2 by arranging the display into a tree of running jobs, job groups, tasks, and subtasks that integrates all available information. The ProgressManager in the user interface therefore constructs a tree of JobTreeElement objects. Since the information is useful only for a specific intended user interface and might change when the users’ preferences change, the maintenance of the tree is handled entirely in the view, not in the model.
The ProgressManager’s internal logic then integrates two sources of information into a single consistent tree: the running and finished jobs, obtained through the observer registered in the preceding example, and the progress reports sent by the running jobs, to be discussed next.
Let the model access the view only through interfaces.
The observer pattern is only one instance of a more general principle, if we perceive the view and the model as different layers of the overall application. 12.2.2 59 In this context, a lower layer accesses a higher layer only through interfaces defined in the lower layer, so as to allow higher layers to be exchanged later on. Furthermore, the calls to higher layers usually take the form of event notifications (see Fig. 9.2). In a typical example, the operating system’s networking component does not assume anything about applications waiting for data, but it will notify them about newly arrived data by passing that data into the buffers belonging to the application’s sockets.
Both aspects—the access through interfaces and the notifications—can also be seen in the handling of progress reports. The model-level Jobs receive an object to be called back for the reports, but this object is given as an interface IProgressMonitor:
org.eclipse.core.runtime.jobs.Job
protected abstract IStatus run(IProgressMonitor monitor);
The user interface can then create a suitable object to receive the callbacks. In Eclipse, this is also done in the ProgressManager class, where progressFor() creates a view-level JobMonitor.
org.eclipse.ui.internal.progress.ProgressManager
public IProgressMonitor createMonitor(Job job, IProgressMonitor group, int ticks) { JobMonitor monitor = progressFor(job); ... handle grouping of jobs return monitor; }
The guideline of accessing the user interface only through interfaces can also be seen as a positive rendering of the earlier strict rule that no class from the user interface must ever occur in the model code. If the model code must collaborate with a view object, it must do so through model-level interfaces implemented by view objects.
Event-listeners mainly invoke operations defined in the model.
We have now discussed in detail the notifications sent from the model layer to the view layer, depicted on the left-hand side of Fig. 9.2. This focus is 12.1 justified by the fact that the decoupling between model and view originates from the proper use of interfaces at this point.
The right-hand side of Fig. 9.2 shows the complementary collaboration 7.1 between view and model. By technical necessity, the user input is always delivered to the application code in the form of events. The question then arises as to how the expected behavior of the overall application should be divided between the event-listeners in the view and the code in the model component.
The main insight is that the event-listeners are a particularly bad place 5.3.5 for valuable code. The code cannot be tested easily, which makes it hard 5.4.8 to get it stable in the first place, let alone keep it stable under necessary changes. Also, the code will probably be lost entirely when the users demand a different interface or the application is ported to a different platform (Fig. 9.1).
It is therefore a good idea to place as little code and logic as possible into the event-listeners, and to move as much as possible into the model 4.1 5.1 instead. There, it can be made reliable through contracts and testing; there, it can be reused on different operation systems; there, it can be maintained independently of the vagaries of user interface development.
In the end, the ideal event-listener invokes only a few methods on the model. The only logic that necessarily remains in the event-listeners relates to the interface-level functionality such as the handling of drag-and-drop of 9.4.4 data and of visual feedback on the current editing gestures.
Design the model first.
It is tempting to start a new project with the user interface: You make rapid progress due to the WindowBuilder, you get early encouragement 7.2 from prospective users, and you can show off to your team leader. All of this is important, since nifty data structures without a usable interface are not worth much—in the end, the users have to accept the application and use it confidently. For this reason, it can also be strategically sensible to start with the interface and even a mock-up of the interface, to check whether anybody will buy the finished product.
Because starting with the user interface is such an obvious choice, we wish to advocate the complementary approach: to start with the model. 59 Here are a few reasons for postponing work on the user interface for a little while.
- You stand a better chance that the model will be portable and reusable. As with the test-first principle, the missing concrete collaborators in 5.2 the user interface reduce the danger of defining the model, and in particular the observer interfaces (Fig. 9.2), specifically for those collaborators. 2.1.2
- Test-first is applicable to the model, and it will have its usual benefits. 5.2
- The model will naturally contain all required functionality, so that the danger of placing too much functionality into the listeners is avoided from the start.
- There is no danger that a mock-up user interface presumes an API for the model that cannot be supported efficiently.
- The mission-critical challenges, such as in algorithmics, will be encountered and can be explored before an expensive investment in the user interface has taken place. If it turns out that the application will take a longer time than expected or cannot be built at all, the company has lost less money. Also, there is still time to hire experts to overcome the problems before the release.
- The user interface can focus on usability. Once the functionality is available, the user interface team just has to provide the most effective access paths to that functionality; it does not have to delve into the business logic aspects.
9.2.2 Together, these aspects maximize the benefits of model-view separation.
Envision the interface while creating the model.
Conversely, a strict focus on the model is likely to have drawbacks for the final product. From an engineering point of view, the API of the model may not suit the demands of the interface, so that workarounds have to be found:
- The event-listeners contain extensive logic to access the existing API. This means that this logic will be lost when the interface has to change.
- 2.4.1 The model contains adapters to provide the expected API.
- The model has to be refactored.
From a usability perspective, the fixed model API may induce developers to take the easy way out of these overheads and to provide a user interface that merely mirrors the internals. A typical example comprises CRUD (CReate 220,145,266 Update Delete) interfaces to databases, which are easy to obtain, but which 114 are known to provide insufficient support for the user’s workflows.
Model-view separation incurs an extra complexity that will pay off.
We have seen much motivation and many benefits of model-view separation, 9.2.2 and we will discuss the details. At the end of this overview, however, let us consider not the benefits, but the costs of model-view separation.
- Splitting the code into separate modules always involves the design of interfaces between the modules, and the communication about them can take a lot of time and presents the potential for mistakes that must be remedied later at high cost. When a data structure is kept right in the user interface, one can hack in a new requirement at the last minute. In contrast, if the data is encapsulated in a different module, one may have to negotiate with the developers who are responsible first.
- The collaboration from model to view always takes place by generic change notifications (Fig. 9.2), rather than specific method calls that update parts of the screen. In the model, one has to provide the 2.1 general OBSERVER pattern for many objects, even if there is in the 2.1.4 end only a single concrete observer in the user interface. Furthermore, 9.4.3 the logic to translate the changes into screen updates itself can be substantial and complex, especially if it is necessary to repaint the smallest possible screen area to keep the application responsive.
Model-view separation is therefore an effort that must be taken at the 9.4 start of a project. The walk-through example of MiniXcel will give you a mental checklist of the single steps, which allows you to assess the overall effort up front. We hope that the checklist is then simple enough to convince you of using model-view separation in all but the most trivial throwaway applications. Even in projects of a few thousand lines, the investment in the extra structure will pay off quickly, since the software becomes more testable, maintainable, and changeable. And if the application happens to live longer than expected, as is usually the case for useful software, it is ready for that next step as well.