CSE 131 Module 4: Encapsulation


Workspace Update

Update your eclipse workspace to get the lab4 package.
Update by right-clicking (control-click on Mac) the main project in the package explorer, drag down to Team... and click Update.

You should see a lab4 package in the src directory.

There will be many red flags in DemoVectorTest because you have not yet implemented the Vector class. That's OK.

Read the CSE131 Style Guide. A substantial part of your grade on this and future labs in CSE131 will be based on style, which includes clear code and documentation. Good syle is important because clarity helps avoid program errors and following standard conventions makes it easier for you and others to understand and modify your code. Expectations are described in the style guide.

This is the last lab in which you will not be penalized greatly for style problems. Do your best on style and read the TA comments carefully to avoid failing modules later on due to poor style.

Project: Creating and testing a Vector class and a Point class

In Part I, you will define a Java class that models a mathematical vector. Part II involves writing methods that implement a point. In Part III you will write a JUnit test file for verifying your implementation of Point.

You are encouraged to work on Parts I and II using test-driven development.

Test-driven development means that the code you write is governed by the tests your code has to pass. For this lab, you are provided a DemoVectorTest JUnit test case. You will use the test methods in that file to drive your implementation of Vector.

You will then write test cases as you develop your Point class.

Thorough testing is important, because you will use your Vector and Point classes in later assignments, as part of some new image manipulation and graphics tools, so you want to be sure that they work correctly.

Part I. Implementing a Vector class:

