(I don't know if this should go into core issue queue, or into coding standards. Please move if applicable.)

It seems that in Drupal core development, there is an unwritten consensus that all non-public class members (methods, properties) should be protected, not private.
in 8.2.x core/lib/ (with some local diff, but more or less these numbers):
- 1106x "protected function".
- 25x "private function".
- 1671x "protected $".
- 31x "private $".
In vendor/:
- 1327x "protected function".
- 790x "private function".
- 1251x "protected $".
- 1649x "private $".

I would prefer not to have this conversation on every issue I participate in, but rather have it once.
I also would like to avoid turning this into a meaningless bikeshed or a purely opinion-based war with two camps. I hope we can at least achieve a minimal consensus.

A lot of arguments about this can be found on the web. Here I am trying to summarize the points, make it a bit more Drupal-specific, and also shift the focus of the question to something which is hopefully more productive. Instead of "which access modifier is preferable" we should ask "which architectural style is preferable" or "which architectural style are we practically following", and base the decision on that.

General thoughts

The protected access modifier (on a non-abstract member) allows child classes to reuse or override a part of the base class.
On the other hand, these members can no longer be refactored without breaking the child classes (which we may not know about).

In unit tests, public methods can be tested without special tricks. protected and private methods can be tested with reflection (but generally this is not wanted or needed).

Small, "SOLID" classes

If your classes are small and based on SOLID principles, then there is little need for inheritance to reuse parts of a class. The class is already the atomic unit to either reuse or replace as a whole. If any part of a class should be reusable, without reusing the whole class, then this part should probably be factored out of the class: Into a static method (a "pure function", ideally), or into a new, separate class (possibly with an interface, so it can be cleanly injected).

If the respective functionality is too awkward or too one-off to deserve to be in a public static method, or a dedicated class (+ interface), then it is ok to have a common base class with (abstract or not) protected methods. The protectedness of these methods is not because someone might want to override or call them. They are specifically designed for this purpose.

Refactoring this code means either changing private methods (which is ok and risk-free), or creating alternative implementations or even new interfaces, while leaving the old classes and interfaces in place for BC.

Members that are "protected by design" are considered API, and changing them would be considered a BC break.

Bigger, non-SOLID classes

Most of the classes in Drupal are quite big, and far from SOLID. When trying to reuse parts of their functionality, inheritance is often the only or the cheapest option. The "private" access modifier would make this very hard. I think this is how this general unwritten standard came into existence.

On the other hand: Whenever someone tries to refactor these protected methods, the question comes up whether some child class might exist that we would break with this change.

Here, the protected keyword is a sign that we don't really know what we are doing, we don't really have a clear design in mind. Someone might want to override or reuse this in a child class. But we don't really know why or how.

Test classes

Test classes generally are not meant for inheritance.

Helper methods in test classes often have a reuse value. But the ideal way to reuse them is not by inheriting another test class. Instead, they should be moved to a shared base class, or moved to a helper class, or become public static methods (on the test class itself, or an "utility" class).

A question of architectural style

To summarize: There is one style where we stuff a lot of stuff into a class and then someone might want to write a child class. We use protected for everything, because we don't really know what is required for inheritors.

The other style is inheritability by design. For every member we make an intentional choice whether it is designed for reuse or overriding, or not. With this style, "private" is the default, and in the few cases we use "protected", it can be understood as a promise.

Question / proposal

The question is whether the "inheritability by design" approach really has any place in core development.
So far it seems not.

If it does, we need to be able to use "private" more often.

Web references

http://programmers.stackexchange.com/questions/312425/why-have-private-f...
http://programmers.stackexchange.com/questions/162643/why-is-clean-code-...
http://fabien.potencier.org/pragmatism-over-theory-protected-vs-private....

Comments

donquixote created an issue. See original summary.

donquixote’s picture

Issue summary: View changes
donquixote’s picture

Issue summary: View changes
dawehner’s picture

I agree, private works when you have small classes, but we aren't there yet, especially, well, the concept of plugins for example makes that difficult. Plugins are more designed to be one-off implementations rather than a combined package of multiple examples. Just my 2 cents for now, maybe more at some other point in time.

donquixote’s picture

Well, I would have some idea how the plugin system could have been done.. but this is too late now I suppose.
Just had a quick look at BlockPluginInterface. extends 5 other interfaces, and adds 8 additional methods into the mix. This system demands inheritance as a primary tool for code reuse, and thus demands the protected keyword.

But in some cases we are actually writing new code, and have a choice.

Version: 8.2.x-dev » 8.3.x-dev

Drupal 8.2.0-beta1 was released on August 3, 2016, which means new developments and disruptive changes should now be targeted against the 8.3.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.3.x-dev » 8.4.x-dev

Drupal 8.3.0-alpha1 will be released the week of January 30, 2017, which means new developments and disruptive changes should now be targeted against the 8.4.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.4.x-dev » 8.5.x-dev

Drupal 8.4.0-alpha1 will be released the week of July 31, 2017, which means new developments and disruptive changes should now be targeted against the 8.5.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.5.x-dev » 8.6.x-dev

Drupal 8.5.0-alpha1 will be released the week of January 17, 2018, which means new developments and disruptive changes should now be targeted against the 8.6.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

donquixote’s picture

I opened #2956549: Policy: Atomic behavior objects , which proposes to add more "behavior objects" as opposed to other object types.
For such objects, final and private keywords are a natural fit.

donquixote’s picture

I found a nice example of this problem.

Drupal\Core\Utility\ThemeRegistry and some other classes currently inherit from Drupal\Core\Cache\CacheCollector.

All properties of CacheCollector are protected instead of private.
Classes that inherit from CacheCollector can and do all read from and write to these properties.

This means, on "Find usages" for e.g. CacheCollector::$storage, the IDE shows me 8 distinct files/classes.
I was originally trying looking for refactoring options for ThemeRegistry, but instead I am now looking at CacheCollector.

If those properties were private, it would force a nice separation between the base class and its child classes.
The method CacheCollector::get() would have to write the result of ::resolveCacheMiss() into ->storage, so that ::resolveCacheMiss() no longer needs to do that.

Even better, we could have an injected dependency provide the functionality of ::resolveCacheMiss(), so that we wouldn't need to inherit from CacheCollector all the time.

donquixote’s picture

Re #11: This also makes it impossible, or very hard, to ever refactor CacheCollector, because its implementation details leak to child classes.
The only thing we could do, without breaking child classes, is to create a CacheCollector2 with private properties, which could be used as an alternative.

E.g currently we initialize CacheCollector::$storage with NULL values, and then check isset() + array_key_exists() for ->has().
When looking at this code, I had the idea to introduce a separate property ::$knownKeys which would be initialized with TRUE instead of NULL, so that one isset() would be enough, and ::$storage would be used exclusively for actual values.

It can be debated whether this would be an improvement. But the current architecture with "protected" instead of "private" shuts this down before we even try.

donquixote’s picture

I agree, private works when you have small classes, but we aren't there yet,

Just had a quick look at BlockPluginInterface. extends 5 other interfaces, and adds 8 additional methods into the mix. This system demands inheritance as a primary tool for code reuse, and thus demands the protected keyword.

I have to correct myself.

Indeed for big classes, inheritance is the only viable way to extend or modify functionality.
For this we want overrideable protected methods.

However, a subclass does not necessarily need access to all properties.
Instead of accessing parent properties directly, a subclass can intercept them in the constructor and write its own copy in its own private variables.
This may ring an alarm of "Oh no, memory usage doubled!" But most of these are just object pointers, and the containing class will be instantiated only once per request if it is a service.

And for those who just find it awkward to have multiple copies of the same value in an object:
The benefit of private properties outweighs this supposed awkwardness.

All usages of a private property, read and write, will be in the same file. This would be a great benefit compared to the current situation.

Such a change does require some rethinking, but it does not require small classes.

Sam152’s picture

I've agreed with the sentiment of this issue for a long time. I think generally speaking we should accept private methods and properties into the codebase where it makes sense. Changing the visibility from private to protected is a good prompt to ask questions like you raised in #11, can some form of composition help instead of relying on an inheritance chain. It's also a huge boon for refactoring as already mentioned.

Is there anything preventing someone from writing a core patch with private methods right now or providing that feedback in a review? The only reference I could find to it explicitly allows "private": https://www.drupal.org/docs/develop/coding-standards/object-oriented-cod...

donquixote’s picture

@Sam152 (#14)

Is there anything preventing someone from writing a core patch with private methods right now or providing that feedback in a review?

It has been brought up in code reviews.

#2953921-14: Refactor out theme hook suggestion building from ThemeManager::render() into a separate function.

I think this should be protected instead of private. We don't yet have resolution on #2727011: Policy: private vs protected, and the role of inheritance, and until we do, I think we should stick with Drupal core's existing convention of virtually never using private methods. I don't see any reason for this one to justify a deviation from the current convention.

#2954402-28: Refactor ThemeManager::render(), split into smaller immutable objects.

All of these private scopes are a BC break. This is supposed to be abstracting the public render method, a method that has and can be, subclassed by replacing the service.

By moving everything to the private scope, this is completely obscuring functionality that was previously accessible.

#2954402-32: Refactor ThemeManager::render(), split into smaller immutable objects.

There are actually very rare use cases to make properties/methods private in core. Make them protected please.

Perhaps there could be valid reasons for protected properties in some cases..
I personally doubt it, I have not introduced a protected property my own code in ages, and never missed it.
But if someone makes a solid argument, there should of course be a case-by-case discussion.
Also, a lot of times we will need to keep existing protected properties for BC (which, ironically, is one of the main reasons to avoid them).

The main goal of this issue is to remove what seems to be an implicit convention.

donquixote’s picture

Note:
In #2954402-28: Refactor ThemeManager::render(), split into smaller immutable objects. @markcarver is actually making such a case-specific argument.
(unlike the other comments which appeal to implicit convention)
I happen to disagree with his argument, but totally approve that this should be openly discussed.

Sam152’s picture

Yeah, I think we can totally put converting existing protected visibility to private to one side, no disagreement that we'd be breaking BC here. The review you linked that referenced sticking to convention was blocked behind this issue, so perhaps that can also be "resolved" if consensus can be reached here :)

Lets not forget, there are no BC implications with changing the visibility from private to protected if at some point in time there is a compelling reason to use inheritance to solve a particular problem.

Tagging for framework manager review.

dawehner’s picture

Lets not forget, there are no BC implications with changing the visibility from private to protected if at some point in time there is a compelling reason to use inheritance to solve a particular problem.

Sadly that's not true either. If you have a subclass and you redefine the variable, you suddenly end up competing about it.

andypost’s picture

Another huge issue with private properties is serialization, basically dependency serialization trait can't restore private properties so a lot of services & the trait will need rework

donquixote’s picture

Sadly that's not true either. If you have a subclass and you redefine the variable, you suddenly end up competing about it.

This is true.
A private property of parent class can coexist with a property (private, protected or public) of a child class if they have the same name, containing distinct values. https://3v4l.org/X2ZRv

Once the parent property is made protected or public, while the child property is private or has lower access level than the parent property, there is a "Fatal error: Access level to C::$x must be protected (as in class B) or weaker". https://3v4l.org/L11de

If both are made protected, their values are no longer independent.
https://3v4l.org/aELVv

Of course similar clashes can occur for any new non-private method that you introduce in a parent class, if there happens to be method with the same name in an inheritor. Afaik we are not currently considering this a BC break, or do we?
Perhaps the name clash with private properties made protected is more likely, if people have a habit of redeclaring the property in inheritors.

My response to this would be: There is no reason to ever make a private property protected.
For an immutable property that is only set in the constructor and then never changed, the child class can simply make a copy in the constructor.
For a mutable property (which only occurs in mutable objects), the current value can either be exposed via a protected getter, or the child class can somehow intercept the method that leads to the modification.

Overall the necessity to access (mutable) parent properties should be the exception rather than the norm.
Of course this depends on the design of those classes.

donquixote’s picture

Another huge issue with private properties is serialization, basically dependency serialization trait can't restore private properties so a lot of services & the trait will need rework

I am not very fond of this object serialization anyway, or of traits.
But obviously since this already happens we do need to support it.
I am confident it can be done, one way or another.

One idea I have for now is to have a new trait that would be embedded in each inheritance level of the class.

class B {
  use AdvancedDependencySerializationTrait;
}

class C extends B {
  use AdvancedDependencySerializationTrait;
}

Not 100% sure yet how this would look like, but I think it could get us somewhere.

There would need to be a mechanism to prevent that non-private properties are initialized more than once.
This problem would only occur for classes that have a mix of protected and private properties.
Otherwise the AdvancedDependencySerializationTrait could be used for class hierarchies with only private properties, while the regular DependencySerializationTrait could be used for class hierarchies with only protected properties.

Other ways to access private properties of a parent class are reflection and, I think to remember, some trick with closures. But I think those would add confusion and would harm performance.

Version: 8.6.x-dev » 8.7.x-dev

Drupal 8.6.0-alpha1 will be released the week of July 16, 2018, which means new developments and disruptive changes should now be targeted against the 8.7.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.7.x-dev » 8.8.x-dev

Drupal 8.7.0-alpha1 will be released the week of March 11, 2019, which means new developments and disruptive changes should now be targeted against the 8.8.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.8.x-dev » 8.9.x-dev

Drupal 8.8.0-alpha1 will be released the week of October 14th, 2019, which means new developments and disruptive changes should now be targeted against the 8.9.x-dev branch. (Any changes to 8.9.x will also be committed to 9.0.x in preparation for Drupal 9’s release, but some changes like significant feature additions will be deferred to 9.1.x.). For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

Version: 8.9.x-dev » 9.1.x-dev

Drupal 8.9.0-beta1 was released on March 20, 2020. 8.9.x is the final, long-term support (LTS) minor release of Drupal 8, which means new developments and disruptive changes should now be targeted against the 9.1.x-dev branch. For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

plach’s picture

xjm’s picture

Title: Policy: private vs protected, and the role of inheritance » [policy, no patch] Private vs protected, and the role of inheritance
Issue tags: +Needs release manager review

Thanks for opening this discussion. I haven't had time to read through the discussion yet, but I definitely have some concerns and would like to help improve the policy.

xjm’s picture

In most cases, I think people think private is magic that protects them from having to worry about BC, but it doesn't really. An extending class will still have to reimplement the method with the same signature, or override all the callers as well as the private method, which is not great DX. In most cases it's not actually making things better.

This also has some overlap and similarity to the discussion about using final, which I'm adding as a related issue.

I think it would be good to discuss specific cases where private and final are actually advantageous, and I'm not sure we can discuss them separately.

xjm’s picture

In #3131900: Refactor assertions that assign return values to variables @longwave suggested that individual test class helpers should perhaps be private, and I might agree with that, but also I think maybe most individual test classes should just be final.

donquixote’s picture

@xjm
I mostly use private on properties, rarely on methods.

If a method is private, I very often find that it can be public static instead, and live in a utility class.
But a private method can be ok as an intermediate solution, while I am still undecided about the final shape of everything.

If a method is protected and meant to be overridden, then it is often possible to split out parts into a base class where this method is abstract protected.

One case where private methods are a good choice is for internal methods with fragile signatures, which have very specific expectations on the parameters and assume that sanity checks are already done by the calling code.

xjm’s picture

Thanks @donquixote; those are helpful suggestions. I think what we would need for a core policy that adopts private is more along those lines, maybe with code snippets: specific examples of where it is helpful, compared with cases where it is not.

mondrake’s picture

In #3131900: Refactor assertions that assign return values to variables @longwave suggested that individual test class helpers should perhaps be private, and I might agree with that, but also I think maybe most individual test classes should just be final.

Please, don't do that. One of the good things of Drupal is that you can override in contrib some of its functionalities. Developers may want to extend core tests (not base classes: concrete tests) so that they can check if their overrides are compatible with the core functionality. Personally, I do that for Pagerer. I override the PagerTest, and if the module tests break on DrupalCI then I would know that something has changed upstream - so I either fix the test, or adjust the runtime code for any new/changed functionality. That works pretty well NOW. Moving to final classes or private methods I would have to rewrite most of the test, and I have no guarantee that I can be notified if things change upstream, unless I start looking and deciphering the tweets of the CRs. I prefer the hard way - hey, a test fails! Let's fix the problem.

I know the other side is guys complaining about changing core tests... it's a trade off.

Look at PHPUnit - they're so strict on visibility that it's become very hard to extend, see for example #3125809: Remove usages of \PHPUnit\Framework\TestListener once PHPUnit 9 support is dropped, or #3133355-13: Introduce PHPUnit-compliant WebAssert::responseHeaderExists() method, and its negative. Personally I think in OSS we should facilitate extendibility and reuse, not complicating it to push out support issues.

EDIT - this comment is about test classes only, not runtime code.

Version: 9.1.x-dev » 9.2.x-dev

Drupal 9.1.0-alpha1 will be released the week of October 19, 2020, which means new developments and disruptive changes should now be targeted for the 9.2.x-dev branch. For more information see the Drupal 9 minor version schedule and the Allowed changes during the Drupal 9 release cycle.

donquixote’s picture

I finally found a good example in the Drupal codebase!

Drupal\Component\Plugin\Derivative\DeriverBase.

An alternative implementation of this class would look like this:



abstract class DeriverBase implements DeriverInterface {

  /**
   * Base plugin definition.
   *
   * Subclasses don't need access to this.
   *
   * @var array|null
   */
  private $basePluginDefinition;

  /**
   * Derived plugin definitions.
   *
   * Subclasses don't need access to this.
   *
   * @var array[]|null
   */
  private $definitions;

  /**
   * {@inheritdoc}
   */
  final public function getDerivativeDefinition($derivative_id, $base_plugin_definition): ?array {
    $definitions = $this->getDerivativeDefinitions($base_plugin_definition);
    return $definitions[$derivative_id] ?? NULL;
  }

  /**
   * {@inheritdoc}
   */
  final public function getDerivativeDefinitions($base_plugin_definition): array {

    if ($this->definitions === NULL
      // In the unexpected case where a deriver is called with a different base
      // plugin definition than in a previous call, all derivatives need to be
      // reset.
      || $this->basePluginDefinition !== $base_plugin_definition
    ) {
      $this->definitions = $this->buildDerivativeDefinitions($base_plugin_definition);
      $this->basePluginDefinition = $base_plugin_definition;
    }

    return $this->definitions;
  }

  /**
   * Builds derivative definitions for a given base plugin definition.
   *
   * @param array $base_plugin_definition
   *   The definition array of the base plugin.
   *
   * @return array
   *   An array of full derivative definitions, keyed by derivative id.
   */
  abstract protected function buildDerivativeDefinitions(array $base_plugin_definition): array;

}

With a base class like this, subclasses don't ever need to touch the properties of the base class.

Version: 9.2.x-dev » 9.3.x-dev

Drupal 9.2.0-alpha1 will be released the week of May 3, 2021, which means new developments and disruptive changes should now be targeted for the 9.3.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.3.x-dev » 9.4.x-dev

Drupal 9.3.0-rc1 was released on November 26, 2021, which means new developments and disruptive changes should now be targeted for the 9.4.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.4.x-dev » 9.5.x-dev

Drupal 9.4.0-alpha1 was released on May 6, 2022, which means new developments and disruptive changes should now be targeted for the 9.5.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.5.x-dev » 10.1.x-dev

Drupal 9.5.0-beta2 and Drupal 10.0.0-beta2 were released on September 29, 2022, which means new developments and disruptive changes should now be targeted for the 10.1.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 10.1.x-dev » 11.x-dev

Drupal core is moving towards using a “main” branch. As an interim step, a new 11.x branch has been opened, as Drupal.org infrastructure cannot currently fully support a branch named main. New developments and disruptive changes should now be targeted for the 11.x branch, which currently accepts only minor-version allowed changes. For more information, see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.