Problem/Motivation

After #2483183: Make breadcrumb block cacheable got committed, @yched revived a recent Twitter conversation: https://twitter.com/yched/status/631907531120607234

@yched: @da_wehner @wimleers Breadcrumb would have seemed to me a good example of composition being more suitable than inheritance ;-)

To which @dawehner replied:

@da_whener: @yched @wimleers You are absolute right here!

Proposed resolution

Make @yched & @da_wehner happy, and in the process end up with better code.

The beauty is that #2526326: Update CacheableMetadata & AccessResult to use RefinableCacheableDependency(Interface|Trait) just landed, which makes this quite easy to fix.

Remaining tasks

Review.

User interface changes

None.

API changes

Breadcrumb loses its set*() methods and applyTo(), i.e. any of the public methods that CacheableMetadata was providing beyond what RefinableCacheableDependencyInterface requires.

Data model changes

None.

Comments

Wim Leers created an issue. See original summary.

wim leers’s picture

Status: Active » Needs review
Related issues: +#2529560: Expand support for link objects
StatusFileSize
new13.93 KB

This patch:

  1. first and foremost makes Breadcrumb no longer extend CacheableMetadata
  2. so, instead, it now implements RefinableCacheableDependencyInterface. This in fact closely aligns with a concern @alexpott raised at #2483183-180: Make breadcrumb block cacheable: that the Breadcrumb value object should be overridable, only refinable. The patch made sure that was the case for the links in the breadcrumb. This change ensures it's also true for its cacheability metadata.
  3. to not repeat a lot of code, it just uses RefinableCacheableDependencyTrait
  4. A consequence: Breadcrumb loses the setCache(Contexts|Tags|MaxAge)() methods. This is a BC break, but, since this class has only existed for two days, that should be totally fine. All set*() calls have been changed to add*() calls.
  5. Another consequence: Breadcrumb loses the applyTo() method. But, it already has a @todo to implement RenderableInterface in HEAD. So, this just implements toRenderable(), which we were planning to do already anyway. (See #2529560: Expand support for link objects.)
  6. CacheableDependencyTrait is added, which allows any class to implement just CacheableDependencyTrait; RefinableCacheableDependencyTrait then simply uses CacheableDependencyTrait.
  7. CacheableMetadata's docs are updated to explain why it has setters, which is beyond what its interface requires.
wim leers’s picture

Issue summary: View changes

Status: Needs review » Needs work

The last submitted patch, 2: breadcrumb_composition_not_inheritance-2551907-2.patch, failed testing.

wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new13.9 KB

PHPStorm--

(Functionally identical patch; it's just that PHPStorm had git added a new file already, which my patch hence did not record.)

dawehner’s picture

StatusFileSize
new6.57 KB
new9.75 KB

This is my definition of composition.

dawehner’s picture

Oh btw. the patch is much smaller now because we are using composition which decouples changes in one piece of code from their parent classes.

yched’s picture

[edit: crosspost with @dawehner, but I guess I'm basically saying something similar :-) ]

Not 100% familiar yet with the classes and APIs around cacheable metadata, but I would have naively expected a change from "Breadcrumb is a CacheableMetadata" to "... has a CacheableMetadata(Interface)", with CacheableMetadata being the unique and universal value objects for cacheability metadata.

But turns out you can have cacheability metadata carried outside a CM object, with tags, contexts, etc. in raw properties of objects implementing their own, unrelated primary domain interface ?

So cacheability metadata can be carried in pure value objects, available for composition, or directly in other domain objects by interface inheritance & helper traits ?

wim leers’s picture

That's also what I first thought that you guys meant, and what https://en.wikipedia.org/wiki/Composition_over_inheritance describes.

But it has that pesky downside, that Wikipedia describes so well:

One drawback to using composition in place of inheritance is that all of the methods being provided by the composed classes must be implemented in the derived class, even if they are only forwarding methods.

This is exactly the problem that traits solve. And AFAICT that's why PHP added it, see https://wiki.php.net/rfc/traits:

They are recognized for their potential in supporting better composition and reuse, hence their integration in newer versions of languages such as Perl 6, Squeak, Scala, Slate and Fortress. Traits have also been ported to Java and C#.


Oh btw. the patch is much smaller now because […]

The only reason this patch is smaller is because it doesn't introduce CacheableDependencyTrait and reverts the clean-up elsewhere I can do that too :). Rerolled, relative to #5 again.


So, given that… what's not to like? We have a Breadcrumb value object that has some of its own data, but then also implements RefinableCacheableDependencyInterface. We don't want to write custom logic for that, so we just compose it using the RefinableCacheableDependencyTrait. That's it!

The last submitted patch, 6: 2551907-6.patch, failed testing.

dawehner’s picture

The only reason this patch is smaller is because it doesn't introduce CacheableDependencyTrait and reverts the clean-up elsewhere I can do that too :). Rerolled, relative to #5 again.

Well I just thought its out of scope :) Anyway great work!

So, given that… what's not to like? We have a Breadcrumb value object that has some of its own data, but then also implements RefinableCacheableDependencyInterface. We don't want to write custom logic for that, so we just compose it using the RefinableCacheableDependencyTrait. That's it!

Well, its still fundamental something different. You implement a breadcrumb against an abstract interface regarding one concrete implementation.
So for example $breadcrumb = new BreadCrumb(new ForumThread()); and boom, the breadcrumb inherits everything from the forum thread (for sure more of a pseudo code).
Traits IMHO are, and really just should be, a tool for c&p code. Once you start thinking about that, you realize that c&p in the first place shows bad design in your software and you
maybe do something better.

wim leers’s picture

Well I just thought its out of scope :)

I'm fine with that :)

Anyway great work!

Glad you like it!

Well, its still fundamental something different.

But sounds like you don't really like it? :P

So for example $breadcrumb = new BreadCrumb(new ForumThread()); and boom, the breadcrumb inherits everything from the forum thread (

Actually, your patch already supports that:

+++ b/core/lib/Drupal/Core/Breadcrumb/Breadcrumb.php
@@ -15,7 +16,21 @@
+  public function __construct(RefinableCacheableDependencyInterface $cacheable_metadata = NULL) {
+    $this->cacheableMetadata = $cacheable_metadata ?: new CacheableMetadata();
+  }

… but what if the object that's passed in (e.g. ForumThread) is enormous? Then we're carrying along a whole lot of cruft & baggage that is unwanted.

Once you start thinking about that, you realize that c&p in the first place shows bad design in your software and you
maybe do something better.

Agreed on the principle, but I think this is the exception. Cacheability metadata is not something vast. It's very specific, very well-defined. And it's something many things want/need. Therefore I think that specifically for the case of value objects that also have cacheability metadata, traits are an excellent choice.

Thoughts?

yched’s picture

One drawback to using composition in place of inheritance is that all of the methods being provided by the composed classes must be implemented in the derived class, even if they are only forwarding methods.

Not sure that's the case here, since we already have the CM value objects that provides the API. No need to forward those, just provide access to the contained value object ?

So instead of :
$breadcrumb->addTag($tag);
you would write :
$breadcrumb->getCacheableMetadata()->addTag($tag);
?

wim leers’s picture

#13: it is the case if you look at @dawehner's patch. Doing what you describe (having a getCacheableMetadata() method) would be problematic: it'd mean that the Breadcrumb value object no longer provides cacheability metadata itself, which means it no longer is a CacheableDependencyInterface implementation.

Which means it would break the entire point of CacheableDependencyInterface: objects you use and interact with, if you use them in a computation, their cacheability metadata should be easily trackable. This breaks that ease. Because you wouldn't be able to do ->addCacheableDependency($breadcrumb) anymore.

See https://www.drupal.org/developing/api/8/cache/cacheable-dependency-inter....

dawehner’s picture

Well yeah, we want to keep implementing the interface this is for sure.

Agreed on the principle, but I think this is the exception. Cacheability metadata is not something vast. It's very specific, very well-defined. And it's something many things want/need. Therefore I think that specifically for the case of value objects that also have cacheability metadata, traits are an excellent choice.

Well, then you could ask why we have the interface kinda in the first place to be honest ... you know what I mean.

wim leers’s picture

Well, then you could ask why we have the interface kinda in the first place to be honest ... you know what I mean.

Interfaces allow alternative implementations. We already have UnchangingCacheableDependencyTrait which allows an object that is known to never change to easily implement CacheableDependencyInterface, for example.

So I don't fully agree with what you're getting at, but I see your point.

How do we find consensus here?

dawehner’s picture

Let's make another point. Inheritance is for using a "A is a B" relationship. "A breadcrumb is a CacheableMetadata" ... For me its not the fundamental property of the breadcrumb to be a cacheable metadata enriched thing.

What about asking crell about it? Pinged him on IRC

wim leers’s picture

I agree that A breadcrumb is a CacheableMetadata makes no sense.

But, I do think that A breadcrumb is a cacheable dependency does make sense. And AFAICT you agree with that. It's what both your patch and mine do/implement, just in different ways.


I just realized we already have an example in core, that kinda follows the line of reasoning I explained:

Cacheability metadata is not something vast. It's very specific, very well-defined. And it's something many things want/need. Therefore I think that specifically for the case of value objects that also have cacheability metadata, traits are an excellent choice.

And that example is AttachmentsInterface + AttachmentsTrait, introduced in #2407195: Move attachment processing to services and per-type response subclasses.

yched’s picture

having a getCacheableMetadata() method would be problematic: it'd mean that the Breadcrumb value object no longer provides cacheability metadata itself, which means it no longer is a CacheableDependencyInterface implementation.

Which means it would break the entire point of CacheableDependencyInterface: objects you use and interact with, if you use them in a computation, their cacheability metadata should be easily trackable. This breaks that ease. Because you wouldn't be able to do ->addCacheableDependency($breadcrumb) anymore

Yeah, I guess that's the crux of what I'm having troubles mapping out exactly, the coexistence of :
- CacheableMatadata : pure value objects holding [contexts, tags, max-age]
- and CacheableDependencyInterface : (other-)domain objects that, on top of their own business logic and properties, directly hold contexts, tags, max-age information without the wrapping value object.

In short, what puzzles me is why CacheableDependencyInterface is :
getCacheContexts()
getCacheTags()
getCacheMaxAge()

rather than specifically "I hold cacheability metadata" (= I "have a" CacheableMetdataInterface, composition), with just :
getCacheableMetadata()
and CM value objects being the universal way to carry or manipulate [contexts, tags, max-age]

(on a related note, CacheableMetadata trips me up each time as implying that it is metadata that you can cache. Rather, it's the metadata describing the cacheability of something else --> CacheabilityMetadata would be more accurate ?
CacheableDependencyInterface or "being a cacheable dependency", well, doesn't really carry a clear meaning to me... :-p)

Crell’s picture

dawehner asked me to jump in here.

Traits in PHP are compile time copy-paste. That means they are best suited for cases where the alternative is code-time copy-paste. If the code in question is something that logically makes sense on its own, it should be a composed object. If not, and it's just boilerplate you don't want to retype, a trait a great fit.

Class implements interface: "can be treated as a"
Class extends class: "Is a special case of"
Class happens to use the same stock code as Class: Trait

"is a" is too generic a relationship, but "can be treated as" and "is a special case of" tend to work as guidelines.

In this case, then, I would say (and this goes a bit into Breadcrumb, too, since I didn't even see the breadcrumb issue until it got committed):

Breadcrumb is an object until itself. It is not a special case of anything, thus should extend nothing.
Breadcrumb is, conceptually, an ordered collection of links/Link objects. Thus it should be coded as a collection. Maybe with ArrayAccess, or Traversable, or something like that.
Breadcrumb can also expose cache metadata. Thus, it should implement whatever the "Yo, I have cache metadata you should ask about" interface is. (The names here still confuse the heck out of me, but that's what it means semantically.)
If a significant percentage of objects that say "yo, I have cache metadata" have the same implementation of that interface, because it's just boilerplate array property manipulation, then that's totally a good use of a trait.

It sounds like part of the question here is "what interface means 'yo, I have cache metadata you should ask about'?" If that's a hard question it indicates that the interfaces may need to be refactored, if possible, because that really needs to NOT be a hard question. :-)

wim leers’s picture

on a related note, CacheableMetadata trips me up each time as implying that it is metadata that you can cache. Rather, it's the metadata describing the cacheability of something else --> CacheabilityMetadata would be more accurate ?

Dear god thank you yes!!!!!!!

I discussed this with catch almost 2 months ago and he agreed we should rename CacheableMetadata to Cacheability. I fear it's too late now though :(

It sounds like part of the question here is "what interface means 'yo, I have cache metadata you should ask about'?"

That's not a hard question at all: it's CacheableDependencyInterface. And Breadcrumb in fact does implement that interface. But rather than repeating the implementation from elsewhere, we're just piggybacking on the pure-cacheability-metadata-value class (CacheableMetadata) and thus extending it.
EDIT: that's what HEAD does. What I'm basically proposing here is that we change Breadcrumb extends CacheableableMetadata to Breadcrumb implements CacheableDependencyInterface { use CacheableDependencyTrait; }.

@Crell: how is CacheableDependencyInterface + CacheableDependencyTrait different from AttachmentsInterface + AttachmentsTrait, which we added together in #2407195: Move attachment processing to services and per-type response subclasses?

Crell’s picture

#21:

"What I'm basically proposing here is..." - Yes please!

@Crell: how is CacheableDependencyInterface + CacheableDependencyTrait different from AttachmentsInterface + AttachmentsTrait, which we added together in #2407195: Move attachment processing to services and per-type response subclasses?

I give up, how? :-) Seems like the exact argument applies here.

Composition would make sense if:
1) The object in question, if it existed, would have dependencies of its own.
2) The object in question, if it existed, would have meaning unto itself as a complete "thing".

Neither is the case here. A trait is the right tool.

Crell’s picture

Status: Needs review » Reviewed & tested by the community

Or in other words...

yched’s picture

I discussed this with catch almost 2 months ago and he agreed we should rename CacheableMetadata to Cacheability. I fear it's too late now though :(

Yeah :-/ Given the importance render cache has in D8 (currently and even more after SmartCache), and the fact that it's ... complex enough ;-) (see the discussions in #2556889: [policy, no patch] Decide if SmartCache is still in scope for 8.0 and whether remaining risks require additional mitigation), I'd think not having misleading names add to the complexity is worth the disruption. I'll create the issue and ask for release manager feedback.

Composition would make sense if:
1) The object in question, if it existed, would have dependencies of its own.
2) The object in question, if it existed, would have meaning unto itself as a complete "thing".

