Detailed Table of Contents
Guidance for the item(s) below:
Previously, you learned:
This week, we cover design patterns, a concept that builds upon the above.
Can explain design patterns
Design pattern: An elegant reusable solution to a commonly recurring problem within a given context in software design.
In software development, there are certain problems that recur in a certain context.
Some examples of recurring design problems:
Design Context | Recurring Problem |
---|---|
Assembling a system that makes use of other existing systems implemented using different technologies | What is the best architecture? |
UI needs to be updated when the data in the application backend changes | How to initiate an update to the UI when data changes without coupling the backend to the UI? |
After repeated attempts at solving such problems, better solutions are discovered and refined over time. These solutions are known as design patterns, a term popularized by the seminal book Design Patterns: Elements of Reusable Object-Oriented Software by the so-called "Gang of Four" (GoF) written by Eric Gamma, Richard Helm, Ralph Johnson, and John Vlissides.
Exercises
Can explain design patterns format
The common format to describe a pattern consists of the following components:
Exercises
Guidance for the item(s) below:
Now that you know what design pattern is, let's learn a few example design patterns.
Can explain the Singleton design pattern
Context
Certain classes should have no more than just one instance (e.g. the main controller class of the system). These single instances are commonly known as singletons.
Problem
A normal class can be instantiated multiple times by invoking the constructor.
Solution
Make the constructor of the singleton class private
, because a public
constructor will allow others to instantiate the class at will. Provide a public
class-level method to access the single instance.
Example:
The <<Singleton>>
in the class above uses the UML stereotype notation, which is used to (optionally) indicate the purpose or the role played by a UML element. In this example, the class Logic
is playing the role of a Singleton class. The general format is <<role/purpose>>
.
Exercises
Can apply the Singleton design pattern
Here is the typical implementation of how the Singleton pattern is applied to a class:
class Logic {
private static Logic theOne = null;
private Logic() {
...
}
public static Logic getInstance() {
if (theOne == null) {
theOne = new Logic();
}
return theOne;
}
}
Notes:
private
, which prevents instantiation from outside the class.private
class-level variable.public
class-level operation getInstance()
which instantiates a single copy of the singleton class when it is executed for the first time. Subsequent calls to this operation return the single instance of the class.If Logic
was not a Singleton class, an object is created like this:
Logic m = new Logic();
But now, the Logic
object needs to be accessed like this:
Logic m = Logic.getInstance();
Can decide when to apply Singleton design pattern
Pros:
Cons:
Given that there are some significant cons, it is recommended that you apply the Singleton pattern when, in addition to requiring only one instance of a class, there is a risk of creating multiple objects by mistake, and creating such multiple objects has real negative consequences.
Can explain the Facade design pattern
Context
Components need to access functionality deep inside other components.
The UI
component of a Library
system might want to access functionality of the Book
class contained inside the Logic
component.
Problem
Access to the component should be allowed without exposing its internal details. e.g. the UI
component should access the functionality of the Logic
component without knowing that it contains a Book
class within it.
Solution
Include a class that sits between the component internals and users of the component such that all access to the component happens through the Facade class.
The following class diagram applies the Facade pattern to the Library System
example. The LibraryLogic
class is the Facade class.
Exercises
Can explain the Command design pattern
Context
A system is required to execute a number of commands, each doing a different task. For example, a system might have to support Sort
, List
, Reset
commands.
Problem
It is preferable that some part of the code executes these commands without having to know each command type. e.g., there can be a CommandQueue
object that is responsible for queuing commands and executing them without knowledge of what each command does.
Solution
The essential element of this pattern is to have a general <<Command>>
object that can be passed around, stored, executed, etc without knowing the type of command (i.e. via polymorphism).
Let us examine an example application of the pattern first:
In the example solution below, the CommandCreator
creates List
, Sort
, and Reset Command
objects and adds them to the CommandQueue
object. The CommandQueue
object treats them all as Command
objects and performs the execute/undo operation on each of them without knowledge of the specific Command
type. When executed, each Command
object will access the DataStore
object to carry out its task. The Command
class can also be an abstract class or an interface.
The general form of the solution is as follows.
The <<Client>>
creates a <<ConcreteCommand>>
object, and passes it to the <<Invoker>>
. The <<Invoker>>
object treats all commands as a general <<Command>>
type. <<Invoker>>
issues a request by calling execute()
on the command. If a command is undoable, <<ConcreteCommand>>
will store the state for undoing the command prior to invoking execute()
. In addition, the <<ConcreteCommand>>
object may have to be linked to any <<Receiver>>
of the command ( ) before it is passed to the <<Invoker>>
. Note that an application of the command pattern does not have to follow the structure given above.
Follow up notes for the item(s) above:
A couple of more design patterns will be covered next week.
Guidance for the item(s) below:
You already know some techniques (e.g., exceptions, assertions) to make the code more resilient to errors. Given next is an overarching approach to coding that aims to push further in that direction.
Can explain defensive programming
A defensive programmer codes under the assumption "if you leave room for things to go wrong, they will go wrong". Therefore, a defensive programmer proactively tries to eliminate any room for things to go wrong.
Consider a method MainApp#getConfig()
that returns a Config
object containing configuration data. A typical implementation is given below:
class MainApp {
Config config;
/** Returns the config object */
Config getConfig() {
return config;
}
}
If the returned Config
object is not meant to be modified, a defensive programmer might use a more defensive implementation given below. This is more defensive because even if the returned Config
object is modified (although it is not meant to be), it will not affect the config
object inside the MainApp
object.
/** Returns a copy of the config object */
Config getConfig() {
return config.copy(); // return a defensive copy
}
Implementation → Error Handling → Defensive Programming → What
Can use defensive coding to enforce compulsory associations
Consider two classes, Account
and Guarantor
, with an association as shown in the following diagram:
Example:
Here, the association is compulsory i.e. an Account
object should always be linked to a Guarantor
. One way to implement this is to simply use a reference variable, like this:
class Account {
Guarantor guarantor;
void setGuarantor(Guarantor g) {
guarantor = g;
}
}
However, what if someone else used the Account
class like this?
Account a = new Account();
a.setGuarantor(null);
This results in an Account
without a Guarantor
! In a real banking system, this could have serious consequences! The code here did not try to prevent such a thing from happening. You can make the code more defensive by proactively enforcing the multiplicity constraint, like this:
class Account {
private Guarantor guarantor;
public Account(Guarantor g) {
if (g == null) {
stopSystemWithMessage(
"multiplicity violated. Null Guarantor");
}
guarantor = g;
}
public void setGuarantor(Guarantor g) {
if (g == null) {
stopSystemWithMessage(
"multiplicity violated. Null Guarantor");
}
guarantor = g;
}
// ...
}
Exercises
Implementation → Error Handling → Defensive Programming → What
Can explain when to use defensive programming
It is not necessary to be 100% defensive all the time. While defensive code may be less prone to be misused or abused, such code can also be more complicated and slower to run.
The suitable degree of defensiveness depends on many factors such as:
Exercises
Guidance for the item(s) below:
Previously, we learned how to measure test coverage. This week, we look into how to increase coverage with the least number of test cases.
First, we take a look at test case design in general, different approaches to test case design, and few different categorization of test cases.
Can explain the need for deliberate test case design
Except for trivial , is not practical because such testing often requires a massive/infinite number of test cases.
Consider the test cases for adding a string object to a :
Exhaustive testing of this operation can take many more test cases.
Program testing can be used to show the presence of bugs, but never to show their absence!
--Edsger Dijkstra
Every test case adds to the cost of testing. In some systems, a single test case can cost thousands of dollars e.g. on-field testing of flight-control software. Therefore, test cases need to be designed to make the best use of testing resources. In particular:
Testing should be effective i.e., it finds a high percentage of existing bugs e.g., a set of test cases that finds 60 defects is more effective than a set that finds only 30 defects in the same system.
Testing should be efficient i.e., it has a high rate of success (bugs found/test cases) a set of 20 test cases that finds 8 defects is more efficient than another set of 40 test cases that finds the same 8 defects.
For testing to be , each new test you add should be targeting a potential fault that is not already targeted by existing test cases. There are test case design techniques that can help us improve the E&E of testing.
Exercises
Can explain positive and negative test cases
A positive test case is when the test is designed to produce an expected/valid behavior. On the other hand, a negative test case is designed to produce a behavior that indicates an invalid/unexpected situation, such as an error message.
Consider the testing of the method print(Integer i)
which prints the value of i
.
i == new Integer(50);
i == null;
Can explain black box and glass box test case design
Test case design can be of three types, based on how much of the SUT's internal details are considered when designing test cases:
Black-box (aka specification-based or responsibility-based) approach: test cases are designed exclusively based on the SUT’s specified external behavior.
White-box (aka glass-box or structured or implementation-based) approach: test cases are designed based on what is known about the SUT’s implementation, i.e. the code.
Gray-box approach: test case design uses some important information about the implementation. For example, if the implementation of a sort operation uses different algorithms to sort lists shorter than 1000 items and lists longer than 1000 items, more meaningful test cases can then be added to verify the correctness of both algorithms.
Black-box and white-box testing
Can explain test case design for use case based testing
Use cases can be used for system testing and acceptance testing. For example, the main success scenario can be one test case while each variation (due to extensions) can form another test case. However, note that use cases do not specify the exact data entered into the system. Instead, it might say something like user enters his personal data into the system
. Therefore, the tester has to choose data by considering equivalence partitions and boundary values. The combinations of these could result in one use case producing many test cases.
To increase the E&E of testing, high-priority use cases are given more attention. For example, a scripted approach can be used to test high-priority test cases, while an exploratory approach is used to test other areas of concern that could emerge during testing.
Guidance for the item(s) below:
Next, a heuristic used for improving the quality of test cases.
Can explain equivalence partitions
Consider the testing of the following operation.
isValidMonth(m)
: returns true
if m
(and int
) is in the range [1..12]
It is inefficient and impractical to test this method for all integer values [-MIN_INT to MAX_INT]
. Fortunately, there is no need to test all possible input values. For example, if the input value 233
fails to produce the correct result, the input 234
is likely to fail too; there is no need to test both.
In general, most SUTs do not treat each input in a unique way. Instead, they process all possible inputs in a small number of distinct ways. That means a range of inputs is treated the same way inside the SUT. Equivalence partitioning (EP) is a test case design technique that uses the above observation to improve the E&E of testing.
Equivalence partition (aka equivalence class): A group of test inputs that are likely to be processed by the SUT in the same way.
By dividing possible inputs into equivalence partitions you can,
Can apply EP for pure functions
Equivalence partitions (EPs) are usually derived from the specifications of the SUT.
These could be EPs for the isValidMonth example:
true
(produces false
)true
true
(produces false
)When the SUT has multiple inputs, you should identify EPs for each input.
Consider the method duplicate(String s, int n): String
which returns a String
that contains s
repeated n
times.
Example EPs for s
:
Example EPs for n
:
0
An EP may not have adjacent values.
Consider the method isPrime(int i): boolean
that returns true if i
is a prime number.
EPs for i
:
Some inputs have only a small number of possible values and a potentially unique behavior for each value. In those cases, you have to consider each value as a partition by itself.
Consider the method showStatusMessage(GameStatus s): String
that returns a unique String
for each of the possible values of s (GameStatus
is an enum
). In this case, each possible value of s
will have to be considered as a partition.
Note that the EP technique is merely a heuristic and not an exact science, especially when applied manually (as opposed to using an automated program analysis tool to derive EPs). The partitions derived depend on how one ‘speculates’ the SUT to behave internally. Applying EP under a glass-box or gray-box approach can yield more precise partitions.
Consider the EPs given above for the method isValidMonth
. A different tester might use these EPs instead:
true
false
Some more examples:
Specification | Equivalence partitions |
---|---|
| [ |
| [ |
Exercises
Can apply EP for OOP methods
When deciding EPs of OOP methods, you need to identify the EPs of all data participants that can potentially influence the behaviour of the method, such as,
Consider this method in the DataStack
class:
push(Object o): boolean
o
to the top of the stack if the stack is not full.true
if the push operation was a success.MutabilityException
if the global flag FREEZE==true
.InvalidValueException
if o
is null.EPs:
DataStack
object: [full] [not full]o
: [null] [not null]FREEZE
: [true][false] Consider a simple Minesweeper app. What are the EPs for the newGame()
method of the Logic
component?
As newGame()
does not have any parameters, the only obvious participant is the Logic
object itself.
Note that if the glass-box or the grey-box approach is used, other associated objects that are involved in the method might also be included as participants. For example, the Minefield
object can be considered as another participant of the newGame()
method. Here, the black-box approach is assumed.
Next, let us identify equivalence partitions for each participant. Will the newGame()
method behave differently for different Logic
objects? If yes, how will it differ? In this case, yes, it might behave differently based on the game state. Therefore, the equivalence partitions are:
PRE_GAME
: before the game starts, minefield does not exist yetREADY
: a new minefield has been created and the app is waiting for the player’s first moveIN_PLAY
: the current minefield is already in useWON
, LOST
: let us assume that newGame()
behaves the same way for these two values Consider the Logic
component of the Minesweeper application. What are the EPs for the markCellAt(int x, int y)
method? The partitions in bold represent valid inputs.
Logic
: PRE_GAME, READY, IN_PLAY, WON, LOSTx
: [MIN_INT..-1] [0..(W-1)] [W..MAX_INT] (assuming a minefield size of WxH)y
: [MIN_INT..-1] [0..(H-1)] [H..MAX_INT]Cell
at (x,y)
: HIDDEN, MARKED, CLEARED Guidance for the item(s) below:
Previously, you learned about equivalence partitions, a heuristic for dividing the possible test cases into partitions. But which test cases should we pick from each partition? Next, let us learn another heuristic which can addresses that problem.
Can explain boundary value analysis
Boundary Value Analysis (BVA) is a test case design heuristic that is based on the observation that bugs often result from incorrect handling of boundaries of equivalence partitions. This is not surprising, as the end points of boundaries are often used in branching instructions, etc., where the programmer can make mistakes.
The markCellAt(int x, int y)
operation could contain code such as if (x > 0 && x <= (W-1))
which involves the boundaries of x’s equivalence partitions.
BVA suggests that when picking test inputs from an equivalence partition, values near boundaries (i.e. boundary values) are more likely to find bugs.
Boundary values are sometimes called corner cases.
Exercises
Can apply boundary value analysis
Typically, you should choose three values around the boundary to test: one value from the boundary, one value just below the boundary, and one value just above the boundary. The number of values to pick depends on other factors, such as the cost of each test case.
Some examples:
Equivalence partition | Some possible test values (boundaries are in bold) |
---|---|
[1-12] | 0,1,2, 11,12,13 |
[MIN_INT, 0] | MIN_INT, MIN_INT+1, -1, 0 , 1 |
[any non-null String] | Empty String, a String of maximum possible length |
[prime numbers] | No specific boundary |
[non-empty Stack] | Stack with: no elements, one element, two elements, no empty spaces, only one empty space |