Patterns for composition

Composition is one of the most challenging aspects of object-oriented design. What are the right boundaries between objects? What should be the responsibility of each object? How should larger objects be composed of smaller objects?

In this chapter, I’d like to present a number of time-tested patterns for breaking larger objects down into smaller objects. Learning about patterns can give you new ways of accomplishing the same task. Patterns aren’t meant to be dogmatically applied. Each pattern can be useful in certain circumstances, but can also be detrimental in others. Because of this I’ve tried to present each pattern with a list of pros and cons so you can make better decisions about when or when not to apply each pattern in your code. We’ll look at some examples, too. I want to give you practical insight into how to apply Object-Oriented CSS in your own work.

Parent-child pattern

We discussed the parent child pattern in chapter 2, but let’s review it here for the sake of completeness. The parent-child pattern is a simple way to provide a namespace for your objects and make the relationship between those objects clear. In object-oriented terms, when you create one object that contains another object this is known as composition.

In chapter 2, we gave an example of a dropdown button:

Created with the following object definitions:

.button { ... }
.button-caret { ... }

And HTML:

<button class="button">
  Dropdown
  <i class="button-caret"></i>
</button>

You’ll note that there are actually two objects here, the button and the caret. The caret appears inside of the button which makes it a child object. This is emphasized with the class name “button-caret”.

If you are more traditional in your approach to CSS you may be tempted to use a shorter name for this child object. Why use “button-caret” when “caret” will do? After all, you could define the child object with a nested selector like this instead:

.button .caret { ... }

The simple explanation is that it can lead to name clashes and unexpected results with other object definitions. Pretend for a moment that we also have an “collapsible-panel” object that has it’s own version of a caret:

.collapsible-panel .caret { ... }

In this example, if the collapsible panel caret had conflicting styles with the button caret this could cause unexpected results when a button object also appeared inside of a collapsible panel. In this case, which set of styles would win? Unless you’ve memorized the rules for CSS specificity you will probably find it hard to predict. Code that is hard to predict and reason about is bad code.

Pros:
  1. Avoids bugs that can surface because of name clashes and contextual CSS.
  2. Makes the relationship between parent and child objects extremely clear.
  3. Makes names consistent and safe.
Cons:
  1. Tightly couples the child object with the parent object. Consider the separate objects pattern instead.
  2. Longer class names. You can mitigate this in some circumstances with either the plural-parent pattern or separate objects pattern.

Grandchild pattern

What about when we have an object inside one object that is in turn inside of another? A grandchild object? In this case, the parent-child pattern can be extended to accommodate additional levels:

.parent { ... }
.parent-child { ... }
.parent-child-grandchild { ... }
.parent-child-greatgrandchild { ... }

An example of where this pattern can be useful is with a menubar. Here is an example with great-grandchild:

/* Menubar */
.menubar { ... }
.menubar-item { ... }
.menubar-item-menu { ... }
.menubar-item-menu-item { ... }
Pros:
  1. Makes the relationship between parent objects clear.
  2. Makes it easier to name grandchild objects.
Cons:
  1. Class names can get extremely long. Try the separate objects pattern instead.

Separate objects pattern

If the parent-child pattern or grandchild pattern is giving you grief you may consider if the objects you are working with would be better off as completely separate objects.

Our menubar example is a good one to highlight this pattern. Once again, consider the following object definitions:

/* Menubar */
.menubar { ... }
.menubar-item { ... }
.menubar-item-menu { ... }
.menubar-item-menu-item { ... }

In this case, we have a menu bar with a row of items, and each menubar item can have a menu which contains it’s own items. We might do better to break this up into two distinct compound objects:

/* Menubar */
.menubar { ... }
.menubar-item { ... }

/* Menu */
.menu { ... }
.menu-item { ... }

This could be especially useful if you use menus in other places (without a menubar object). For example, a menu attached to a dropdown button.

Pros:
  1. Easier to reason about in code because you are working smaller objects.
  2. Shorter class names.
Cons:
  1. The relationships between objects may be less clear.

Plural-parent pattern

One way to simplify parent-child relationships is to use pluralization in places where it makes sense. For example, you could create the following object definitions for a gallery of images:

/* Parent object */
.gallery { ... }

/* Child object */
.gallery-image { ... }

But you can shorten the class names a bit by making the parent class name the pluralized version of the child name:

/* Pluralized parent object */
.images { ... }

/* Child object */
.image { ... }

