[policy, no patch]: Atomic behavior objects

Created on 28 March 2018, about 7 years ago
Updated 27 May 2025, 12 days ago

In #2727011: [policy, no patch] Private vs protected, and the role of inheritance β†’ , I talked a lot about how I think our classes and interfaces are too big.
(As long as this is the case, there is an argument preventing us from using "private" instead of "protected".)

I want to propose a vision and strategy to tackle this problem.

I think the key towards smaller classes and interfaces is "atomic behavior objects".
Instead of creating classes for "domain objects" (if I am using this term right) or "manager"-type broad-purpose services, our focus should be to identify standalone behaviors, and create interfaces and classes based on those.

(I don't say to remove all domain objects or services, just to change the focus)

Atomic behavior objects

How do such "behavior object" classes and interfaces look like?

  • The starting point is always a single method. Additional methods should be the exception.
    Additional methods should only be added, if they are relevant to every conceivable implementation of the main method.
  • The behavior doesn't have to be globally useful or relevant. E.g. its only application might be within a specific algorithm, so we would mark the interface as "@internal". But it has to be complete and self-contained.
  • Such classes typically don't have getters. All the data they contain should only be used for the implementation of the main behavior method.
  • They should be designed as immutable, so won't have setters. Perhaps some of them can have "withers" (methods that return a modified clone). But none of those would be part of the interface.
  • The constructor must always provide a fully valid and usable instance.
  • The constructor expects parameters in a format that is relevant to the main behavior. E.g. not a settings array, but specific values that will be saved in private properties, and mostly be used as-is to run the behavior. The constructor can do some basic sanity checks, but that's it.
  • Static factory methods ("named constructors") can be used to extract constructor arguments from configuration arrays, if necessary.
  • Inheritance should be avoided, or reduced to a minimum. Most classes can/should be either final or abstract.
  • Properties should only be accessible within the same file. Which means they should be private.
  • Base classes should be abstract, with private properties, and well-defined extension points.

Naming

One challenge is to give such classes and their interfaces good names.

In my own projects, I use a naming scheme like "$vendor\$package\$category\{$what}_{$how}" for classes (with underscore), and "$vendor\$package\$category\[$what}Interface" for interfaces (without underscore). This way, the class alias always gives sufficient information to understand its purpose, without looking at the class imports.

In our official coding standards we do not use underscores in class names, so it can either be "$namespace\[$what}{$how}" or "$namespace\{$how}{$what}". Putting the $what first makes multiple implementations align nicely in a directory.

The name of the interface (the $what part) should describe the behavior itself, not where or how or why it is meant to be used.

We could have one directory/namespace per behavior type (one interface plus one or more implementations), or we could have different behavior types live together in one namespace.. up for debate.

Callables/functional?

Someone could ask: Why not functional programming, using closures/callables instead of those one-method objects?
Some simple reasons:

  • PHP does not support signature type checks for callable parameters. The callable type hint allows any type of callable.
  • Closures cannot be cleanly organized in class files, addressed by global names, and reused. Functions (outside of classes, or static) don't have constructor parameters (the "use" part of closures).

"Plugins"?

Someone else might say: Plugins!
On the one hand, plugins (in D8) usually are designed to implement one specific behavior. Which is great!
Unfortunately, they typically do a number of things which is not relevant to this main behavior:

  • They can tell about their own plugin id.
  • They can tell about their own plugin definition.
  • Some of them manage their own configuration, and show a configuration form.

Value objects?

Value objects are another category of nicely-behaving objects, if implemented correctly.
I don't mind having those too. But I am not talking about them here.

Examples

We already have some "behavior objects" in core:

  • Drupal\Core\Controller\TitleResolverInterface
  • Drupal\Core\Render\Placeholder\PlaceholderStrategyInterface
    I find the name and the doc descriptions somewhat misleading. But it does qualify.
  • Drupal\Core\PathProcessor\OutboundPathProcessorInterface
    Again, confusing class name, and a doc description that just parrots the class name.
  • Drupal\aggregator\FeedInterface\ParserInterface
  • Drupal\Core\Extension\InfoParserInterface
    I like this one, because it is such a clearly defined purpose.
  • Drupal\Core\Asset\AssetDumperInterface

Then there is "renderkit", which is full of behavior objects.
https://cgit.drupalcode.org/renderkit/tree/src
Some of them might be overkill, but it illustrates the idea.

Some "behaviors" currently dwell in manager-type services.
E.g. ThemeManager and ModuleHandler each have an ::alter() method with the same signature, which could easily live in an AltererInterface.

ThemeManager::render() could live in a ThemeRendererInterface or ThemeHookRendererInterface (something like that).

Why does it matter?

Individual small classes and interfaces are easier to read, they fit more easily into one chunk of your brain.

They make it much easier to implement compositional patterns. E.g. a decorator for a one-method interface does not need pass-through implementations for all other methods.

Classes and interfaces with only one or few methods reduce or eliminate the need for inheritance. We can use "final" and "private" a lot more.

Short classes and methods reduce the number of code lines from which one local variable or private object member can be seen or manipulated.

Tiny interfaces with one or two methods allow for fine-grained dependency injection.

Tiny classes are easier to be made immutable.

Huge and complex classes, on the other hand, are harder to read and obfuscate bugs, bottlenecks and vulnerabilities.

Splitting things up into smaller classes and interfaces can have a cost, though:

  • More clutter in the filesystem.
  • Deeper stack traces.
  • Deeper object composition trees.
  • Some classes and interfaces which only have a local relevance, and no higher-level meaning.
  • More autoload calls, perhaps higher memory footprint for PHP opcache.
    On the other hand, having things nicely split up often allows for some interesting optimizations.
    This needs to be evaluated as we go.
🌱 Plan
Status

Postponed: needs info

Version

11.0 πŸ”₯

Component

other

Created by

πŸ‡©πŸ‡ͺGermany donquixote

Live updates comments and jobs are added and updated live.
Sign in to follow issues

Comments & Activities

Not all content is available!

It's likely this issue predates Contrib.social: some issue and comment data are missing.

  • πŸ‡³πŸ‡ΏNew Zealand quietone

    There has been no discussion here for 7 years. Of the 3 participants, 2 have pointed out cases where the proposal doesn't work for Drupal. Both of those have responses but there was no further participation from the objectors. Maybe this issue has served its purpose?

    Does anyone else support this idea? If there is support, add a comment. It would also help to update the issue summary using the standard issue template β†’ .

    I am setting the status to Postponed (maintainer needs more info). If we don't receive additional information to help with the issue, it may be closed after three months.

    Changing title per Special titles β†’ .

  • πŸ‡©πŸ‡ͺGermany donquixote

    I somehow doubt that this is going anywhere as a general policy.
    Lets see if there are any responses from others, otherwise ok to see it closed after 3 months.

  • πŸ‡·πŸ‡ΊRussia Chi

    The issue summary describes well know best practices in object design. Especially, Single-responsibility principle, Interface segregation principle and Composition over inheritance. There nothing new there.

    So the question is: Should we need a policy to enforce best practices? At first glance, this might seem counter-intuitive. After all, best practices are ideally adopted voluntarily, without requiring formal mandates.

    However, it’s worth noting that some people debate whether certain principles like "composition over inheritance" should even be classified as best practices. See 🌱 Use final to define classes that are NOT extension points Active .

Production build 0.71.5 2024