© Jason Lee Hodges 2019
J. L. HodgesSoftware Engineering from Scratchhttps://doi.org/10.1007/978-1-4842-5206-2_8

8. Classes

Jason Lee Hodges1 
(1)
Draper, UT, USA
 

During the industrial revolution, Eli Whitney, while striving to produce and fulfill an order of 10,000 muskets for the US military, invented the manufacturing concept of interchangeable parts. His interchangeable parts were components of the weapon that were so near identical that they would fit into any assembly of that same type of weapon. This made it possible to mass produce individual parts with huge productivity gains and assemble the weapons at a later time as necessary. Prior to this methodology, weapons were created one at a time from start to finish, usually by a blacksmith, and each weapon was unique. By creating interchangeable parts, not only did Eli Whitney make weapons that were easier to repair through replaceable component standardization, but he also laid the fundamental groundwork for mass manufacturing strategies to come (such as the moving assembly line).

In software engineering, writing the same code multiple times throughout your program is comparable to building a weapon from scratch each time in terms of each method’s relative lack of productivity. In addition to the productivity loss of writing the same code twice, by writing duplicative code, you’ve also introduced multiple points of failure and maintenance rather than a single accountable piece of code. In contrast, if you “Don’t Repeat Yourself” or ensure that you are writing DRY code, this is analogous to instead using an already mass produced interchangeable part to assemble that weapon. Thus, it could be said that the invention of interchangeable parts was the catalyst for modularity in manufacturing. In the previous chapter, you were introduced to functions as an idiom of creating modularity in your code. In this chapter, you will be introduced to another strategy for modularity known as classes.

Encapsulation

Classes are essentially custom data types in your code that capture or encapsulate certain values and functionality into a single namespace. Just like a String type has certain properties about it (like its length and its values at certain indexes) and operations that can be performed on it (like concatenation or interpolation), a class or custom type can be defined with properties and methods. A property or field is simply a variable that is contained within the class, and a method is simply a function contained within the class. Properties and methods are considered members of a class. Given that, it could be said generically that a class is simply a collection of members.

To create your own class in Scala, you first use the class keyword followed by the name you want to give to your custom type. By convention, the name usually starts with an uppercase letter. After you define the name of the class, you provide parameter variables inside parentheses just like you would a function. These parameter variables will populate the properties of the class. Next, you define the body of the class, which usually contains some default behavior that you want to accomplish each time someone uses the class, as well as functions or methods and any additional properties that you want to be able to access within the encapsulation of the class. Listing 8-1 demonstrates a basic example of creating and using a class. Using a class is typically referred to as instantiating a class or constructing a new class.
examples.sc
class Weapon(weaponType: String = “Musket”) {
    println(s"Construction of ${this.weaponType} completed.")
}
var musket = new Weapon()
for(i <- Range(1,10001)){
    new Weapon(s"Musket #${i}")
}
Listing 8-1

Defining and constructing a Weapon class

You’ll notice that, just like in a function definition, when defining the parameters of the class, you can provide default values to their variables. By doing that, if the Weapon class is instantiated or constructed without any arguments passed to these parameters, it will default to the value provided in the definition. To construct or instantiate a class, you use the new keyword followed by the name of the class with parentheses that contain the arguments you want to pass to the class. If you execute the code in Listing 8-1, you will see that the first instantiation of the Weapon class defaults to printing out the word “Musket” as its weapon type since no argument is passed to the weaponType parameter. After that, the for loop constructs 10,000 instances of the Weapon class and passes in the i variable to the weaponType parameter to denote in which iteration of the loop the weapon was constructed.

You might have noticed in the println function of the class body in this example that the weaponType variable was accessed using a this keyword. The this keyword is not necessary in this case but was provided to demonstrate a key point. In classes, when you provide arguments to parameter variables, they are assigned to the newly constructed instance of the class as a property, and the this keyword is simply referring to the individual instance of the class that was constructed. This default assignment functionality happens in what is known as the primary constructor method. The primary constructor method is a default function that is built into the creation of a class that takes any arguments passed to parameters and assigns them as properties of the instance of the class. After the primary constructor method is finished executing, Scala will then execute the body of the code that immediately follows the class definition. This execution is known as the secondary constructor method since it executes each time a class is instantiated. In other languages, there is typically only one constructor method, and it is usually accessed by defining a method in the class with a special keyword.

