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.
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.
class
Contact
(
val
contact_id
:
Integer
,
val
firstName
:
String
,
val
lastName
:
String
,
val
:
String
,
val
enabled
:
Boolean
)
{
def
sendEmail
()
=
{
println
(
"To: "
+
+
" Subject: My Subject Body: 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 in Example 9-2 shows our new class.
case
class
(
val
address
:
String
,
val
subject
:
String
,
val
body
:
String
)
{
def
send
()
:
Boolean
=
.
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.
object
{
def
send
(
msg
:
)
:
Boolean
=
{
println
(
"To: "
+
msg
.
address
+
" Subject: "
+
msg
.
subject
+
" Body: "
+
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 in Example 9-4.
class
Contact
(
val
contact_id
:
Integer
,
val
firstName
:
String
,
val
lastName
:
String
,
val
:
String
,
val
enabled
:
Boolean
)
{
def
sendEmail
()
=
{
new
(
,
"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?
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.
case
class
(
val
address
:
String
,
val
subject
:
String
,
val
body
:
String
,
val
isDearReader
:
Boolean
,
val
name
:
String
)
{
def
send
()
:
Boolean
=
.
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.
object
{
def
send
(
msg
:
)
:
Boolean
=
{
if
(
msg
.
isDearReader
)
{
println
(
"To: "
+
msg
.
address
+
" Subject: "
+
msg
.
subject
+
" Body: Dear "
+
msg
.
name
+
", "
+
msg
.
body
)
}
else
{
println
(
"To: "
+
msg
.
address
+
" Subject: "
+
msg
.
subject
+
" Body: "
+
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.
object
{
def
send
(
msg
:
)
:
Boolean
=
{
msg
match
{
case
(
address
,
subject
,
body
,
true
,
name
)
=>
println
(
"To: "
+
address
+
" Subject: "
+
subject
+
" Body: Dear "
+
name
+
", "
+
body
)
case
(
address
,
subject
,
body
,
_
)
=>
println
(
"To: "
+
address
+
" Subject: "
+
subject
+
" Body: "
+
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.
object
{
def
send
(
to
:
String
,
subject
:
String
,
body
:
String
)
:
Boolean
=
{
println
(
"To: "
+
to
+
" Subject: "
+
subject
+
" Body: "
+
body
)
true
}
def
send
(
msg
:
)
:
Boolean
=
{
msg
match
{
case
(
address
,
subject
,
body
,
true
,
name
)
=>
send
(
address
,
subject
,
"Dear "
+
name
+
", "
+
body
)
case
(
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.
def
sendEmail
()
=
{
new
(
this
.
,
"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.
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:
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.
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.
case
class
CommandLineOption
(
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.
def
askForInput
(
question
:
String
)
:
String
=
{
(
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.
def
prompt
()
=
{
options
.
foreach
(
option
=>
println
(
option
.
_1
+
") "
+
option
.
_2
.
description
))
options
.
get
(
askForInput
(
"Option"
).
trim
.
toLowerCase
)
match
{
case
Some
(
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.
val
options
:
Map
[
String
,CommandLineOption
]
=
Map
(
"1"
->
new
CommandLineOption
(
"Add Customer"
,
Customer
.
createCustomer
),
"2"
->
new
CommandLineOption
(
"List Customers"
,
Customer
.
list
),
"q"
->
new
CommandLineOption
(
"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.
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.
()
=>
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.
val
options
:
Map
[
String
,CommandLineOption
]
=
Map
(
"1"
->
new
CommandLineOption
(
"Add Customer"
,
Customer
.
createCustomer
),
"2"
->
new
CommandLineOption
(
"List Customers"
,
Customer
.
list
),
"3"
->
new
CommandLineOption
(
"List Enabled Contacts for Enabled Customers"
,
()
=>
Customer
.
eachEnabledContact
(
contact
=>
println
(
contact
))
),
"q"
->
new
CommandLineOption
(
"Quit"
,
sys
.
exit
)
)
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.
class
Foo
{
val
value
=
"bar"
def
accept
(
func
:
Foo
=>
Unit
)
=
{
func
(
this
)
}
}
new
Foo
().
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.
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.