With this organizational structure of our projects and source code from last time in mind, let’s now focus on the different design paradigms for what we actually implement.
The main design paradigms that most development falls under revolve around the notion of state.
Question
What does it mean for a program to maintain state?
Answer
State is, generally speaking, the values of some variables of interest that are maintained for the program to perform
necessary recordkeeping.
Remark
Crucially: Some programmatic tasks are best accomplished by maintaining state, while others do not need it.
The distinction between whether a task demands state or not distinguishes Functional Programming from Object-Oriented Programming:
Gist
Functional
Organizes flow of a program as a stateless sequence of function calls, without side-effects between each.
Object-Oriented
Organizes flow of a program as a stateful sequence of commands to objects, preserving state between each.
Examples
Functional
Calls to the Math.pow(a, b) function, which returns a^b
Math.pow(3, 2) => 9.0Math.pow(2, 3) => 8.0
Object-Oriented
Designing a video game player that has some health, which can be doubled with the doubleHealth() method.
player.health => 10player.doubleHealth(); // player.health now 20player.doubleHealth(); // player.health now 40
Explanation
Functional
Calls to Math.pow do not affect each other, no state is carried over from one call to next.
Object-Oriented
Commands to the player object in the form of doubleHealth() depend on its current state in player.health;
Java Implementation
Functional
Methods and properties of the class possess the static modifier, demonstrating that they belong to the class.
Object-Oriented
Methods and properties of the class that are NOT tagged as static belong to individual objects / instances of the class.
So, we can see that some tasks demand the maintenance of state while others do not, and a crucial part of the study of data structures includes how
state is managed in the cleanest, most efficient way (also, for data that is far more complex than a single int like player.health!).
Remark
Remark: this is a very scant overview of these paradigms, which you’ll revisit in great detail in your Programming Languages
class.
Some last notes of mention on the above:
Most large tasks will involve a mixture of functional and object-oriented tools, but in development of those tools, you will generally be working in one
style or the other at any given time.
This course (CMSI 2120) will focus largely on Object-Oriented techniques as they are so central to how we organize data in state. The next course, CMSI 2130,
will have a heavier focus on the Functional approach.
Much of this course will require us to define our own data structures from first-principles (so that we understand how they work, algorithmically).
Definition
For this purpose, we will of course be creating our own Classes using Object-Oriented Design principles
to represent our data structures.
Question
What is a Class in Java?
Answer
A Class is a template for a type! Thus far we’ve only been using Classes to define static methods and constants,
but they can also be used to template non-static objects.
We can think of Classes as blueprints from which we might create specific, concrete instances / objects.
Definition
Objects of a class are specific instances of the blueprint, each with their own state (properties) but with behaviors common to the Class.
Remark
If you think of Human being a class with certain expectations of behaviors (eat, sleep, etc.) and properties (name, age, etc.),
then Andrew would be a particular object of that Class (Andrew has his own name and age, but shares common behaviors with other humans). By analogy:
Classes : Objects :: Cookie Cutters : Cookies.
Definition
This is what it means to practice Object Oriented Programming (OOP): to define classes of objects, and use instances of those
objects to track and manipulate a program’s state.
Example
You might remember these in Python when you created classes like the following:
# Python class -- defined the blueprint for Personclass Person: # Attributes were unique to each Person instance, # and initialized by the constructor: def __init__(self, name): self.name = name # Methods define what commands each Person instance # can be given, like: def introduction (self): print("My name is " + self.name)# Outside of the class, instances can be made:dave = Person("Dave")andy = Person("Andy")# ...and methods called upon themdave.introduction();andy.introduction();
Example
Draw a pictorial representation of a single Person class “cookie cutter” with multiple Person “cookies”.
…and no, that’s not some vague reference to Soilent Green.
Designing Java Classes
Toolkit
Designing classes in the OOP paradigm involves several common class components.
We’ll look at their definitions first and then implement them ourselves in an example.
Class Components
Most classes (in any language) have several key components:
Definition
Component 1: Fields (AKA data members) are values that every class object will independently maintain, i.e., might have different values
between each object of the class.
Toolkit
Fields are typically the first thing declared in a Class with the syntax, and (as usual in Java) are typed / declared just like variables:
public class ClassName { <modifiers> type1 field1; <modifiers> type2 field2; // ...}
Toolkit
Each object maintains its own non-static fields, but shares all static fields with other members of the class.
Debug
This is why static constants are OK, but static fields are almost always the wrong approach to solving a problem — they are risky and can lead
to unexpected behavior.
If fields provide the state of an object, then we also need a means of making them do things!
Definition
Component 2: Non-Static Methods are functions that are called upon objects, declared just like static methods but lacking the
static keyword.
Remark
Generally, methods are commands or requests to objects, sometimes modifying the object’s fields as a result.
We’ve seen a couple of methods in Java’s String class already like .length() and .charAt(index).
Debug
Important: reminder that fields and methods which belong to each object of a class DO NOT have the
static keyword attached to them; we only use static when a method or field belongs to the Class itself, not an instance of that Class.
For review: this is what distinguishes functional (static) from object-oriented (non-static) implementations.
Definition
Component 3: Constructors are methods that share the class name (and no return type specified), that describe how to instantiate the fields
of a new object of that class.
Toolkit
The syntax for declaring a constructor (a class may have multiple constructors) is:
<modifiers> ClassName (parameters) { // What should happen when a new object of this // type is created using the "new" keyword}
Toolkit
Similar to Python’s self keyword for referencing the instance calling a method (including constructors), Java uses the this
keyword.
Toolkit
Constructors are called whenever we instantiate a new object, which follows the general format:
// Instantiating a new object of class TypeType obj = new Type(arg1, arg2, ...);// e.g. a Person instance whose constructor expects// only their name and agePerson yenrof = new Person("Yenrof", 29);
These are the three major Java class components that we’ll practice shortly and throughout this course!
“Motivating” Example: Forneymon
I’m glad you’re all here because I’m designing an all new battle game that in no way, shape, or form resembles a certain Nintendo franchise…
Naturally, I’m calling it Forneymon, where we have different mythical pets of different types locked in psuedo-humane combat for our amusement!
I’m starting off slow and hoping we can develop the concept a bit in this class… here’s the gist:
I have two types of Forneymon right now: the Burnymon, which singes its opponents with the fire of 5 suns, and Dampymon, which annoys its opponents by
getting them wet.
Both have a name that its trainer err… owner has given it.
Presently, Forneymon only have one behavior: taking damage of a certain type (e.g. burny damage or dampy damage).
Dampymon take 5 BONUS damage from burny attacks and start with 25 health, but Burnymon start with 15 health.
When attempting to “print out” (i.e., toString) a particular Forneymon, they will repeat their name 2 times (with a space in between).
Let’s try to design these two Forneymon classes… in class!
Animation credit to LMU’s own Niccolo Menotti!
Graphic credit to LMU’s own Niccolo Menotti, Aiden Srouji, and Aiden Dionisio!
Interfaces
Many times, you’ll be implementing classes that have to adhere to some guidelines set by someone else (cough professor or boss cough)
In any event, these guidelines are often given not only in plain-English descriptions (like above), but also in the programmatic-contract form of an interface.
Remark
Interfaces are like the buttons on our calculator analogy: they’re what define the ways that a user interacts with a class’ objects!
Definition
In Java, an interface is an outline of an implementing class’ methods that defines how a user interacts with it.
Interfaces define only the method signatures of public methods that a user is expected to employ when using your class.
Classes that implement (i.e., provide the method bodies for those methods) an interface must define at least the specified methods
(though they can have others as well) in order to successfully compile. This ensures that other parts of the code that expect definitions for certain
methods are satisfied.
Multiple classes may implement the same interface, allowing for different data structures (the implementing class) to implement the same data type
(the interface).
Toolkit
The syntax for declaring an interface:
public interface InterfaceName { // Method Signatures here}
In other words, interfaces are ways to establish a contract between your class’ implementation and the guidelines expected of it.
Just how your class implements these methods is up to you, but the signatures posed by the interface bind you to that contract.
Toolkit
Interfaces are defined in their own source files and typically list only method signatures of the class’ expected behavior.
Example
We’ll start by illustrating how a minimal interface might look for the Forneymon described above.
package forneymon;public interface MinForneymon { // Method signatures alone! public int takeDamage (int dmg, String type); public String toString ();}
Toolkit
Any classes that implement an interface must use the implements keyword in their class definition.
Example
We’ll start by scaffolding our Burnymon class as implementing the MinForneymon interface.
package forneymon;/** * Burnymon singe their opponents with the fire of 5 suns. * - Start with 15 health * - Deal burny damage */public class Burnymon implements MinForneymon { public int takeDamage (int dmg, String type) { throw new UnsupportedOperationException(); } public String toString () { throw new UnsupportedOperationException(); }}
Toolkit
Note: it is common practice to throw new UnsupportedOperationException(); before you have implemented a particular method, but which you know is required
by an interface.
Example
Try to compile your code after removing the takeDamage method above — you’ll get an error because you broke your contract with the interface!
Question
Hey, how come we didn’t get a compilation error if we didn’t include the toString implementation?
Answer
Ah, because all classes already have an implementation… but from where?! (More on that later).
Now we’re ready to start implementing our Burnymon class!
Implementing a Class
Question
I know my Burnymon has health and a name; how do I add these to my class definition?
Answer
You define fields for each.
Apropos, all Forneymon will have some name and some amount of health, but two different Forneymon might have different names or different amounts of health.
Conventionally, fields are defined at the top of a class definition.
Question
I need to specify how to create new Burneymon objects. What should I add to my class definition?
Answer
You add constructors that are generally used for populating the fields of each newly created object.
Conventionally, constructors are defined after fields in a class definition.
Question
I’ve noted that my Forneymon can take and deal damage of a particular type. How do I add behaviors to my objects?
Answer
You define methods, which are functions that are called upon objects.
Conventionally, methods are defined after constructors in a class definition.
Great! Now we have a nice looking Burnymon class:
package forneymon;/** * Burnymon singe their opponents with the fire of 5 suns. * - Start with 15 health * - Deal burny damage */public class Burnymon implements MinForneymon { int health; String name; Burnymon (String n) { this.health = 15; this.name = n; } public int takeDamage (int dmg, String type) { this.health -= dmg; return this.health; } public String toString () { return this.name + " " + this.name; }}
In a moment, we’ll look at a more principled approach to testing, but for now, let’s try out our new Burnymon.
package forneymon;public class BattleTest { public static void main(String[] args) { Burnymon emberlizard = new Burnymon("Dave"); System.out.println(emberlizard.health); emberlizard.takeDamage(5, "dampy"); System.out.println(emberlizard.health); // [!] Check out what happens when we just put the // Burnymon where a string is expected! Hmm... System.out.println(emberlizard); }}
Class Design: Best Practices
So you know how to make a class now — fantastic! …but does it follow good practices?
Let’s take a second and observe good style and development habits that will save you time, tears, and possibly employment in the future.
Good documentation
Many programmers believe that good documentation entails adding comments to every. single. line. of. code.
This is a silly (and hyperbolic) sentiment.
Definition
Good documentation clarifies only that which good naming could not make obvious.
This means that if you have named your functions and variables well, then your code and its functionality will be obvious and require little commenting.
That said, you should add comments when a particular piece of code has non-obvious functionality.
At the very least, you should:
Definition
Add a class-level comment explaining the purpose and capabilities of a class. Use the /** ... */ block comment for this.
Definition
Add method-level comments explaining the purpose, inputs, and outputs of methods. Use the /** ... */ block comment for this.
For methods, it is integral that you at least document:
The purpose / summary of the method (a brief description)
A description of each parameter
A description of the returned value (for non-void methods).
Toolkit
The nice bonus about using an IDE: most will stub any documentation expected for a given function, e.g.:
/** * Deals the specified amount of damage to this Forneymon * as moderated by the damage type. * @param dmg The amount of damage to be taken * @param type The type of the taken damage, e.g., "burny" * @return The remaining health of this Forneymon */public int takeDamage (int dmg, String type) { // ...}
Toolkit
Curious about those little @param, @return line leadings? They are used when generating javadocs for a project, which can be generated
at the command line or with the assistance of an IDE.
If you’ve never seen javadocs generated for a project, you should definitely try it!
Question
Why is it important to practice good documentation?
Answer
Many reasons, not least of which: (1) it helps structure your own algorithms and clarify the flow of your code; (2) it helps you remember
what a particular piece of code does when you haven’t seen it in awhile; (3) it helps others understand your code long after you’ve quit your job and have left the
legacy support to the poor intern who barely makes any money.
Field Access Restriction
Notice that we put no access modifiers on our Burnymon’s fields. Was this a good idea?
Let’s do a small test, modifying our BattleTest.java.
package forneymon;public class BattleTest { public static void main(String[] args) { Burnymon emberlizard = new Burnymon("Dave"); emberlizard.takeDamage(5, "dampy"); emberlizard.health = 1000; // HAHA! System.out.println(emberlizard.health); }}
Hacks! Cheats! Someone has evaded our fool-proof security to modify emberlizard’s health to an absurd amount!
Definition
A good practice in class design is to restrict access to fields so that they cannot be modified or read except through the methods that we define.
Toolkit
We can use the Java access modifiers to make fields private when we don’t want users of our class to be able to access the fields directly.
By tagging fields as private, the fields can only be accessed directly (i.e., through the dot access operator) in that class’ methods.
Let’s make that change now:
...private int health;private String name;...
Now, if we try to re-compile BattleTest.java, we get an error! Huzzah! Hackers stopped cold.
Suppose we want to allow users to be able to read, but not modify a class’ fields.
Toolkit
We add simple methods to our class called “getters” (to read a field) and “setters” (to set the value of a field).
This is as simple as adding the following to our updated Burnymon class:
Now, whenever we want to read the health and name outside of class methods, we can just use the getHealth and getName methods — without fear of anything being modified
outside of our control!
Debug
Warning: getters are often more stylistically appropriate than setters. Setters can easily violate good OOP design principles; often, it is better to
provide some command / method to an object that sets some field appropriately.
This is precisely why it is better to have a takeDamage method than a setHealth; the prior makes it easier for us, the designers, to
ensure that the class’s user does not improperly tamper with its state (e.g., by stupidly setting the health to 1000000).
Unit Testing
Since we’re practicing good software engineering, we shouldn’t be satisfied with only the BattleTest.java specified above — let’s make some unit tests!
Here’s an example of a JUnit test file for our Burnymon class:
package forneymon;import static org.junit.Assert.*;import org.junit.Test;public class BurnymonTests { @Test public void testTakeDamage() { Burnymon burny = new Burnymon("Dave"); assertEquals(15, burny.getHealth()); burny.takeDamage(5, "dampy"); assertEquals(10, burny.getHealth()); } @Test public void testToString() { Burnymon burny = new Burnymon("Dave"); assertEquals("Dave Dave", burny.toString()); }}
Follow these best practices and you will find unbridled success!
Next week, we’ll continue the discussion about classes and move into the exciting world of inheritance.
After that, we’ll start learning about our first data structures!