The plural parent pattern technically breaks a compound object into two distinct objects (see the separate objects pattern), but in practice the relationship between the two objects is preserved because of the tight relationship between the names.

Pros:
  1. Shares the same semantics as the parent-child pattern.
  2. Shorter class names.
Cons:
  1. Parent-child relationships may not be as clear.

Item pattern

Admittedly, the item pattern is just another variation on the parent-child pattern, but it’s worth mentioning here because it is so useful. Coming up with class names is hard and this is one pattern that will help you when working with lists or collections of objects. You’ve seen it in action already in the menubar example, but let’s dive into the pattern with a fuller explanation here.

Here’s our gallery example with the parent-child pattern again:

/* Parent object */
.gallery { ... }

/* Child object */
.gallery-image { ... }

If we use the item pattern instead, it looks like this:

/* Parent object */
.gallery { ... }

/* Child object */
.gallery-item { ... }

Using the item pattern here might be a much better choice if each child object contained more than just an image. You could then use the grandchild pattern to allow for styles for a caption and summary…

.gallery-item-image { ... }
.gallery-item-caption { ... }
.gallery-item-summary { ... }
Pros:
  1. Great for lists or collections of objects.
  2. Simple naming scheme can be helpful when you can’t think of a good name for child objects.
Cons:
  1. Longer class names. Consider the plural-parent pattern or the separate objects pattern instead.

Contextual pattern

In the Oxygen system contextual CSS is generally an antipattern (a pattern which should be avoided at all costs). It’s an antipattern in most cases because generally objects should appear and behave the same in any context. If you want different behavior you should use different objects!

However, there is one prominent use case for contextual CSS: theming. I recommend that you limit the use of contextual CSS to a small number of themes or theme types. For example, suppose that I wanted to modify a button so that when it appears on a dark panel it is drawn in white.

I could achieve this with contextual selectors like this:

.button { ... }
.red-panel .button { ... }
.green-panel .button { ... }
.blue-panel .button { .... }

That is, override the default CSS for a button in the context of the panel it is drawn on. But if all I needed was to make buttons white on a reversed background, I would be better off writing the code like this:

.button { ... }
.reversed .button { ... }

Then on dark panels I could also add a reversed class to make all buttons on the panel white.

It’s worth noting that this can get tricky if you end up with nested contexts. For instance, when you need to nest a light panel inside of a dark panel. In this case, you may find yourself writing something more like:

.button { ... }
.reversed .button { ... }
.reversed .not-reversed .button { ... }

Wow, that got out of hand quickly! If you find yourself in this situation you may just want to provide subclasses for objects to modify their appearance:

.button { ... }
.button.reversed-button { ... }

In this example, it’s more work to apply the “reversed-button” class in every context where you need it reversed, but it may be far easier than dealing with the complexity introduced by contextual CSS.

Theming isn’t the only valid use for contextual CSS. Two other strong use cases are (1) setting typographical styles (which we will discuss in a future chapter) and (2) walling off old, crufty CSS in a legacy section of your site (to do this, nest old CSS under a “legacy” class and add the class to a container object where you want the old classes to still be active).

Pros:
  1. Useful for theming, typography, or walling off legacy CSS.
  2. Fewer class names in the markup.
Cons:
  1. Can get out of hand, fast! Use subclasses or modifiers instead.

Prefix pattern

One pattern that is especially useful for CSS libraries is to prefix all CSS with a few letters.

For instance, if you created a UI library you might want to prefix the class names with “ui-” like this:

.ui-button { ... }
.ui-textbox { ... }
.ui-selectbox { ... }

Another use for this pattern is to explicitly mark certain classes as “deprecated” or “legacy”. This can be useful if you need to maintain a few classes during an interim period before they can be removed but encourage others not to use the class names in new work:

.deprecated-button { ... }
.deprecated-textbox { ... }
.deprecated-selectbox { ... }
Pros:
  1. Great to provide a namespace for library code.
  2. Useful to deprecate old code.
Cons:
  1. Longer class names.

In summary

Figuring out the right boundaries between objects can be difficult. But naming conventions and patterns go a long way toward helping us have a systematic way of approaching this.

Hopefully, this chapter has given you some additional ideas about how you can break up your code in a more modular way. Remember that each of the patterns introduced here can be useful – some more than others – but they also have reasons they may not be the best approach. So apply them thoughtfully.

Subscribe to the Oxygen newsletter to be notified as chapters are written!

Discussion