Once you have constructed a new instance of a class, you now have an encapsulated namespace for calling properties and methods of the class. That being said, one of the main benefits of encapsulation is that you as the developer can obfuscate parts of the functionality of the class away from whoever is using it (abstraction). In the case of a String type, when the string is constructed, it assigns a value to the length property without us needing to know how it calculated the length. That property is available to us as the developer by typing .length after the string to access the length property. So, now that we have created our own type, let’s access the single property of the Weapon class in Listing 8-2.
examples.sc
...
println(musket.weaponType)
Terminal Output
examples.sc: error: value weaponType is not a member of this.Weapon
println(musket.weaponType)
               ^
one error found
Listing 8-2

Attempting to access a property of the Weapon class

Unfortunately, when trying to access the property of the instance of our class, we encounter an error suggesting that the field we are attempting to access is not a member of the class that was instantiated. This is because, by default, all properties of the class are considered private unless explicitly deemed public for developer use, further providing obfuscation capabilities to the developer who defines the class. You can change the property to be public for use by adding a variable assignment keyword before the parameter in the primary constructor (either var if the value needs to change at any point during its lifespan or val if it can remain immutable). By adding the variable assignment keyword in the parameter definition of the primary constructor, you are telling the class that the parameter needs to be assigned to an instance variable of the class (otherwise known as a property or field). But, why would a developer want to keep some properties private and other properties public? In some cases, when you define a class, you will want to control the developer experience (DX) of the downstream developers who will be using your class. Perhaps you don’t ever want them accessing a property directly, or perhaps you want to control how the property is stored in the instance variable. In these scenarios, a best practice is to use methods known in programming as getters and setters. Listing 8-3 demonstrates how to make an instance variable public for use as well as examples of getters and setters.
class Weapon(var weaponType: String = "Musket", barrelLength: Int) {
    private var length = s"${barrelLength} inches"
    def getBarrelLength(): String = {
        return s"The barrell is ${this.length} long"
    }
    def setBarrelLength(length: Int){
        this.length = s"${length} inches"
    }
}
var musket = new Weapon("Musket", 36)
println(musket.weaponType)
println(musket.getBarrelLength)
musket.weaponType = "Big Musket"
musket.setBarrelLength(40)
println(musket.weaponType)
println(musket.getBarrelLength)
Terminal Output
Musket
The barrell is 36 inches long
Big Musket
The barrell is 40 inches long
Listing 8-3

Demonstration of public and private variables in a class

Notice that the secondary constructor sets a new instance variable, length, in addition to the parameters defined in the primary constructor. This variable is preceded by the private keyword to keep it protected from developer access similar to the default behavior for the barrelLength parameter in the primary constructor. The secondary constructor takes whatever argument was provided to the barrelLength parameter and stores it as a string along with its unit of measurement (inches). After the secondary constructor, two methods are defined to access and modify the barrelLength property, getBarrelLength and setBarrellLength. These are examples of a getter method and a setter method. The getter method returns a string that provides a message that we have tailored to ensure a good downstream developer experience. The setter method ensures that any integer passed into the setBarrelLength method gets stored in our private length variable as a string with a unit of measurement just as the secondary constructor did upon the creation of the instance of the class. We can therefore ensure that any time the property is accessed via the getter, it will always have a unit of measurement attached to it. Many languages require the use of getters and setters and do not allow for direct access to properties of a class. This can lead to an awfully verbose set of unnecessary boilerplate code if you don’t need to control the experience behind getting and setting variables. Thus, in Scala, it is a best practice to only follow the getter and setter pattern when necessary and to protect your variables when necessary. Otherwise, you can stick to Scala shorthand to allow the downstream developers to access the properties of the class directly.

Object Reference

