Coding Style – When to Use Classes vs. POD (PDS) and Functions

clean codecoding-stylemaintainability

Recently, I've read a blog post, that I can't find back, about how we should "free the data". The main point of the post was that we use classes and encapsulation too much since a lot of problems can be solved with less overhead by using plain old (passive) data structure combined with function overload. The post raised my awareness about the cost of creating (and maintaining) classes and classes hierarchies. In order to share this newly earned awareness with my colleagues, I tried to pinpoint conditions that justify the creation of classes. So far, I've found

  • Presence of an invariant. For instance, a map should always contains the same number of keys and elements. You do not want the user to add a key and forget to add the corresponding element.
  • Implementation hiding to have the freedom to change it easily. For instance, a Point can be encoded with Cartesian coordinates (x,y) or with a radius and an angle.
  • Homogeneous manipulation. For instance if you want Dog and Cat to be manipulated the same way because they are both specialization of the more general concept of Animal.

What are the other reasons to create classes or classes hierarchies?

Edit: By cost, I refer to the time, money and Technical Debt required to create and maintain classes and classes hierarchies. This cost should be compare to the cost of other solutions.

Edit 2: I realized that trying to make this question general was a mistake. I definitely have c++ in mind.

Best Answer

This is a very broad question, as it depends on the language used, and the features that language offers:

  • Can "plain old data structures" be made immutable?
  • Does the language enforce encapsulation of private functions and data, or is it by convention?
  • Is the language statically or dynamically typed?
  • Does it allow functions outside of static classes?
  • Does it treat functions as first class values?
  • Does it support interfaces?
  • Does it support records, structs etc?
  • Does it even support classes?

Depending on the answers to the above questions, the strategy used will vary. If, for example, the language doesn't support classes, you won't be using them...

Having said all that, there are some general rules that can be followed across languages:

  1. Avoid global state. If you have data, that's globally accessible and is mutable, you're on the path to debugging hell. Just don't do it.
  2. Avoid coupling. Whether it's through having objects spin up instances of other classes, or functions hard-coded to call other public functions, you're making the code harder to test and maintain. Use injection techniques and keep coupling as loose as possible.
  3. Avoid inheritance. Inheritance causes coupling problems, including the Fragile Base Class Problem, weakens encapsulation and causes testing problems. Unless you are using a language that can only achieve polymorphism via inheritance (ie doesn't support truly abstract classes or interfaces), then don't use inheritance.

As a rule of thumb, for a typical modern language that supports static functions and classes:

  1. Keep data as immutable as possible,
  2. Keep data and functionality as separate as possible,
  3. But use objects to encapsulate state and provide methods to handle that state,
  4. Only use functions (static methods) when they can be made pure, ie they produce a result from the parameters in a deterministic fashion without side effects.
  5. Design to interfaces (or the equivalent) and use injection as much as possible.
Related Topic