Functional OOP - Becoming Functional (2014)

Becoming Functional (2014)

Chapter 9. Functional OOP

Dealing with immutable variables brings up an interesting question as we dive into object-oriented programming (OOP): “Why would we have an object if we’re never going to change it?” This is where I’ve seen many people have an epiphany about functional programming. They understand the concept that an object is no longer something that “acts”; instead, it “contains” data.

As we go through this chapter, my hope is that you’ll also understand that objects are merely containers that encapsulate a set of data. We’ll answer the question “How does work get done?” by using static functions that will take our objects.

Back at XXY, your boss has asked you to extract the “send email” logic so that you can send emails for any type of report that might be requested in the future. He wants this to be done such that no other code that already calls sendEmail() has to be modified.

Static Encapsulation

Let’s begin by refactoring. Your boss wants you to extract the def sendEmail() function so that the functionality can be reused. Let’s first look at the Contact class and the corresponding def sendEmail() function that we will be migrating, as shown in Example 9-1.

Example 9-1. Send Email original

classContact(val contact_id :Integer,

val firstName :String,

val lastName :String,

val email :String,

val enabled :Boolean) {

def sendEmail() = {

println("To: " + email + "\nSubject: My Subject\nBody: My Body")

}

}

Let’s begin extracting this functionality by creating a function that will take an Email object. Let’s define our Email class, which will contain three members: address, subject, and body. It will also contain a send() method, which will call the Email.send method. The code inExample 9-2 shows our new class.

Example 9-2. Our new Email class

caseclassEmail(val address :String,

val subject :String,

val body :String) {

def send() :Boolean = Email.send(this)

}

Now, we can write our function itself. We will create the function send, which takes an Email object. For those not familiar with Scala, the code in Example 9-3 will seem odd with an object definition. An object is a singleton; it’s where we will normally keep our static methods.

The body of our function will actually be the body of the original sendEmail function from our Email class. We’ve extracted this send function into our Email singleton, as shown in Example 9-3.

Example 9-3. Our new Email object

objectEmail {

def send(msg :Email) :Boolean = {

println("To: " + msg.address + "\nSubject: " + msg.subject +

"\nBody: " + msg.body)

true

}

}

We’ve kept encapsulation by moving the send function into the Email singleton, allowing us to keep the email functionality within the Email object. We can now modify the sendEmail method in Contact to create a new Email object and then call its send() method, as shown inExample 9-4.

Example 9-4. Refactored Contact class

classContact(val contact_id :Integer,

val firstName :String,

val lastName :String,

val email :String,

val enabled :Boolean) {

def sendEmail() = {

newEmail(email, "My Subject", "My Body").send()

}

}

Now you can see that our Email class has become nothing more than a container of the data itself; it has a minimal amount of code inside the class. We’re calling into the Email singleton to perform the actual email functionality. How do objects as containers actually change how we see functions and data?

Objects As Containers

Your boss has requested that certain emails contain the name of a Contact in the format “Dear <name>”. We’ll add two parameters to our Email object, isDearReader and name. isDearReader indicates whether we should use the format, and name is the name we will use when sending the email. In Example 9-5, you can see our new Email class with the added fields.

Example 9-5. The Email class with isDearReader and name fields

caseclassEmail(val address :String,

val subject :String,

val body :String,

val isDearReader :Boolean,

val name :String) {

def send() :Boolean = Email.send(this)

}

Next we’ll update the Email object to use these new parameters. In Example 9-6, we’ll update the send method. We’ll do this with an if statement to test if the isDearReader field is true. If it is, we’ll append the name field to our output.

Example 9-6. The Email object using isDearReader and name fields

objectEmail {

def send(msg :Email) :Boolean = {

if(msg.isDearReader) {

println("To: " + msg.address + "\nSubject: " + msg.subject +

"\nBody: Dear " + msg.name + ",\n" + msg.body)

} else {

println("To: " + msg.address + "\nSubject: " + msg.subject +

"\nBody: " + msg.body)

}

true

}

}

