1. Introduction

In this first lab, the aim is that you familiarize yourselves with how design patterns can be implemented in existing projects, and how they can be used to change an existing codebase. You will also examine what effect they will have on an application.

1.1. Tasks

In your teams, you will divide the three main tasks (A, B, and C) among your three pairs. The tasks are related to the application of design patterns in various contexts. The source code for each task is given on gitlab.

1.2. Eclipse

While an IDE is not neccessary it might help out with the exercises in this and the other labs involving the Java programming language.

To get eclipse on the IDA Ubuntu system issue the following command:

module add prog/eclipse/4.10

Note that the Eclipse might be updated to a newer version, use:

module add prog/eclipse

or run:

module avail

to see which version of Eclipse is available.

To start eclipse. Start the terminal and enter:

eclipse &

You are of course free to use your own computers for all labs in this course. However, then the assistants will not help with configuration and setup.

1.3. Task A: The Iterator and Strategy Patterns

The package for task A contains an initial implementation of streams, as in, sequences that produce values upon request but are not backed by a data structure. The file Test.java contains an example that is supposed to demonstrate this functionality by producing even and odd numbers. However, the code does not work; only “Even number 7” is produced (7 is not an even number, and you should expect all odd or even numbers to be printed once). The code is supposed to demonstrate the Iterator and Strategy design patterns.

For something that should influence your design decisions (especially for question 4), consider a more elaborate example of functionality we would like:

public static void main(String[] args) {
    IEnumerable<Integer> evenNumbers = new EvenNumbers();
    ISelector<Integer, String> stringRepresentation = new StringRepresentation();
    IAction<String> print = new PrintAction();
    IPredicate<Integer> divisibleByFour = new ModuloPredicate(4);
    evenNumbers
    .skip(3)
    .take(10)
    .where(divisibleByFour)
    .select(stringRepresentation)
    .forEach(print);
    evenNumbers
    //.skip(3)
    .take(2)
    //.where(divisibleByFour)
    .select(stringRepresentation)
    .forEach(print);
}

Here, we enumerate even numbers using two separate streams, and we expect the first stream to return numbers 8, 10, 12, … after the call to skip(3). We only retain the first ten of those numbers, i.e., 8, 10, 12…, after invoking take(10) and so on.

  1. First, you should describe which elements implement the Strategy design pattern, and relate the concepts in that design pattern to the example code given. You should be able to reason with respect to both the basic example on GitLab and the elaborate one given above.
  2. Second, you should explain how the current implementation violates the principles of the Iterator design pattern (applicable to both the basic example on GitLab and the elaborate one given above; note that these test examples do not contain the violation - look in the implementation)
  3. Third, you should correct the implementation provided so that it works as intended. Explain why it did not work initially.
  4. Fourth, you should explain how you encapsulated all objects in interfaces to enforce Interface Segregation, which states that all clients should use client­specific interfaces. This assumes that all methods accessible through an interface is useable by any client. In your explanation, provide sufficient code or a UML diagram to explain how to extend the current implementation to the elaborate functionality given in the example above, by providing interfaces between the different classes. You do not need to provide a working implementation.

1.4. Task B: The Adapter and Factory Patterns

In this example, two software companies have merged their operations and want to reuse the same build systems for their software. Both have started to implement functionality to use build configuration files in different formats. In one company, the developers have created an Ant­based build system that is programmatically interpreted to provide information for build servers and allow customizations of the build process. Ant uses XML. The other company uses YAML. The implementations are not complete, and different, but still, provide information about the build system. Some merge operations have been performed, such as moving all domain classes that describe concepts in the configuration files, to a separate package “domain.” Still, it is awkward to developers who use the configuration parsers to have to switch between the two implementations depending on what configuration format is used.

When creating an XML build configuration, you obtain one kind of object (classProject), and when you use the YamlBuildConfigurationReader, you get another (class BuildConfig). Both may be used to build projects, but there are different interfaces to the classes that compile projects (or print messages to the console at least).

In this task, you may not change code in the packages XML and YAML, respectively. You may only add “glue code” according to the two design patterns Factory Method and Adapter, to ensure that the two can be used with a standard interface. You shall keep the ConfigurationReader, Build & Compile classes intact. However, you may add code to ensure a common interface (outside of the XML and YAML packages). Note that each Reader class produces an object that may be used to build software. Thus, we like to do something like this:

public static void main(String[] args) {
    /* ... */
    final String target = "dist";
    buildTarget("build.xml", target);
    buildTarget("build.yaml", target);
}