A vector is a mathematical description of a direction and a magnitude. Another way to think about a vector is a location relative to some point of origin. If you think about them this way, you can see how vectors can be added, as shown below.

  1. Open the file DemoVectorTest.java and take a look at the JUnit tests that appear in the file.
    You are to proceed with the development of your Vector class by considering these tests one at a time. For example, the init() test will require you to write the constructor for Vector.
    You will have red flags because Vector is not yet complete, but you can run the JUnit test nonetheless. If you expand the lab4.DemoVectorTest in the JUnit window, you can see which tests are passing or not. Clicking on a given test shows you the detail below in the Failure Trace window.

    A green checkmark indicates success; a dark X indicates failure; a red X indicates compilation problems not yet addressed.

    After that, the arith() test would have you implement Vector's plus and minus methods.

    Develop and then test each method one at a time! Read through the instructions below before and during your develpment to guide your writing of the Vector class.

  2. Open the file Vector.java in the provided lab4 package, and fill in your name and other information at the top. A class named Vector has been defined in the file, but so far it contains no instance variables or methods. (Notice that the class name exactly matches the file name, including capitalization. Java requires this.)
  3. Instance variables: The instance variables of a class define what kind of information each object of that class will hold. If we think of a vector as a translation in two-dimensional space, then we can represent a vector as the change in x and the change in y. In other words, we want each Vector object to contain two distances, deltaX and deltaY.
    To accomplish this, declare two instance variables in the Vector class, and name them deltaX and deltaY. Both should be of type double.

    Naming conventions: By convention, names of instance variables and methods should begin with lower case letters, to distinguish them from class names, which begin with upper case letters. If a variable has multi-word name, we usually capitalize subsequent words. For example, thisIsAVeryLongVariableName.

    Encapsulation: Objects usually contain data, and it is good design practice to make sure that this data can't be "messed with" by other classes. Other classes should call methods on the object to access the information. That way, each class can control what is seen and, more importantly, how it is modified.

    Make both of the instance variables in your Vector class private by typing the keyword private at the beginning of the declaration, before the type of variable.
    When you make the variables private, Java will make sure that the only way that code in other classes can see or modify their values is by calling methods of the Vector class.

  4. Initialization: Assigning to a variable for the first time is called "initializing" the variable. Constructurs usually have the job of initializing instance variables. When we create Vector objects, we will probably want to supply the deltaX and deltaY values for them. So, define a public constructor that takes two double parameters and assigns their values to deltaX and deltaY. Recall that a constructor always has the same name as the class.

    Tip: You can type the constructor yourself if you want, but Eclipse provides some tools for generating these kinds of constructors automatically. To create the constructor automatically, first position your text cursor on the class name at the beginning of the file. Then open the Source menu and select "Generate constructor using fields." Finally, select the boxes for deltaX and deltaY so that the constructor will have parameters that are used to initialize those instance variables.

    Name masking: A method or constructor may have a parameter whose name is the same as the name of an instance variable. For example, you might have a parameter and instance variable both with the name "deltaX." When a name is used in a program, it refers to the "closest" declaration. So any use of the name "deltaX" would refer to the parameter. In this case, we say that the parameter masks the instance variable. But inside the method or constructor, we may still want to use or change the value of the instance variable. Within a method, the keyword this always refers to the object on which the method has been invoked (i.e., "this" object). When an instance variable is masked, you can still refer to it by preceding its name with "this." For example, inside the method, this.deltaX = deltaX will assign the value of the parameter deltaX to the instance variable named deltaX inside "this" object.

  5. Implicit targets: Normally, when you call a method on a target object, you identify the target, and then identify the method and its actual parameters. For example,
    calls the deposit method on the object to which alice refers. In other words, alice is the target. If you don't identify a target, then it is assumed that the target is the same object that is currently executing a method. For example, if we call the deposit method on alice and inside of the deposit method there is an expression getBalance(), then it is understood that the method will be called on alice since that is the object in which the deposit method is executing. In such cases, it is not necessary (and considered bad style) to use the word "this" because it is already understood that this object is the target.

  6. The toString method: It is customary to provide a method called toString that takes no parameters and returns a String value that is a textual description of the object. You can call this method yourself, but Java will also call it whenever it needs to concatenate the object onto a String. Define a toString method for the Vector class that returns a textual description of the vector. For example, (new Vector(4,3)).toString() might have the value "[4 3]" as its return value. (Hint: Form this string by concatenating various characters with the deltaX and deltaY values.)

  7. Accessors: Most classes provide accessor methods that other parts of a program can call to get information from an object. For the Vector class, define two accessor methods named getDeltaX and getDeltaY that take no parameters and return the values of the instance variables deltaX and deltaY, respectively. Note: Here, the instance variables will not be masked by matching parameter names (in fact, there are no parameters at all), so there is no need to use "this." You can refer to the instance variables by their names alone. (Tip: You can type the methods yourself, or you can open the Eclipse Source menu and select "Generate Getters and Setters." Check the boxes for "getDeltaX" and "getDeltaY." Study the methods after creating them.)

  8. At this point, your DemoVectorTest JUnit test should pass its init() test.

  9. Accessors that compute their return value: Sometimes accessors provide information that is not directly stored inside the object, but is instead computed when the method is called. For example, write a method called magnitude that takes no parameters and returns a double, the length of the vector. Use the pythagorean theorem to compute the length of the vector. Recall that the method Math.sqrt(x) returns the square root of x.
  10. Mutators and immutable objects: Often, a class will provide mutator methods, such as setDeltaX, that allow controlled modification of the data stored in the corresponding instance variables. However, we will not provide mutators for the Vector class. Instead, each of our Vector objects will be immutable, meaning that once it is created, its value will never change. So, whenever we want a Vector with a different direction or magnitude, we will have to create a new object.

    Since Vector objects will be immutable, the rest of these methods will create new vectors as their return values. They'll do this by first computing the desired deltaX and deltaY values, and then using the Java keyword new to call the constructor you wrote earlier. You can define local variables inside the methods whenever it's convenient, but remember that the final result of each method will be a new vector. Don't modify the object on which the method was called.

    Note: You may want to add the word final to the declaration of your instance variables. This indicates that the instance variable's value should be established by the constructor and not be changed by any other method. Adding final will prevent you from accidentally changing the value in the methods you write for the Vector class.

  11. Define a method called deflectX that takes no parameters and returns a new vector that is identical to "this" one, except that its deltaX component has the opposite sign. For example, if this vector is [-3 4], then the new vector would be [3 4]. In other words, the method creates a vector oriented in the opposite x direction.
  12. Define a method called deflectY that takes no parameters and returns a new vector that is identical to "this" one, except that its deltaY component has the opposite sign.

  13. Define a method called plus that takes another vector as its parameter and returns a new vector that is the sum of this vector and the one provided as input. Recall that to add two vectors, you add their x-coordinates and their y-coordinates. For example, suppose you have a vector with value [3 4] and you call the plus method on it, passing in a vector with value [-5 2]. Then the vector returned by the method should have the value [-2 6].

    Hint: The parameter type and return type of this method are both Vector. When you create the new vector to be returned, you will need to supply parameter values. To compute those parameter values, you can use both "this" vector (the one on which the method was called) and the vector that was passed in as a parameter.

  14. Define a method called minus that takes another vector as its parameter and returns a new vector that is the difference of this vector minus the one provided as input.
  15. Challenge: Write minus in terms of methods you have already defined for the Vector class. Which computer science principle are you applying by doing that?

    At this point, your DemoVectorTest JUnit test should also pass its arith() test.

  16. Define a method called scale takes a double named factor as its parameter. When you call this method on a vector, it should return a new vector whose direction is the same, but whose magnitude has been multiplied by the given parameter.
    Recall: Scaling a vector by some factor can be accomplished by scaling its deltaX and deltaY components by that same factor. Be sure to return a new vector, and don't change the one on which this method was called.

    At this point, your DemoVectorTest JUnit test should also pass its scale() test.

  17. Define a method called rescale takes a double named magnitude as its parameter. When you call this method on a vector, it should return a new vector whose direction is the same, but whose magnitude is the one supplied as the parameter.
    Hint: First call the magnitude method to find this vector's magnitude and save it in a local variable. Use this to compute a scale factor, and then let the scale method do the rest of the work.
    NOTE: If the target of the rescale method has a zero magnitude, no particular direction is defined for the resulting vector. One could consider this an error condition, but for the purposes of this assignment, if the original magnitude is zero, let the resulting vector have deltaX equal to the given magnitude, and deltaY equal zero.