It’s important to understand that class encapsulation creates an instantiated object that is not accessible from other objects but can be referenced from other variables. It is possible to store a newly instantiated class of type Weapon in one variable and then set another variable equal to the first variable. Now, two variables are both pointing to the same instantiated Weapon object in memory. This is known as object reference. Either variable can access the getter and setter methods of the underlying object to modify them, and the other variable will have access to the changes created by the original variable. However, if another object is created and stored in its own new variable, access to its getter and setter methods will not modify anything about the original object. Listing 8-4 demonstrates this object reference functionality using the Weapon class defined in Listing 8-3.
var johnsMusket = new Weapon("Musket", 36)
var janesMusket = johnsMusket
var jimsMusket = new Weapon("Heavy Musket", 42)
janesMusket.setBarrelLength(40)
jimsMusket.setBarrelLength(45)
println(johnsMusket.getBarrelLength)
println(jimsMusket.getBarrelLength)
janesMusket = jimsMusket
janesMusket.weaponType = "Jim and Jane's Musket"
println(jimsMusket.weaponType)
println(johnsMusket.weaponType)
Terminal Output
The barrell is 40 inches long
The barrell is 45 inches long
Jim and Jane's Musket
Musket
Listing 8-4

Demonstration of object reference functionality

Notice in this example that John’s musket was originally instantiated with a length of 36 inches. Then the variable janesMusket is assigned to John’s musket. Both johnsMusket and janesMusket are referencing the same object in the computer’s memory. Separately, Jim has a musket instantiated with the weapon type of “Heavy Musket” and a length of 42 inches. Once all of the variables have been assigned to reference an object, janesMusket calls the setter method to change the barrel length on the object that it references and jimsMusket does the same. Next, the barrel length of johnsMusket and jimsMusket are printed to the terminal. Notice that when janesMusket made a call to the setter method, it changed the value of the barrel length of John’s musket from 36 inches to 40 inches. When jimsMusket made a call to its setter method, it changed its barrel length from 42 inches to 45 inches but did not impact Jane or John’s musket at all.

After those two print statements occur, the program reassigns janesMusket to the object referenced by the jimsMusket variable. Now, both janesMusket and jimsMusket are pointing to the same object in memory, and only the variable johnsMusket has access to the object that represented John’s musket. The janesMusket variable then makes a direct call to set the weaponType of the object it references to “Jim and Jane’s Musket.” Then we print out the weaponType of jimsMusket to verify that the change to the janesMusket weaponType actually changed the weaponType of their shared object. Next, we print out the weapon type of John’s musket to ensure that the changes to Jane and Jim’s weapon type do not impact John’s musket.

If, for some reason, you wanted to reassign the johnsMusket variable to either jimsMusket or janesMusket, then all three variables would be pointed to the same object in memory. At that point, the object that was originally instantiated to represent John’s musket now has no variable reference and your program will no longer be able to access it anywhere. In order to free up memory, Scala will then collect that non-referenceable object and delete it in a process known as garbage collection.

It is important to have a thorough understanding of object reference because it is possible to have a class that is instantiated with arguments that are references to other instantiated classes. You could think of this as a kind of nesting of classes. Understanding that a referenced object can be modified from another part of your program and would therefor update the properties of a nested class might make you think twice about whether or not you should protect the properties of that nested class instead of allowing them to stay public.

Also, when comparing equality between two instantiated objects, the expression will only validate to true if the objects are the exact same reference in memory. If you wish to customize the way Scala compares two similar objects of the same class type, you can instead override the built-in equals method of the class. Listing 8-5 demonstrates the override of this built-in equals method as well as object reference comparison.
examples.sc
class Weapon(var weaponType: String = “Musket”, barrelLength: Int) {
    private var length = s"${barrelLength} inches"
    def getBarrelLength(): String = {
        return s"The barrell is ${this.length} long"
    }
    def setBarrelLength(length: Int){
        this.length = s"${length} inches"
    }
    override def equals(comparableObject: Any): Boolean = {
        return (comparableObject.asInstanceOf[Weapon].weaponType == this.weaponType && comparableObject.asInstanceOf[Weapon].getBarrelLength == this.getBarrelLength)
    }
}
var weapon1: Weapon = new Weapon("Rifle",50)
var weapon2 = new Weapon("Rifle",50)
println(weapon1 == weapon2)
Listing 8-5

