<<

OOP

By Adam Tuttle (Bio)
gist

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.

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 this 10 lines of code does. 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(), 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. 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.

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 some methods related to that data into an object. It often will include a distinction between public and private bits of data. For example, we may have a User object that contains the user's name, birthday, and salary. The object also has accessors and mutators. Accessors are methods that allow you to read or write a bit of data on the object -- public or private, it makes no difference; Mutators change the object and may or may not require any input. For example, a shopping cart object may have a mutator cart.calculateSalesTax('PA') that would calculate the sales tax for the items in the cart based on the customer's residence in Pennsylvania.

Using the objects created with Encapsulation, we can apply the rest of the techniques 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.

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 MXUnit. 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 getUser() method that returns an object of your User class. It also has a saveUser(user) 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 User 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 User object, and pass the User object to the saveUser(user) 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.

Going back to the shopping cart tax calculation example, the state whose tax is being calculated is a requirement for the calculation, and it is provided to the function during use. Let's extend this example and 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 ShoppingCart 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:

Session.shoppingCart = new model.ShoppingCart(
    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.

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 PhoneNumber bean, or the same collection of objects for addresses or email addresses. Instead, these things should all be handled within the Person service, 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 Person 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.