Effective Object Design in Four Ways
Every developer has their own preferences, but I would offer up the following criteria to consider when deciding on an appropriate object design pattern for your code.
- Readability: Like all good code, object-oriented code should be readable not only to you, but also to other developers. Some design patterns are easier to interpret than others and you should always keep readability in mind. If you are having a hard time understanding what your code is doing, then other developers will almost certainly have no clue.
- Repetition: One of the major benefits of object-oriented code is that it reduces redundancy. If your code is likely to have many objects of the same type, then an object-oriented design is almost certainly appropriate. However, some patterns reduce redundancy more than others. Keep this in mind, while simultaneously considering that greater redundancy reduction may result in loss (or at least more difficult implementation) of certain customization options.
Chickenobject delegating a
layEggbehavior to a higher-up prototype
Birdobject.) Prior to selecting a design pattern, take a moment to consider whether you expect a hierarchical structure to be necessary, and if so, which behaviors should be placed on which object types.
And with those few short recommendations complete, let’s get to our review of the most common design patterns you are likely to encounter.
Factory Object Creation Pattern
The Factory Object Creation Pattern, or simply the Factory Pattern, uses so called “factory functions” to create objects of a similar type. Each object created by such a function has the same properties, to include both state and behavior. Take for example the following:
Here, we have a function,
makeRobot(), which takes two parameters (
job) and uses them to assign state to an object literal inside the function, which it then returns. In addition, the function defines a method,
introduce(), on the same object. In this example we instantiate two robot objects, both of which have the same properties (albeit with different values.) If we wanted to, we could create thousands more robots in exactly the same way and reliably predict what their properties would be each time.
Although the factory pattern is useful for creating like objects, it has two major drawbacks. First, there is no way to check whether a given object was created by a certain factory. We cannot, for example, say something like
bender instanceof makeRobot to find out how
bender was created. Second, the factory pattern does not share behaviors, rather, it simply creates new versions of a behavior every time it is called and adds them to the object being created. As a result, methods are repeated anew on every object created by the factory function, taking up valuable space. In a large program, this could prove extremely slow and wasteful.
One way to address some of the weaknesses of the factory pattern is to use the so-called Constructor Pattern. In this pattern, we use a “constructor function,” which is really just a regular function that is called using the
new keyword. By using the
- The function will immediately create a new object.
- The function execution context (
this) will be set as the new object.
- The function code will execute within the new object’s execution context.
- The function will implicitly return the new object, absent some other explicit return.
Let’s alter our previous example and try making some robots using the constructor pattern.
This snippet looks a lot like the previous one, except this time we use the
this keyword inside the function to reference a new object, set some state and properties on it, and then implicitly return when the function finishes executing. For the sake of convention (not any actual syntactical reason), we have called our function simply
Robot with a capital “R”. And, unlike with the factory pattern, we can even check to see whether a given object was constructed by the
Robot function with
You might be tempted to think of this as though we had created a
Robot “class”, but it is important to remember that we are not creating copies of
Robot as we might be in a true class language. Rather, we are exploiting a link that is created between the newly instantiated object’s prototype and its corresponding constructor function’s prototype, which facilitates prototypal delegation. We haven’t really taken advantage of that functionality in the above snippet though as we are still creating a new
introduce() method on every single new robot. Let’s see if we can fix that.
Thus far we haven’t really explored prototypal delegation other than to mention briefly that it exists. Now it’s time to see it in action and eliminate some code redundancy at the same time. Object prototypes and their delegation behavior are worthy of an entire blog post, but we can get at least a basic picture here. In essence, when a certain property is called on a certain object, for example
someRobot.introduce(), it goes to look for that property first on itself. If no such property exists, it then looks at the properties available to its prototype object, which in turn looks at its prototype object if necessary, and so on all the way up to the top-level
Object.prototype. The prototype chain allows delegation of behavior, wherein we don’t have to define some shared method on lower-level objects of the same type. Instead, we can define the behavior on whichever prototype they all share and thus eliminate redundancy by only defining the code once. Here it is in action with our robots.
As in the constructor pattern we are using the
new keyword to create a new object, assign some state, and then implicitly return that object. However, in this case we do not define the
introduce() method on each of our robots. Rather, we define it on the
Robot.prototype object, which as we have seen, acts as the prototype of each new object created by the
Robot constructor function. When we attempt to call, for example,
wallE object sees that it has no such method and goes searching up its prototype chain, quickly finding a method by that name on
Robot.prototype. Indeed, if we check
wallE’s prototype by using
Object.getPrototypeOf(), we can see that it is indeed
Object Linked to Other Object Pattern
In this snippet, we first define a
Robot object, which will serve as the prototype for all future robots. The
Robot object contains all the behaviors we expect of our robots; however, it does not set any state. Rather, we define an
init() method on
Robot, which we will use to set state on any future robots. Speaking of future robots, instead of creating them with a function, we do so by using the
Object.create() method, which accepts a prototype as an argument. By passing
Robot to the
Object.create() method, we ensure that the resulting object has
Robot for its prototype. We then call the
init() method on our individual robots to set the necessary state. We can even check to see whether a given object is of a certain type by using the handy
Object.getPrototypeOf() method, as we did in previous snippets.
OLOO allows us to share like behaviors and check the type of individual objects, all while sidestepping the class illusions inherent in the constructor and pseudo-classical patterns. For many developers, this method is preferred because it provides for easy-to-understand code that is also efficient and clean.