We can refactor this even further by using pattern matching. By using a pattern match on the msg variable, we will have two case statements: one for when isDearReader is true, and the other for when isDearReader is any other value. This refactor is shown in Example 9-7.

Example 9-7. The Email object with isDearReader using a pattern match

objectEmail {

def send(msg :Email) :Boolean = {

msg match {

caseEmail(address, subject, body, true, name) =>

println("To: " + address + "\nSubject: " + subject +

"\nBody: Dear " + name + ",\n" + body)

caseEmail(address, subject, body, _) =>

println("To: " + address + "\nSubject: " + subject +

"\nBody: " + body)

}

true

}

}

We can refactor this further still by creating a send method that takes the to, subject, and body fields and performs the send. We have refactored this based on what we believe constitutes the most basic components of sending an email. Example 9-8 shows this refactoring.

Example 9-8. The Email object extracting the send function with common functionality

objectEmail {

def send(to :String, subject :String, body :String) :Boolean = {

println("To: " + to + "\nSubject: " + subject + "\nBody: " + body)

true

}

def send(msg :Email) :Boolean = {

msg match {

caseEmail(address, subject, body, true, name) =>

send(address, subject, "Dear " + name + ",\n" + body)

caseEmail(address, subject, body, _, _) =>

send(address, subject, body)

}

true

}

}

Now that we’ve updated the Email functionality, we need to update our Contact.sendEmail() method so that we can take advantage of this new feature. Your boss has asked that any time you call sendEmail() on the contact, you should use the isDearReader functionality. We can now update our code as shown in Example 9-9.

Example 9-9. The Contact classes’ sendEmail() method handling isDearReader

def sendEmail() = {

newEmail(this.email, "My Subject", "My Body", true, this.firstName).send()

}

Our Email class is now more of a container; its primary job is to contain all of the fields that are necessary for creating the email, not necessarily sending it. This illustrates the harmony that we really want between functional programming and OOP.

Code as Data

Back at XXY, your boss has asked that you allow for a way to create a customer from the command line. Thus we’re going to create a new CommandLine object, which will actually have a few different functions:

§ Display a question and get input from a user.

§ Display all possible options to a user.

§ Interpret a user’s input.

Let’s begin by creating a really simple class representing our command-line options. We’ll call it CommandLineOption, and it will be a case class, as shown in Example 9-10. Our class will have a description and a function func to be executed when it is selected.

EXTENSION OF THE STRATEGY DESIGN PATTERN

This method is fairly similar to the Strategy Java design pattern, except that we can directly pass a function rather than an implementing class of an interface.

Example 9-10. The CommandLineOption case class

caseclassCommandLineOption(description :String, func : () => Unit)

Next, let’s create the CommandLine object, which will have two primary methods. The first will askForInput given some prompt, as shown in Example 9-11.

Example 9-11. The CommandLine.askForInput method

def askForInput(question :String) :String = {

print(question + ": ")

readLine()

}

Next, we will create a method that gives the user a prompt of options and asks her for input. The method will draw from the options variable, which will be of type Map[String, CommandLineOption] and will allow us to search the Map for the option that the user selects. Check out the prompt function in Example 9-12.

Example 9-12. The CommandLine.prompt method

def prompt() = {

options.foreach(option => println(option._1 + ") " + option._2.description))

options.get(askForInput("Option").trim.toLowerCase) match {

caseSome(CommandLineOption(_, exec)) => exec()

case_=> println("Invalid input")

}

}

Notice how we iterate over each option, printing out ._1 and accessing ._2.description. The _1 refers to the first option of the Map (the String), whereas the _2 refers to the second option (the CommandLineOption).

Next, we askForInput and then search the options variable for the option. We will have either Some, in which case we extract the func from our CommandLineOption class, or we will have None, for which we assume the user gave us bad input.