Comparing equality between instantiated class objects

In this example, if you comment out the override of the default equals method, the print statement will print false because weapon1 and weapon2 do not point to the same object in memory. However, by overriding the default equals method, we can check each individual property of the class for equality and return an overall true or false if all the properties of the object are the same. You’ll notice that the method signature requires an Any type for the comparableObject parameter. The parameter can be named anything you like, but in order to explicitly override the built-in equals method, the type of the method signature needs to match exactly (so it needs to take in an Any type and return a Boolean). Due to that, in the body of the method, we have to explicitly cast the comparableObject to a Weapon type using the .asInstanceOf method in order to access the getter and setter methods and the weaponType instance property. Because the weaponType of both instantiated objects is “Rifle” and they both have a barrelLength of 50, our override equals method will now return true when comparing equality for these two objects.

You might also notice that in this example when instantiating weapon1, the type of the variable that it is stored in is explicitly set to a Weapon type. This was done for the sake of example; however, it is typically considered redundant to specify the custom type that you are instantiating when you store a new class object in a variable. You can simply let Scala implicitly assign the variable to the class type as shown in the assignment to weapon2. Worthy to note that you could also have explicitly assigned the variable an AnyRef type as all custom types or classes are sub-types of the AnyRef type.

Up to this point, we have been printing out properties of the class object. However, what would happen if you tried to print out the object itself? What might you expect to see in the terminal? Perhaps you would only see the name of the variable or perhaps the properties of the class. But, what if the class only has methods and no properties? Listing 8-6 demonstrates the output of that operation using the weapon1 object created in Listing 8-5.
...
println(weapon1)
Terminal Output
Main$$anon$1$Weapon@4d591d15
Listing 8-6

Printing an instantiated class object

Just like a class has a built-in equals method, it also has a built-in .toString method that is invoked implicitly when passing the object to the println function. The built-in .toString method prints out some information about the call site of the object, its type, and its location in memory. But, most likely this is not what you want when using a print in most cases. So, just like the equals method, you can override the .toString to do whatever you need to do with it. Listing 8-7 shows an override method that can be added to the Weapon class definition and an example of what it would look like when printed to the terminal.
examples.sc
...
override def toString(): String = {
        return s"Weapon(type: ${this.weaponType}, length: ${this.length})"
    }
...
var weapon1: Weapon = new Weapon("Rifle",50)
println(weapon1)
Terminal Output
Weapon(type: Rifle, length: 50 inches)
Listing 8-7

Overriding the default toString method of a class

It should be pretty obvious that this overridden .toString method is now much more usable. It gives us the type of the variable as well as values for each of its parameters in a customized way that we defined.

Exercise 8-1

Create your own custom class that represents a Date object. Your Date class will need to contain a property for month, day, and year. Also, add a method to subtract a day from the date and another method to add a day to the date. As you create your class, consider the following:
  1. 1.

    How will you represent months? Will they be numbers or strings?

     
  2. 2.

    Will your properties be public or private?

     
  3. 3.

    When adding a day to a date, how will the method behave if the date currently represents the last day of the month? What if it is the last day of the year?

     
  4. 4.

    How will the subtract day method behave if the date represents the first day of a month or year?

     
  5. 5.

    What format will be displayed on the terminal when printing your date? How might you provide a configuration option to allow the developer to specify what format they prefer?

     
  6. 6.

    How will you ensure that the developer passes the correct values to each property? What error messaging could you provide to the developer in the case that they get it wrong?

     

Case Classes