Expected output:

Building MyProject
Building target init
Building target compile
Building target dist

simple example build file
Building target init:
Building target compile:
Building target dist:

Also, you should be able to answer the following two questions:

  1. What specific functionality is offered by the Factory Method and adapter design patterns when enabling access to these two different implementations? Why do we need to include both as part of our solution?
  2. How can your solution benefit the users of this application? Justify your design by explaining how it is intended to be used, and what information is required by the client(an application that uses your Adapter/Factory implementation, or extends it)

Also perform the following tasks:

  1. Provide a UML class diagram of the project. Highlight the used design patterns.

1.5. Task C: The Visitor and Interpreter Patterns

When the Visitor design pattern is used, responsibility is divided between the structure being traversed and the Visitor who performs actions on the structure. It is a pattern that relies on knowledge of how polymorphism works. In this example, you will explore a non­functioning Visitor implementation. You will do this by first familiarizing yourself with the basics of polymorphism and single dispatch, and later explain the behavior related to the Visitor design pattern. Read how subtype polymorphism works and explain the behavior in the polymorphism.PolymorphismTest class

  1. Explain why the actual runtime type of subAsBase is not taken into account when invoking aMethod(). Try the other invocations that are deactivated currently and explain the results of invoking aMethod() there.
  2. Explain why the implementation in the visitor package does not work. In the VisitorTest class, there is a small but incorect program that is intended to be used to illustrate the Visitor pattern. There are two errors: one that gives error messages and another that gives an incorrect result. The output of the Visitor should match one of the toString functions.
  3. Identify what is wrong, explain the error, and apply a fix to eliminate the errors. If you wonder how to construct Expression type objects, there is a class ExpressionTest you may use.

1.5.1. Extending the Interpreter

The Interpreter pattern is exemplified in the package interpreter.lisp, where a small language is interpreted, a mini­version of Scheme. Scheme uses parentheses to delimit all compound expressions, and the first term in a compound expression is the operator/function name/language keyword. Thus, instead of “1+2”, you would write “(+ 1 2)”. This form is also called prefix form, compared to infix form where the operator is between the operands. All expressions are evaluated by first resolving the first symbol encountered in the expression (“+” in “(+ 1 2)”), and then resolving the remaining subexpressions. Look at class interpreter.lisp.InterpreterTest for a description of the syntax of the language. Try to write your own expressions. Use the interpreter to evaluate the results. You are to add another type of expression:

(case <cond>
    (<val1> <expr1>)
    (<val2> <expr2>)  ...)

<cond> is an expression that will evaluate to a value that will have to be equal to either the values of <val1>, <val2> or any other value in the case expression. Depending on the value of <cond>, either <expr1> or <expr2> may be evaluated and used as the value of the expression. The default result will be Constants.FALSE.

Examples:

(case 1  (2 0)  ((* 1 1) 1))
=> 1
(case
    (* 3 (+ 1 2))
    ((+ 6 2) 0)
    (14 2)
    ((+ 6 3) (- 2 1)))
=> 1
(def f (x)
    (case x(1 0)))
(f 0)
=> FALSE

Note

Most of you don’t know Lisp code, so it might be helpful to think of the second expression above as being evaluated using something like the following:

cond = 3 * (1 + 2);
val1 = 6 + 2;
if (cond == val1) {
  expr1 = 0;
  return expr1;
}
val2 = 14;
if (cond == val2) {
  expr2 = 2;
  return expr2;
}
val3 = 6 + 3;
if (cond == val3) {
  expr3 = 2 - 1;
  return expr3;
}
return false;

To help get started: ask yourself the question “what would you need to change the existing structure?”

1.5.2. Implement the new conditional expression using the Interpreter design pattern.

When implementing this addition, you will notice that the CompoundExpression class needs to be modified. Make sure that you decouple CompoundExpression from the concrete subclasses, and your new addition. There should be no direct connections between CompoundExpression and its subclasses so that it is possible to add other expression types later on without violating the Open/Closed principle here: CompoundExpression should be closed for modifications that pertain to different classes.

Note

The existing CompoundExpression class breaks the open-closed principle, which is what we want to resolve here. With a redesign of CompoundExpression.evaluate, it should be possible to add the behavior of for example the case expression without modifying the CompoundExpression class (which means you can ship a JAR-file to a customer, and the customer can add classes extending behaviour of CompoundExpression).

In particular the CompoundExpression.evaluate should not compare the strings "if" or "case" directly (which is probably how you solved Section 1.5.1 Extending the Interpreter).