So, what does this options variable look like? It’s actually really simple: we build a Map (indicated by a <key> -> <value> syntax) containing the option that the user will input (as the key), and the CommandLineOption object (as the value). The definition of all of our existing options is shown in Example 9-13.

Example 9-13. The CommandLine.options variable

val options :Map[String, CommandLineOption] =Map(

"1" -> newCommandLineOption("Add Customer", Customer.createCustomer),

"2" -> newCommandLineOption("List Customers", Customer.list),

"q" -> newCommandLineOption("Quit", sys.exit)

)

The beauty of being able to reference functions is that we can actually set a function from another Object as part of another function. Notice how we have two options, Add Customer and List Customers, that reference previously existing functions? This allows us to use a pre-existing function without breaking the encapsulation.

Your boss has come back to ask you to create an input option that allows users to view all enabled contacts for all enabled customers. This seems really straightforward. We already have a function, eachEnabledContact, that we can pass a function to and print out each contact!

Let’s see what our function would look like to print out each enabled contact in Example 9-14. Here we will use our eachEnabledContact method and pass a function that takes a single argument and prints the variable.

Example 9-14. How Customer.eachEnabledContact would be used to print out the contacts

Customer.eachEnabledContact(contact => println(contact))

And if we needed that to be its own function, we would just use Scala’s empty parentheses syntax, as in Example 9-15. This example defines a function that takes no arguments but executes the code in Example 9-14.

Example 9-15. Encapsulating the printing of each enabled contact as its own function

() =>Customer.eachEnabledContact(contact => println(contact))

So, let’s look at our new options variable in Example 9-16 and see how it works with our new option. We’ll just continue down the line and add it as option 3.

Example 9-16. The CommandLine.options variable with the new “print each enabled contact” option added

val options :Map[String, CommandLineOption] =Map(

"1" -> newCommandLineOption("Add Customer", Customer.createCustomer),

"2" -> newCommandLineOption("List Customers", Customer.list),

"3" -> newCommandLineOption("List Enabled Contacts for Enabled Customers",

() =>Customer.eachEnabledContact(contact => println(contact))

),

"q" -> newCommandLineOption("Quit", sys.exit)

)

Conclusion

It’s important to understand that functional programming itself is not a replacement for OOP; in fact, we can still use many OOP concepts. Objects are no longer used to encapsulate a large group of statements in an imperative manner, but instead are designed to encapsulate a set of variables into a common grouping.

We are able to expand concepts such as the Command pattern or the State pattern by just creating a class that contains a method to be defined later. This style of definition allows us to change the method at runtime without breaking encapsulation or having lots of erroneous classes living everywhere.

Think back to our CommandLineOption example: we created quite a few options by just passing functions to a new CommandLineOption. This allows us to create tons of objects extending from an abstract object without actually defining every type. We can also more easily implement patterns, such as the Visitor pattern, where Object A accepts Object B, and Object B does some operation on Object A.

Let’s assume that we have a class Foo that has an accept method. But we’re not going to accept another class; instead, we just accept the function that performs the visitor work we want to do. The visitor just becomes a simple function that we’re passing to Foo. See Example 9-17.

Example 9-17. Visitor pattern using functions

classFoo {

val value = "bar"

def accept(func :Foo => Unit) = {

func(this)

}

}

newFoo().visit(f => println(f.value))

Now you can see that functional programming allows us to continue using many OOP concepts and ideas while reducing the number of classes we write. Where we would write classes to encapsulate a single function, we can now just send a function rather than an implementing class.

How about an example in which we implement a command pattern where we have a string transformer (take a string, transform it, return a string)? Think about how you would implement it and then check out Example 9-18.

Example 9-18. Command pattern using functions

def toUpperCase(str :String) : () => String= { () => str.toUpperCase }

def transform : () => String= toUpperCase("foo")

println(transform())

Notice that we no longer need to create separate objects, but instead we can just return the command as a function to execute. This decreases the number of classes we’re creating and keeping track of, thus increasing the readability of our code.