OOP


Content Author

Adam Tuttle

AdamTuttle

Reviewed/Revised By

Denard Springle

ddspringle

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.

What is OOP?

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.

Classes, Instances, Methods, and Abstraction

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, Decoupling, Inheritance, and Polymorphism

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.

Why Should I use OOP?

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.

How to Write Object Oriented ColdFusion

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.

Common OOP Pitfalls

This list is not exhaustive, but hopefully will help you avoid some of the more common problems today's developers cause for themselves.

Accessing Global Scopes Directly

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.

The 5-for-1 Problem

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.