At this point, our class has gotten relatively big. It has a getter and setter, a private variable, and two overridden methods. You might wonder if you will need to put this much work into every single class that you create, especially if they will all need the exact same type of functionality. After all, isn’t it a fundamental rule of coding not to repeat yourself? Some languages do in fact require that you write all of this boilerplate code for every class. However, in Scala there is a shortcut known as a case class. Case classes are often used as models to represent data in your program (perhaps coming from a database) and are really useful when you need more convenient functionality by default without having to write it yourself.

By adding the case modifier keyword in front of your class definition, it automatically sets all parameters to public variables (without requiring a var or val keyword), defines an equals method similar to what we just wrote, and provides a more useful toString method. If you need to explicitly set something to private or override one of the built-in methods, you still can, but the case class simply attempts to eliminate unnecessary boilerplate code from your program. Listing 8-8 illustrates the conversion of our Weapon class to a simple case class with no getter or setter to intercept any functionality of the instance properties of the object.
case class Weapon(weaponType: String = "Musket", length: Int)
var weapon1 = new Weapon("Rifle",50)
var weapon2 = new Weapon("Rifle",50)
var weapon3 = new Weapon("Musket",32)
println(weapon1)
println(weapon1 == weapon2)
println(weapon2 == weapon3)
println(weapon2.weaponType)
println(weapon3.length)
Terminal Output
Weapon(Rifle,50)
true
false
Rifle
32
Listing 8-8

Example of a case class

Notice that because there was no functionality that had to be explicitly written or overridden in this class, there is no body to the class, making its definition extremely concise. This is really useful when you need a quick inline object in a piece of code. Just as expected, the toString method prints out a string similar to what we had written in our override method, and the equals method works exactly the same as when we compared each individual property of the class. We also can access the individual properties of the class directly without a getter or a setter and without providing a variable keyword in the primary constructor. Hopefully by walking you through the verbosity that many other languages encounter, you can fully appreciate all of the functionality wrapped up out of the box in a simple case keyword provided right before the class keyword in your class definition.

Exercise 8-2

  1. 1.

    Write a case class that represents a character in a movie. The properties of the class might be the name of the character, their height, and perhaps a catch phrase that they are famous for.

     
  2. 2.

    Unlike our Weapon object, case classes can have bodies if they need to. Create a body for your movie character and define a method that will return the catch phrase in all caps when invoked.

     
  3. 3.

    Experiment with constructing a few instances of your case class. Access its properties directly, compare the instances for equality, and print out the objects to the terminal to see how they behave.

     

Companion Objects

A companion object in Scala is a simple data structure that encapsulates members, similar to a class, but the object does not need to first be instantiated and stored in a variable to use it. The companion object is required to have the same name of the normal class that it is a “companion” to and will then have access to the private members of that class. Accessible members of a class that is not first instantiated in many other languages are known as static members, and they are typically encapsulated into a class to organize the namespace of their functionality rather than to be used for unique instantiation.

To create a companion object, you first use the object keyword followed by the name of the class that the companion object is attached to. Then you define a scope or body of the companion object that contains members just like a class. Listing 8-9 provides an example of a companion object that is paired with our Weapon class.
exmaples.sc
case class Weapon(weaponType: String = Weapon.default_weapon, length: Int)
object Weapon {
    val unit_of_measurement = "inches"
    val default_weapon = "Musket"
    def useWeapon(weapon: Weapon){
        println(s"Using weapon ${weapon}")
    }
}
println(Weapon.unit_of_measurement)
println(Weapon.default_weapon)
Weapon.useWeapon(new Weapon("Big" + Weapon.default_weapon, 40))
Terminal Output
inches
Musket
Using weapon Weapon(BigMusket,40)
Listing 8-9

Demonstration of a companion object for using “static” members

In this example, notice that in order to print out the default weapon and the unit of measure, we did not need to first instantiate a Weapon object with the new keyword. We simply referenced the member directly from the definition of the companion object by calling Weapon.unit_of_measurement and Weapon.default_weapon. You might have also noticed that the static member default_weapon was used in the class’s primary constructor as well to provide a default value in case no argument is passed to the weaponType parameter. This eliminates the need to keep both references updated if you decided to change what the default weapon should be. However, in order to pass an actual Weapon data type to the useWeapon method, we did need to instantiate a new Weapon inline to pass as an argument. That newly instantiated weapon is now an instance of the Weapon case class and uses its conveniently built-in toString method when the useWeapon static method passes it to the println function.