At this point, make sure your code passes all the DemoVectorTest cases provided with this lab. Do NOT change the test code (we are watching and will know if the revision changes on that file).

Part II: A Point Class

Note: You are encouraged to develop Point as you did Vector, using test-driven development. However, this time you have to write the tests yourself. So try to do parts II and III simultaneously.
  1. In the Package Explorer, select the lab4 package and use the File menu to create a new class named Point in that package. In the Point.java file that is created, write an implementation according to the specificaion described in the following steps. Just as for the Vector class, create a separate JUnit test case for the Point class, and write tests for the Point constructor and each method. Test-driven development is recommended. Write the tests as you go, rather than at the end.

  2. Instance variables: Since a point is just an (x,y) pair, declare two instance variables (of type double) to hold the x and y coordinates of the point. These variables should be private to prevent changes from outside. The constructor and all methods in this class should be public.

  3. Constructor: Write a constructor that takes x and y as parameters, and initializes the new Point object with the values that are passed in.

  4. Accessors: Create getX and getY methods that return the coordinate values.

  5. toString: Write a toString method that returns a String representation of the point. For example, if x is 3 and y is -2, you might return the String "(3.0, -2.0)".

  6. Adding a Vector to a Point: It doesn't make sense to add two points together, but it does make sense to add a vector to a point. The result is a new point that differs from the old point by the deltaX and deltaY of the vector. For example, if we have the point (3,-2) and we add the vector [4 1], we will get the point (7,-1). Write a plus method of the Point class that takes a Vector as a paramter and creates a new point that results from adding this point to the given vector.

  7. Subtracting Points:When you subtract one point from another, you get a Vector, as shown in the figure below. As an example, suppose point P is (4,2) and suppose that point Q is (1,6), then P-Q would be the vector [3 -4]. (To see why this makes sense, consider what point you would get by adding the vector to Q.)

    Write a minus method of the Point class that takes another Point as its parameter and returns the appropriate vector.

  8. Distance to another point: Write a method that takes another point as a parameter and returns the distance between this point and the given point.
    Try to implement this by reducing this problem to calls of methods you have already written

  9. Test all of your methods thorougly by writing tests in the PointTest.java file.

