One could write a book specifically about Object Oriented Programming (OOP). This chapter is an OOP primer to get you started, but for a more in-depth explanation, check out Matt Gifford's Object Oriented Programming in ColdFusion.
Object Oriented Programming is a set of concepts and techniques that make use of the "object" language construct, to write more reusable, maintainable, and organized code. Objects are implemented differently in every language; in ColdFusion, we have ColdFusion Components (CFCs). Using objects doesn't require OOP, and not every use of objects is OOP. They are simply the building blocks for writing OOP code.
When you write a lot of OOP code, you'll quickly find yourself writing repetitive code to wire together everything necessary to respond to a given request; if you take some time to write a single path through the code that analyzes the request and automatically wires together the things necessary to respond, then you've essentially written your own front-controller framework.
Frameworks are not an essential part of OOP, but they do solve a common set of problems, and it's best not to write code without one. Some people choose to write their own frameworks, but if you're just getting started, make use of one of the many that are already available, as they have been established and have already had time to work out the kinks and bugs.
Two popular and still maintained frameworks include framework-one (fw/1) which is a MVC (Model View Controller) framework and ColdBox which is a HMVC (Hierarchical Model View Controller) framework.
So what exactly does OOP do to make your code more organized and maintainable? You decouple unrelated sections of code, you encapsulate related functionality into the same object, and different but related object types can inherit functionality from one another or from a base object. This is all possible through the understanding and use of classes, instances, methods, and abstraction.
By combining some or all of the concepts below, you'll find that your code is very DRY (Don't Repeat Yourself) and well organized, enabling you to have a good separation of logic from presentation.
In ColdFusion, a Class is simply a CFC. The component defines a set of public methods (functions) and can have both public and private data. It may also have private methods that are used by other methods in the class to improve code reuse and abstract complex jobs into small maintainable chunks.
Anything that's both complex and discrete is a good candidate for abstraction. For example, say you have an array of structures, each with a key named "foo", and you want to get an array of the values of "foo" from each struct. Doing so probably only takes about 10 lines of code, but it's a fairly complex chunk of code; anyone reading your code later will get distracted from reading the entire method to figure out what those 10 lines of code do.
Instead, you could take that same 10 lines
of code and wrap it in its own method -- let's call it
reduceArrayToFoos()
-- and then call it instead of writing that code
inline. Then when your coworkers, or even yourself, are reading your
code in 6 months, you'll be able to scan past that line because it's
obvious what it does. The same concept can be applied at a higher level
to abstract related functionalities for the same data or job into a
utility or model class.
Inside a CFC, the this scope is where you can place public variable, and the variables scope is where you can place private variables that are globally available to the entire class. Methods can also have private variables that are not shared to other methods, which you put in the local scope (or use the var keyword, which does the same thing).
Instances tend to be the concept of OOP that people have the most
trouble with. Your CFC is the class; it's the blueprint for an object,
but you can create as many Instances of that class as you like. When you
call CreateObject()
, or use the new
keyword (e.g. new models.bean.myBean()
),
you're creating an instance of the class you specify.
You can use instances in two different ways: The first are Transient instances. A transient instance is one where you create, use, and throw away when you're done. If you don't specify that it should be saved, ColdFusion will throw it away for you at the end of the request. Beans are an example of a Transient instance. You load the bean with the data you need for that request, process data into or out of the bean, and then the bean is discarded at the end of the request.
The second instance are Singleton instances. This is an instance that lives beyond the request that created it, and future requests can use it. Typically with a singleton, you create only one single instance of it and everything that uses it uses that same instance, hence the name singleton. You tell ColdFusion to persist it by storing it in one of the persistent scopes: Server, Application, and Session would be the most common. Services are an example of a Singleton instance. The service typically operates on the bean data - either loading the data into the bean from the database, or persisting data in the bean to the database, for example. The service only needs to be loaded once since it handles transient data (beans).
Now that you understand these basic concepts, let's dive into the details of what makes OOP tick.
Encapsulation is the concept of bundling bits of data and methods
related to that data into an object, oftn referred to as a bean.
It often will include a distinction
between public and private bits of data. For example, we may have a UserBean
object that contains the user's name, birthday, and salary.
The bean object will often also have accessors and mutators.
Accessors are methods that allow you to read data stored in an object.
These are often referred to as getters, as they
allow you to get (access) data from an object. In our UserBean
object this
could be getName()
, getBirthday()
and getSalary()
.
Mutators are methods that allow you to change the data stored in an object.
These are often referred to as setters, as they allow you to set (mutate)
the data stored in an object. In our UserBean
object this could be
setName( 'Bob' )
, setBirthday( 'July 4th' )
and setSalary( '100000' )
.
In ColdFusion CFCs you can specify accessors="true"
in the component
definition and accessors and mutators will be implicitly created for you.
It is recommended that if you want to have getters and setters to specify
this in the component definition instead of creating your own user defined
functions as the creation and execution of implicit accessors and mutators
is quite a bit faster than using UDFs.
To make use of implicit accessors and mutators you simply define the properties of
the object and ensure accessors="true"
is in your component definition.
One final method that is common in Bean objects is a getMemento()
UDF. This method
takes the data stored in the object and puts it into a struct. This allows you
to quickly retrieve all of the data stored in the object and is quite handy for debugging.
Putting it all together we would end up with the following code:
component displayname="UserBean" accessors="true" {
property name="name" type="string" default="";
property name="birthday" type="string" default="";
property name="salary" type="numeric" default="0";
public struct function getMemento() {
local._user = {
name = getName(),
birthday = getBirthday(),
salary = getSalary()
}
return local._user;
}
}
Using the objects created with the principle of Encapsulation, we can apply the rest of the techniques below to create more maintainable and testable code.
Decoupling is the separation of chunks of code that shouldn't need to know about the details of each other. The obvious case of this is separating the presentation of data from the logic that retrieves it from the database and manipulates it.
However, decoupling also applies
to unrelated groups of related code. For example, all of the code for
widget management should be together, but it should not be mixed with
the code for user management; and both groups should have their own
class: WidgetService
and UserService
. By decoupling your code, you can
change the logic of the UserService
with confidence that you're not
messing with anything widget-related.
Inheritance is a way to reuse existing code, or a way to write code in one location that many objects can make use of. When an object of class B inherits from class A, object B contains all of the code -- that is, methods and data -- from class A, plus all of the code from object B. Importantly, for any methods and data that exist in both classes A and B, the value or implementation from object B takes precedence; allowing you to override the behavior or value of an object when you extend (that is, inherit from) it. Lastly, your implementation of methods in object B can also call the implementation from object A, more or less as a "wrapper" for the implementation in class A.
For example, we may have a BaseBean
object that has a
getJSON()
method in it that simply has return serializeJSON( getMemento() );
.
Our UserBean
object could inherit the BaseBean
object and we could quickly
return data stored in the UserBean
object as JSON by calling the getJSON()
method.
Polymorphism is a concept that is more evident in strictly typed
languages, where it indicates that one type is somehow derived from
another or implements a specific interface. In ColdFusion, as a
dynamically typed language, data is implicitly polymorphic. Polymorphism
allows objects of different types to have the same, or similar, APIs.
For example, a user object and an administrator object are very similar.
An administrator might inherit from the user class, then add its own
additional properties and methods for special abilities it has
permission to use. However, for everything that both Users and
Administrators interact with, the code can assume the object implements
everything in the User class. Consider blog comments and the blog author
is its administrator. When an administrator leaves a comment, the
administrator object will have the same getUsername()
and
getEmailAddress()
methods that a user object would. You can think of
polymorphism as a strict use of inheritance to make related but slightly
different classes.
OOP has survived the test of time by proving that it solves a certain set of problems well. When well written, it is DRY, Maintainable, and Testable.
DRY is an acronym for Don't Repeat Yourself, and means that you should
write your code in a way that promotes reusing existing implementations.
Writing a utility class with commonly used user defined functions allows
you to address a bug found in the functionality of one of those
functions in one place. If the same code was not a UDF and instead had
been copied and pasted all over your codebase, you would have to do a
lot of searching and replacing. Not only would that be tedious, but you
would have more opportunities to make a mistake while applying the fix.
Similarly, the DRY approach would say that there should only be one
class, your UserService
, capable of reading and writing User data from
and to the database. With this approach, if you notice a bug when data
is stored to the database, you know exactly where to find it, because
there's only one place that writes that data to the database.
Maintainability is a sort of nebulous, almost subjective topic, but a majority of veteran developers agree that writing OOP code that is DRY makes it vastly more maintainable than the typical copy & paste "spaghetti code" approach.
OOP also makes your code much more unit-testable. OOP is not a requirement for integration tests, as with using a tool like Selenium, because it doesn't look at the code. Instead, it looks at "screens" or "views" and interacts with them, allowing you to assert that certain results and behaviors occur. For example, while integration testing is making sure that the beach looks the way you expect, unit testing is making sure that each grain of sand on the beach looks and behaves as it should. Unit tests know the names of the methods in each class and verify that each does what it should. If you're interested in Unit testing for ColdFusion, the most popular tool for the job is TestBox. Both have their place, but if you want to do unit testing, you're better off with an OOP approach.
There are very few strict requirements for writing OOP code in ColdFusion. You'll be using Objects (CFCs), but what you name them, what folders you put them in, if any, and so on, are all entirely up to you. What it boils down to is that you use Objects (Classes, Instances of those classes, and Abstraction) along with the concepts discussed above: Encapsulation, Inheritance, Polymorphism, and Decoupling.
Let's say your application has users. You need a UserService
, so you
create an object named UserService
. It has a getUserByUid()
method that
returns an instance of your UserBean
object. It also has a saveUser( userObj )
method to save any changes you might make to that user object while the
application is running, such as if the user updates their password. You
only need one UserService
, so you make it a Singleton, but every user
gets its own UserBean
object, so that should be a transient object. We've
described two types of objects here: data objects, sometimes referred to
as Beans, and Services. Collectively, these should be considered
your application's Model. (If you choose to use ORM, the ORM objects
would also be part of your Model.)
Your code should take the new password from the user, put it into the
UserBean
object, and pass the UserBean
object to the saveUser( userObj )
method of
the UserService
. Then, barring any errors, it should report to the user
that their password has been updated. This type of process is a
Controller.
The last important piece are the templates that display data to the user, collect data from them, and allow them to navigate around the data; these are known as Views. But where do views get the data they display, and what do they do with it after they collect it? They get it from, and give it to, the controller.
In a nutshell, that is Object Oriented Programming. If the way you write your code satisfies these requirements, then your code is Object Oriented. Obviously, there are still a lot of details whose implementation is up to you; and that is why there are frameworks -- and many of them.
People have differing preferences for how they want their Models, Views, and Controllers organized, what behaviors the framework should add or hide, and how the framework provides "extension" points where you can modify the way the framework works or affect your code at a future point; thus we have different frameworks to choose from.
In fact, if you choose not to use an existing framework, instead choosing to write your own OOP code, you will do one of two things: you'll find yourself rewriting similar code over and over to wire together the request and response for each type of request, or you'll try to write something that automates that for all requests based on part of the request. If you choose the latter, you've just written your own MVC framework. Unless you've experienced at least a few of the existing frameworks, know what they have to offer, and know that you can do better, it is recommended that you use what currently exists.
There's always room for improvement, but existing frameworks have the advantage that they've been available for a while and have large existing userbases that have reported and helped work out most, if not all, of the bugs. When you start from scratch, you will have to go through all of that as well. A hybrid approach would be to fork, or contribute, to an existing framework that provides most of what you want or does it mostly the way you want, but to add to it or update it as you see fit. If the wider community appreciates your changes, there's a good chance they'll be accepted into the framework and distributed to the rest of the community.
This list is not exhaustive, but hopefully will help you avoid some of the more common problems today's developers cause for themselves.
Red flags go up during a code review when there is code that is accessing global scopes (Server, Application, Session, Request, Client, Cookie) in model objects. One of the core tenets is obviously decoupling, and in this case it requires that everything that the object needs should be passed in either when the object is instantiated or when it is used. It should not reference anything outside itself.
Let's say that we want to track statistics about our customers; specifically, we
want to see a log of every time they update their cart: additions,
removals, and updates. To do so, we're going to use the application's
existing logging service. We happen to know that the logging service is
persisted as application.LogService
, so we could just reference it from
the ShoppingCartService
object and add our logging:
application.LogService.log('user 1234 added item f3489j to their cart');
However, this breaks encapsulation and is considered bad practice. Instead, it should be set into the ShoppingCart object during instantiation:
shoppingCartService = new models.services.ShoppingCartService(
logService = application.logService
);
Did you notice that the above code sample still references a shared scope (Session)? That's because this is not a sample from inside the ShoppingCart class, this is inside a controller.
Modern (H)MVC frameworks can inject objects into one another. This is
another OOP technique known as object Composition. Object composition is
used to represent "has-a" relationships: every employee has an address,
so every Employee object has access to a place to store an Address object.
This allows you to inject the LogService
directly into your ShoppingCartService
and have it available in the variables scope.
Another common mistake people make is to create 5 different objects for every table in their database: Gateway, DAO (Data Access Object), Bean, Service, and Controller. While it is conceivable that some or all of these classes would be necessary for any given table in your application, it's not necessary to split them up. Generally speaking, a Gateway and a DAO do the same things, except a DAO is meant to read or write one record while a Gateway is meant to read or write many records.
Right away, it seems obvious to combine them. Then again, with the Gateway and DAO outside of the service, there's not much left in the service. The actual preference is to combine all three. If you're using ORM you'll still have that external to your service, but aside from that, there is no need to split data access out from the service at all.
In looking at Beans and Controllers, not every table in your database
needs its own beans and controllers -- or services for that matter.
Consider a person that has multiple addresses, email addresses, and
phone numbers. In the 3rd Normal
Form of database
serialization, you'll have separate tables for people, addresses,
emails, and phone numbers. But this does not necessitate a
PhoneNumberService
, PhoneNumberController
, and PhoneNumberBean
, or the
same collection of objects for addresses or email addresses. Instead,
these things should all be handled within the PersonService
, and you
can skip the controllers. Whether or not you create beans for them would
depend on how you use your beans, but they are not strictly necessary --
they could easily be a property in the PersonService
object.
If the 5-for-1 problem arises from a "top down" approach where the database is at the top and dictates the object model, then the majority of veterans these days would advocate for thinking with your user interface as the top, and dictate the object model from that, keeping it as simple as possible, but as complex as necessary.