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.
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.
Attempting to access a property of the Weapon class
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
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.
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.
Printing an instantiated class object
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
- 1.
How will you represent months? Will they be numbers or strings?
- 2.
Will your properties be public or private?
- 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.
How will the subtract day method behave if the date represents the first day of a month or year?
- 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.
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.
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.
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.
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.
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.
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
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
- 1.
What types of commands will you need to add to make this usable?
- 2.
What kind of parsing tokenization will you use to allow for editing the file?
- 3.
Will the user be able to preview what the existing text of the file is when editing?
- 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.