Application

Now that you’ve learned how to create classes, lets modify our Nebula Operating System with some additional commands that can take advantage of this new functionality. Just like any basic operating system, our shell should have the ability to make basic text files. We can represent text files in our system as a case class that contains two members: a title and text body. Listing 8-10 provides an example of two new commands that we will need to add to our pattern matching scenarios in order to create new text files and to list out text files that have already been created. Those two commands will call two additional functions that are also provided in this listing along with the definition for the case class we will be using and a variable that will store a list of the saved files.
nebula.sc
...
case c if c.contains("make") => createTextFile(c)
case c if c.contains("show") => showTextFiles()
...
case class TextFile(title: String, text: String)
var files = new scala.collection.mutable.ListBuffer[TextFile]()
def createTextFile(userInput: String) {
    var tokens = userInput.split("/")
    try{
        files += new TextFile(tokens(1).trim, tokens(2).trim)
    }
    catch {
        case _: Throwable =>  println("An error occurred trying to create a text file.")
    }
}
def showTextFiles(){
    for(i <- Range(0,files.length)){
        println(files(i))
    }
}
Terminal Output
Welcome to the Nebula Operating System (NOS)! Version 1.0.4
NOS> make/Star/A star is born.
NOS> show
TextFile(Star,A star is born.)
NOS>
Listing 8-10

Additional commands added to the Nebula OS shell to create and list out basic text files

It is important to remember how pattern matching works when adding these commands into your pattern scenarios. Because your text files may contain + or - characters, you may want to ensure that these new commands are added before the add command and the subtraction command patterns so that those commands do not inadvertently try to call their subsequent functions instead of calling the new function to create a text file. You’ll notice that the command to create a new text file, make, requires you to separate the user input of the title and the text body by a forward slash. This is used as the delimiter for our tokens in this example so that spaces can be used in the text body. In a more complicated system, you would also want to allow the user to be able to use forward slash characters in their text body. However, for the sake of simplicity in this example, we can consider it a forbidden character as it will split the text body into separate index positions of the token list in the createTextFile function.

Note that the files variable is initialized as an empty list that will contain objects of our custom type TextFile. The createTextFile function simply takes the index position of the title of the file (the next value passed after the make keyword) and the text body (the value passed after the title of the file) and instantiates a new TextFile object that it then adds to the files list after trimming any unnecessary whitespace. If the function is unable to properly parse the command, it will handle the error and print a message to the screen just as it did when we created the addCommand function. Once the file has been added to the list of files, you can show all the available files created in our operating system using the show command, which simply loops through our list and prints out each object. Because the objects are instances of a case class, it can use the built-in toString method to print a readable object to the screen.

Exercise 8-3

See if you can extend the Nebula OS script to edit an existing text file. You may need to loop through all the objects in the files list to check for the title of the file in order to modify it. Consider the following:
  1. 1.

    What types of commands will you need to add to make this usable?

     
  2. 2.

    What kind of parsing tokenization will you use to allow for editing the file?

     
  3. 3.

    Will the user be able to preview what the existing text of the file is when editing?

     
  4. 4.

    What additional functions will need to be added for the commands to utilize?

     

Summary

In this chapter, you learned that a class can encapsulate functions and variables (known as methods and properties respectively) as members of the class. You learned how to define a new class and how to reference it with variables. Next, you were introduced to some of the default behavior of accessing the public and private members of a class and how to modify that behavior. After which you were shown the shorthand version of that behavior modification using case classes. From there, you were introduced to companion objects and their use for accessing members of a class without first creating an instance of a class. Finally, we applied the use of a case class to our example command-line operating system. This OS script is now starting to get quite large and will continue to grow larger as we add more commands, classes, and functions. In the next chapter, you will be introduced to a new method of breaking up your scripts into modules to help organize your code.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.14.246.148