So that's what trips me up. The object in question does exist, and does have meaning as a complete "thing" : it's CacheableMetadata (which would be better named CacheabilityMetadata or Cacheability, as per beginning of #21), which *are* the value objects holding "cacheability info" (= [contexts, tags, max-age]).

So, unless I'm missing something :
- Those CM objects are used for composition in some places (some things "have a" CM that describes their cacheability)
- And some other objects chose to *not* "have a CM", completely overlook the existence of that "value object" class, and instead directly hold [contexts, tags, max-age] properties among their other business properties (with direct getMaxAge(), getContexts() methods in the middle of their other business methods)
- The latter is what our current "I have cacheability metadata" interface, CacheableDependencyInterface, encourages

Two patterns for carrying "cacheability metadata", sometimes with composition and a CM value object ("I have a"), sometimes with inheritance and direct properties/methods ("I am a"), is what confuses me. It also means we have to duplicate the APIs that provide easy manipulation of the metadata ("add tag", "reduce max age"...) ?

yched’s picture

wim leers’s picture

#25: The sole reason CacheableMetadata exists as its own thing, is to do calculations with cacheability metadata. I.e. for internal uses. Perhaps it should've been marked as @internal. (Perhaps we can do that in #2561773?)

So yes, every class should implement CacheableDependencyInterface, and no, you should generally not ever interact with CacheableMetadata.

The main purpose of CacheableMetadata is for computing the cacheability of various different CacheableDependencyInterface objects (access results, renderables, Entities, Contexts, et cetera) after they have been combined in some way to compute something else.

yched’s picture

Thanks @Wim, I replied in #2561773-7: CacheableMetadata is misnamed

Fine with RTBC here then :-)

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 9: breadcrumb_composition_not_inheritance-2551907-9.patch, failed testing.

jibran’s picture

Status: Needs work » Reviewed & tested by the community

  • catch committed 0f9fd87 on 8.0.x
    Issue #2551907 by Wim Leers, dawehner: Follow-up for #2483183: make the...
catch’s picture

Status: Reviewed & tested by the community » Fixed

I nearly asked for a change notice but for something that's only existed for weeks it's probably better to have nothing.

Committed/pushed to 8.0.x, thanks!

fgm’s picture

This needed an update to this change notice, though : https://www.drupal.org/node/2543936 , because setCacheContexts is replaced by addCacheContexts() with this patch. Added.

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.