Part III: Testing with JUnit

Thorough testing is very important to help ensure that software works as expected. JUnit is a feature of Eclipse that supports automated testing.

To begin testing your Point class, first create a JUnit test, as follows.

  1. In the Package Explorer window of Eclipse, right click on the Point class and select "New -> JUnit Test" from the popup menu.
  2. A dialog box will come up showing PointTest as the name of the test class. It is important that you do the following in the dialog box:
    1. At the top, select "New JUnit 4 test." (We are not using the older JUnit 3.8)
    2. At the bottom, you may see a warning that JUnit 4 is not on the build path. In that case, click on "Click here" in that message to add JUnit 4 to the build path. What this means is that the Java compiler will know to look in the JUnit package to find the relevant files to compile your tests. Another dialog box will come up to add to the build path. JUnit 4 should be selected already. Just click "OK."
    3. Click "finish."
  3. A new class called "PointTest.java" will appear in your project. This is where you will put your test cases. Every file that you turn in should begin with a header comment that includes your name, lab section, etc. Copy the header comment from one of your other files and modify the information as appropriate.
  4. Just above your header (but after the line that says "package lab4"), add the following lines so that the compiler will import certain JUnit features:
    import static org.junit.Assert.*;
    import org.junit.Test;
  5. A test method: Now you're ready to begin writing some tests your Point class. Model these after what you saw in DemoVectorTest, but confine yourself (mostly) to testing a Point's behaior.
  6. Things to remember about JUnit: Every test method must be preceded by the annotation "@Test" on the line before the start of the method. Also, JUnit requires that every test method be public and have a "void" return type, meaning that it does not return a value. The method assertEquals is available for use in JUnit test cases. It takes two parameters: The first parameter should be what you "expect" the value to be, and the second parameter is an expression that is supposed to equal that expected value. The assertEquals method checks to see if the two parameter values are equal. When comparing doubles, you can provide a third parameter to specify an amount by which the first two parameters are allowed to differ. When an assertEquals fails, the test will fail and JUnit will show you a failure trace so you can diagnose the problem. But the first thing to check is whether you have written the test correctly!

    To run the JUnit test, right click on VectorTest in the Package Explorer, and select "Run as -> JUnit test." After running a JUnit test, results are shown in a JUnit tab at the left. Is the bar green? If so, congratulations! Your constructor, toString, and accessors must be working. If the bar isn't green, you'll see a failure trace. Click on the line of the failure trace that's inside your VectorTest program, and you will see where the failure occurred. Then you can start checking your Vector implementation to see what's wrong. After fixing the problem(s), rerun the test case to make sure that the bar is green. To rerun the test case, click the little green arrow in the JUnit window, or right click on the TestVector class and select "Run as -> JUnit Test."

  7. Writing your own test methods: Create test methods inside the PointTest class in concert with your development of the Point class. Create a test method that checks the correctness of each of your Point methods. In each test case, you will create one or more Point and/or Vector objects as necessary, call one or more methods, and have one or more assertEquals statements to check the results (or call a helper method that will eventually do the assertEquals.

    We recommend adding one test case at a time, perhaps writing each test as you complete each method of the Vector class.

    Ideally, use a test-driven development methodology, as discussed above. Run the test case, fix the method it is testing (if necessary), and then add another method to the Point class and a new corresponding test method to your PointTest class. Don't delete any of the previous test methods. By the time you are done, you'll have a number of methods that each test a different part of your class.

    Remember that when the parameters are of type double, assertEquals can take a third parameter, which is the tolerance within which you are willing to treat the values as equal. For example, assertEquals(5.0, m, 0.025) says that the value of m is expected to be within 0.025 (+/-) of the value 5.0.

    Be sure to create test cases that cover not only the "normal" cases, but also cases that are likely to cause errors. Your goal in PointTest is to make the Point class fail so you can find errors and fix them.

To Get Credit

Submitting your work (read carefully)

Last modified 22:14:02 CST 11 November 2009 by Ron K. Cytron