Relevant Reading Material
Chapters From How to Design Classes (HtDC)
-
Chapter 12
-
Chapter 13
-
Chapter 14
-
Chapter 15
-
Chapter 16
-
Intermezzo 2
Single Responsibility
Recall from CS5001 our rule
One task one function
The rule is still relevant for Java
One task one method
We have another rule that refers to classes
One class one responsibility
After mapping our information to data and in Java’s case, classes we aim to add methods to our classes based on the responsibility that we have assigned to our class.
Let’s consider out code for Shapes
again
One way to implement moveX()
in the concrete classes that implement Shape
could be
// In Class Circle
public Shape moveX(Integer dx) {
return new Circle(new Posn(this.pin.getX() + dx), this.pin.getY()), this.radius);
}
// In Class Square
public Shape moveX(Integer dx) {
return new Square(new Posn(this.pin.getX() + dx), this.pin.getY()), this.side);
}
// In Class Rectangle
public Shape moveX(Integer dx) {
return new Rectangle(new Posn(this.pin.getX() + dx), this.pin.getY()), this.width, this.height);
}
Observe
-
The code is very similar
-
The code is essentially updating a value, the
x
coordinate ofPosn
, inside a class that is notPosn
.
Who is responsible for manipulating x
and y
?
Posn
should be the entity responsible for updating any data that we captured as being part of Posn
.
Here is another implementation
//In Class Posn
public Posn moveX(Intefer dx) {
return new Posn(this.x + dx, this.y);
}
// In Class Circle
public Shape moveX(Integer dx) {
return new Circle(this.pin.moveX(dx), this.radius);
}
// In Class Square
public Shape moveX(Integer dx) {
return new Square(this.pin.moveX(dx), this.side);
}
// In Class Rectangle
public Shape moveX(Integer dx) {
return new Rectangle(this.pin.moveX(dx), this.width, this.height);
}
And here is the updated UML Class Diagram
The code is similar for moveY()
. Now any class that contains a Posn
can leverage our new methods.
Here is a new rule, the Law of Demeter [1]
Only talk to your friends
For a method m
inside a class C
your friends are
-
fields defined/inherited in the class
C
-
arguments of
m
-
methods defined/inherited in
C
-
methods available on the arguments of
m
-
methods available on the fields inherited/defined in
C
-
local values created in the body of
m
-
methods available on local values created in the body of
m
Design Recipe
-
Data Analysis
-
Data Examples
-
Template
-
-
Signature
-
Purpose
-
Examples
-
Function Definition
-
Tests
-
Review
Nothing has changed about the Design Recipe. What has changed is the outcome of each step since we are now going to create Java programs instead of Racket programs.
Racket
;;;; Data Analysis and Definitions:
(define-struct student (last first teacher))
;; A student is a (make-student Symbol Symbol Symbol)
;; INTERP: represents a student's first and last name
;; and the name of the their teacher
;;;; Data Examples:
;; (make-student 'Joe 'Doe 'Mary)
;; (make-student 'Mat 'Jones 'Fritz)
;;;; Template:
;; student-fn: Student -> ???
;; (define (student-fn a-student a-teacher)
;; ... (student-last a-student) ...
;; ... (student-first a-student) ...
;; ... (student-teacher a-student) ...)
;;;; Signature: subst-teacher : Student Symbol -> Student
;;;; Purpose: to create a student structure with a new
;;;; teacher name if the teacher's name matches 'Fritz
;;;; Examples:
;; (subst-teacher (make-student 'Mat 'Jones 'Fritz) 'Elise)
;; =
;; (make-student 'Mat 'Jones 'Elise)
;; (subst-teacher (make-student 'Joe 'Doe 'Mary) 'Elise)
;; =
;; (make-student 'Joe 'Doe 'Mary)
;;;; Function Definition:
(define (subst-teacher a-student a-teacher)
(cond
[(symbol=? (student-teacher a-student) 'Fritz)
(make-student (student-last a-student)
(student-first a-student)
a-teacher)]
[else a-student]))
;;;; Tests:
(check-expect (subst-teacher (make-student 'Mat 'Jones 'Fritz) 'Elise)
(make-student 'Mat 'Jones 'Elise))
(check-expect (subst-teacher (make-student 'Joe 'Doe 'Mary) 'Elise)
(make-student 'Joe 'Doe 'Mary))
Java
Data Analysis
We are trying to map information to data. Instead of define-struct
we are going to design Java classes.
Create a UML class diagram to capture
-
Java classes that you are going to create
-
Fields for each class that you need
-
Methods for each class that you need
-
Dependencies between classes; arrows that show relationships between classes.
For example recall the Author
example from Lab1
Data Examples
Java code that creates instance of your classes. This code ends up in
your setup()
method inside your Test
class (e.g., AuthorTest
,
EmailTest
, AddressTest
).
Templates
The goal of the template it do decompose your data. At this point data in Java means a class. So we need a template for a class that, hopefully, all class methods can use as a starting point. The contents of the template is a list of operations and fields that we can rely on. It is in fact the same list as the list of "friends" as we have defined it for the Law of Demeter.
So lets take Author
template()
contains all the elements (fields and methods) that we can access.
Observer that the template above does not include equals(Object o)
. The template that we have above
concerns only the Author
class. This template has higher chances of being used for methods that do not
take any argument.
We write a different template for every possible combination of argument types e.g.,
Observe that template(Author a)
can call any method on a
because we are still within Author
and
have access to all (private
, public
and protected
) fields and methods.
The third template template(Email e)
can only call public methods on e
which is of type Email
.
We write a template for each combination of list of argument types that we plan to use in order to implement a method in our class.
Signature
The signature that we used to write for each Racket function is now embedded into the method signature for Java. The types that we have to include as part of the Java method’s signature are essentially the signature that we used to write as comments in Racket.
public class MyInt {
private Integer val;
public MyInt(Integer val) {
this.val = val;
}
// signature : getVal() : -> Integer
public Integer getVal() {
return this.val;
}
// signature : greaterThan: Integer -> Boolean
public Boolean greaterThan(Integer other) {
return this.val > other.getVal();
}
We do have to remember however that Java methods are inside a class and we always has access to the this
class. In Racket
we had to pass a value of our struct
as an argument, in Java we are already inside the instance of the class.
Another way to visualizing this property is to assume an invinisble first argument that has the type of the class
that defines this method and it is called this
and that Java auto-populates each call with the right object for the
first argument called this
.
e.g.,
this
as an argument to each method.public class MyInt {
private Integer val;
public MyInt(MyInt this, Integer val) {
this.val = val;
}
// signature : getVal() : MyInt -> Integer
public Integer getVal(MyInt this) {
return this.val;
}
// signature : greaterThan: MyInt Integer -> Boolean
public Boolean greaterThan(MyInt this, Integer other) {
return this.val > other.getVal();
}
Purpose
Similar to Racket only in Java we need to write our purpose using Javadoc, which means that we have
-
A one sentence description of what this method does.
-
Any
WHERE
clauses that the method requires -
Any
INVARIANTS
,HALTING MEASURES
,TERMINATION ARGUMENTS
that the method requires -
A one line comment for each formal argument using
@param
-
A one line comment explaining the method’s return value using
@return
.
Examples
Examples in Java are written as part of the Javadoc comment for the method so that when we generate our documentation and provide it to clients they will have example uses for each method.
Function Definition (now Method Definition)
The name is misleading for Java since we do not have function but methods. So we will rename this step to Method Definition.
Tests
In Java our tests are found in a companion JUnit class. Instead of check-expect
and its variants we use the JUnit Assert
class along with its assertion
methods.
Review
This step is the same. Go back and review each step of the Design Recipe.
UML Sequence Diagrams
UML Sequence Diagrams are used to model the flow of execution within your system. Sequence Diagrams are typically used to model:
-
Usage scenarios. A sequence of operations that your code goes through.
-
The logic of methods. Designate the steps taken during the excution of a method including the objects called, arguments passed etc.
-
The logic of a component/service. Similar to methods but the entities used can be a component, a web service etc.
We will mostly use Sequence Diagrams to show the exectuion of our methods.
Consider the addition of the following method in Author
...
/**
* Return the author's first name
*
* @return the author's first name
*/
public String getFirstName() {
return this.person.getFirst();
}
...
With the following code in our Test class
...
Author a1 = new Author(new Person("John", "Smith"), ...); // elided arguments
a1.getFirstName();
...
Here is the Sequence Diagrams for this getFirstName()
.
-
Sequence Diagrams are graphs with the Y-axis being time. Time moves from top to bottom.
-
Each object appears as a line with the same header and footer. [2] The header and footer contain
-
the object’s bound variable if any, if there is no bound variable then leave this blank
-
a colon
:
-
the object’s runtime type
-
-
A method call is an arrow from left to right where
-
the arrow originates from the object that makes the method call
-
the arrow terminates on the object that receives the method call
-
the arrow is labeled with the method name and its arguments
-
-
A method return is a dotted arrow from right to left where [3]
-
the arrow originates from the object returning a value
-
the arrow terminates on the object that receives the return value
-
the arrow can be labelled with the return value
-
-
Also notice that on the line that represents an object we see vertical rectangles. These rectangles denote the execution time of a method. Think of them as representing the method’s body.
Recursive Data
Recall Racket Lists
Using an itemization, structs and a self reference.
;; +-------------+
;; \/ |
;; A List of Number (LoN) is one of |
;; - empty |
;; - (cons Number LoN) --------------+
;; Template
;; (define (lon-fn alon)
;; (cond
;; [(empty? alon) ...]
;; [(cons? alon) ... (first alon) ...
;; ... (lon-fn (rest alon)) ...]
;;
;;; Signature:
;;lon-size: LoN -> Number
;;;; Purpose
;; Given a list of numbers return the number of elements in the list.
;; +---------------+
;; \/ ;; |
(define (lon-size alon) ;; |
(cond ;; |
[(empty? alon) 0] ;; |
[(cons? alon) (+ 1 (lon-size (rest alon)))]))
Now in Java
Same idea, itemization, classes and a self reference
Adding a size()
operation will require the following modifications
-
Add method
Integer size()
onList
-
Implement
size()
in each class that implementsList
, i.e.,Empty
andCons
Here is the updated UML diagram
And here is the Java code
/**
* Represents a List of Integers
*/
public interface List {
/**
* Returns the total number of elements in the list.
*
* @return number of elements in this list
*/
Integer size(); (1)
/**
* Returns true if empty and false otherwise
*
*/
Boolean isEmpty();
/**
* Given a new element {@code element} prepend it to this list
*
* @param element new element to add to the list
* @return updated list with {@code element} prependeds
*/
List add(Integer element);
/**
* Return the last element of this list.
*
* @return the last element of this list.
*/
Integer last();
}
/**
* Represents the empty list of integers.
*/
public class Empty implements List {
@Override
public String toString() {
return "Empty{}";
}
/**
* {@inheritDoc}
*/
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
else return true;
}
/**
* {@inheritDoc}
*/
@Override
public int hashCode() {
return 42;
}
@Override
public Integer size() { (2)
return 0; (3)
}
@Override
public Boolean isEmpty() {
return true;
}
@Override
public List add(Integer element) {
return new Cons(element, this);
}
@Override
public Integer last() throws InvalidCallException {
throw new InvalidCallException("Called last() on empty!");
}
}
/**
* Represents a non-emty list of integers
*
*/
public class Cons implements List {
private Integer first;
private List rest;
/**
* Given an integer and a list create a new list with the
* same elements as {@code rest} and with {@code first} prepended.
*
* @param first new element to add to the beginning of the list
* @param rest the list we are going to use to add our new element
*/
public Cons(Integer first, List rest) {
this.first = first;
this.rest = rest;
}
/**
* Getter for property 'first'.
*
* @return Value for property 'first'.
*/
public Integer getFirst() {
return first;
}
/**
* Getter for property 'rest'.
*
* @return Value for property 'rest'.
*/
public List getRest() {
return rest;
}
/**
* {@inheritDoc}
*/
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Cons cons = (Cons) o;
if (first != null ? !first.equals(cons.first) : cons.first != null) return false;
return rest != null ? rest.equals(cons.rest) : cons.rest == null;
}
@Override
public String toString() {
return "Cons{" +
"first=" + first +
", rest=" + rest +
'}';
}
/**
* {@inheritDoc}
*/
@Override
public int hashCode() {
int result = first != null ? first.hashCode() : 0;
result = 31 * result + (rest != null ? rest.hashCode() : 0);
return result;
}
@Override
public Integer size() { (4)
return 1 + this.getRest().size(); (5)
}
@Override
public Boolean isEmpty() {
return false;
}
@Override
public List add(Integer element) {
return new Cons(element, this);
}
@Override
public Integer last() {
if (this.rest.isEmpty()) {
return this.first;
} else {
return this.rest.last();
}
}
}
1 | First we add the appropriate method signature on the List interface |
2 | We implement the method size() inside Empty |
3 | In the empty case the size of the list is 0 |
4 | We implement the method size() inside Cons |
5 | In the non-empty case we add 1 to the size of the rest of the list. The rest of the list
is the field rest inside Cons so we call size() on that object. |
Here is the Sequence Diagrams for size()
when called on a 3 element list.
Observer that the bodies of the methods inside the concrete classes contain similar
code to the right-hand side expression in our cond clauses.
|
Lets also add the following methods
-
a method called
Boolean isEmpty()
that returnstrue
if our list is empty andfalse
otherwise -
a method called
List add(Integer num)
that addsnum
to the beginning of the list. -
a method called
Integer last()
that returns the last element of the list -
a method called
List addToEnd(Integer element)
that adds the given element to the end of the list -
a method called
Integer elementAt(Integer index)
that returns the element at indexindex
Java Packages
Java allows you to organize your Java classes into groups. The grouping is similar (or inspired) to how you group files on your computer using folders.
The Java language provides the keyword package
that we can use to create a package and provide a name. For example if we wanted to keep all of our code for lists of integers into a package called integerlist
we add the following line at the top of each .java
file
package integerlist;
/**
* Represents a List of Integers
*/
public interface List {
/**
* Returns the total number of elements in the list.
*
* @return number of elements in this list
*/
Integer size(); (1)
/**
* Returns true if empty and false otherwise
*
*/
Boolean isEmpty();
/**
* Given a new element {@code element} prepend it to this list
*
* @param element new element to add to the list
* @return updated list with {@code element} prependeds
*/
List add(Integer element);
/**
* Return the last element of this list.
*
* @return the last element of this list.
*/
Integer last();
}
The naming convention for Java packages is to keep all names lowercase. In order to refer to the List
interface using it’s full qualified name we write
-
integerlist.List
We can also created nested packages, just like you can create nested folders. We separate nested packages with a .
, e.g.,
package edu.neu.ccs.cs5004.lecture3.integerlist;
/**
* Represents a List of Integers
*/
public interface List {
/**
* Returns the total number of elements in the list.
*
* @return number of elements in this list
*/
Integer size(); (1)
/**
* Returns true if empty and false otherwise
*
*/
Boolean isEmpty();
/**
* Given a new element {@code element} prepend it to this list
*
* @param element new element to add to the list
* @return updated list with {@code element} prependeds
*/
List add(Integer element);
/**
* Return the last element of this list.
*
* @return the last element of this list.
*/
Integer last();
}
When we create a packages the Java language requires that your save your .java
in a folder structure that *reflects your package’s name and nesting. So the preceding nested package example must have the following folder structure
edu
└── neu
└── ccs
└── cs5004
└── lecture3
└── List.java
If you use your IDE to create a package (by selecting New → Package instead of New → Class the IDE will create the necessary folder structure.
We use fully qualified names when we use Java’s import
to include code from other libraries. By default java.lang
is always imported so we can omit importing java.lang
. Recall that the Java Documentation as well as your documentation uses package names to organize your Javadoc’s generated HTML pages.
Accessing Class properties
Here is a table that explains what can be accessed from which location (class, package, world) for each Java modifier (public
, private
, protected
and default) [4]
Access Modifier | Same Class | Same Package | Subclass | Other Packages |
---|---|---|---|---|
|
Y |
Y |
Y |
Y |
|
Y |
Y |
Y |
N |
|
Y |
N |
N |
N |
default |
Y |
Y |
|
N |
In this course avoid using default. |
Exceptions
An exception is an event that occurs during execution of a program and disrupts the normal flow of execution.
Three kinds of exceptions
-
Checked Exceptions capture events that a well-written application must anticipate and recover from.
-
opening a file using the file name provided by the user and cannot find that file
-
opening a connection to a server whose address was provided to the program by an external entity
-
-
Errors capture exceptional conditions external to the application that the application cannot anticipate or recover from.
-
error reading from the disk
-
error reading from the connection to the database
-
-
Runtime Exception captures events that are exceptional and internal to the application. The application typically cannot anticipate them or recover from
-
passing in the incorrect values for the arguments to a function
-
logical errors; going over the length of a list
-
Java’s exception hierarchy dictates the kind of exception (checked, runtime or error).
Java provides special syntax to mark code that could throw an exception as well as catching an exception. Let’s look at an example.
/**
* Represents an exception thrown when an invalid value is given for radius
*/
public class InvalidRadiusException extends RuntimeException {
/**
* {@inheritDoc}
*/
public InvalidRadiusException(String message) {
super(message);
}
}
Since our InvalidRadiusException
extends RuntimeException
it is not a checked exception. It is a runtime exception.
We can then throw this exception when appropriate in our Circle
's constructor method using the
Java keyword throw
and passing an object of our exception.
public class Circle extends AbstractShape {
/**
* Given a pin and a radius greater than 0, creates a circle
*
* @param pin the location of this circle's pin
* @param radius this circle's radius. The radius must be greater than 0
public Circle(Posn pin, Integer radius) {
super(pin);
if (radius <= 0) {
throw new InvalidRadiusException("Radius must be > 0, given: " + radius);
}
this.radius = radius;
}
// elided code
}
Code that calls the constructor of Circle
can optionally catch the exception and accordingly do something about this abnormal case. For example one possibility would be
class StickFigure {
private Circle head;
private Body body;
public StickFigure(Integer head, Integer body) {
try {
this.head = new Circle(new Pin(0,0) head);(1)
} catch (InvalidRadiusException invalidRadius) {
ShowErrorMEssage errorMessage =(2)
new ShowErrorMessage("We detected an incorrect value " +
"for the stick figure's head. " +
"Please provide a positive number.");
new Window(errorMessage).exit();
}
this.body = new Body(body);
}
}
1 | Code that is calling a method that can throw an exception can try to call the method. If there is not exception because of the execution of the code in the try -block then Java jumps over the catch -block and continues as normal |
2 | if the execution of the code inside the try -block does throw an exception then Java will check each catch statement to see if there type of the thrown exception matches the argument of the catch -block. If it does, then run the code inside the catch -block and exit. |
If there is no catch
-block that matches the thrown exception, Java will traverse all the callers that lead to the execution of this code. If none of the callers deal with the thrown exception the JVM will exit and print out the details of the thrown exception on your Console.
Java allows us to have more than one catch
-blocks.
try {
// try to run these expressions/statements that may throw an exception
} catch (ExceptionType ext) {
// catch an exception of type ExceptionType, bind it to the variable ext
// perform actions to recover
} catch (ExceptionType2 ext2) {
// catch an exception of type ExceptionType2, bind it to the variable ext2
// perform actions to recover
} catch (IOException | SQLException ex) {
// catching multiple exceptions for which we deal with in the same
// way
}
In the case where your exception is a checked exception, any method that might throw the checked exception must [5] declare it’s intend to do so in the method header.
Syntax to declare that a method can throw an exception (of any kind)
public Integer method() throws RuntimeException, ArgumentException, NetworkException {
}
Always create your own exceptions. Do not rely on the String message allowed by
the already defined exceptions.
|
Testing methods that throw exceptions in JUnit
JUnit can be used to check your methods even under conditions that throw exceptions.
To tell JUnit that you expect an exception to the thrown we add to the @Test
annotation
the expected exception
@Test(expected=MyException.class)
public void testElementAt() throws Exception {
//call the method in a way that causes an the expected exception to be thrown
}
Let’s add a test for our Circle
class that will validate the use case when we pass
a value for radius
that is not a positive integer.
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
public class CircleTest {
private Circle incorrectCircle;
private Circle circleRadius10;
@Before
public void setUp() throws Exception {
// this.incorrectCircle = new Circle(new Posn(1,1),-1);
this.circleRadius10 = new Circle(new Posn(10,10), 10);
}
@Test
public void testGetRadius() throws Exception {
Assert.assertEquals(this.circleRadius10.getRadius(), new Integer(10));
}
@Test(expected=InvalidRadiusException.class)
public void testInvalidRadius() throws Exception {
this.incorrectCircle = new Circle(new Posn(0,0), -1);
}
}
Also let’s see what happens if we call Circle
's constructor without using a try
-catch
block.
We create a simple class UseCircle
with only one method.
/**
* Created by therapon on 5/24/16.
*/
public class UseCircle {
public Circle createAnInvalidCircle(){
return new Circle(new Posn(0,0), -1);
}
}
and a corresponding test for UseCircle
called UseCircleTest
import org.junit.Before;
import org.junit.Test;
public class UseCircleTest {
private UseCircle uc;
@Before
public void setUp() throws Exception {
this.uc = new UseCircle();
}
@Test
public void createAnInvalidCircle() throws Exception {
uc.createAnInvalidCircle();
}
}
And here is the output on our Console when we run the tests inside UseCircleTest
InvalidRadiusException: Radius must be > 0, given: -1
at Circle.<init>(Circle.java:10)
at UseCircle.createAnInvalidCircle(UseCircle.java:8)
at UseCircleTest.createAnInvalidCircle(UseCircleTest.java:14)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:497)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:119)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:42)
at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:234)
at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:74)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:497)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144)