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.
| Comment | File | Size | Author |
|---|---|---|---|
| #9 | interdiff.txt | 5.28 KB | wim leers |
| #9 | breadcrumb_composition_not_inheritance-2551907-9.patch | 9.32 KB | wim leers |
| #6 | 2551907-6.patch | 9.75 KB | dawehner |
| #6 | interdiff.txt | 6.57 KB | dawehner |
| #5 | breadcrumb_composition_not_inheritance-2551907-5.patch | 13.9 KB | wim leers |
Comments
Comment #2
wim leersThis patch:
Breadcrumbno longer extendCacheableMetadataRefinableCacheableDependencyInterface. This in fact closely aligns with a concern @alexpott raised at #2483183-180: Make breadcrumb block cacheable: that theBreadcrumbvalue 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.RefinableCacheableDependencyTraitBreadcrumbloses thesetCache(Contexts|Tags|MaxAge)()methods. This is a BC break, but, since this class has only existed for two days, that should be totally fine. Allset*()calls have been changed toadd*()calls.Breadcrumbloses theapplyTo()method. But, it already has a@todoto implementRenderableInterfacein HEAD. So, this just implementstoRenderable(), which we were planning to do already anyway. (See #2529560: Expand support for link objects.)CacheableDependencyTraitis added, which allows any class to implement justCacheableDependencyTrait;RefinableCacheableDependencyTraitthen simply usesCacheableDependencyTrait.CacheableMetadata's docs are updated to explain why it has setters, which is beyond what its interface requires.Comment #3
wim leersComment #5
wim leersPHPStorm--
(Functionally identical patch; it's just that PHPStorm had
git added a new file already, which my patch hence did not record.)Comment #6
dawehnerThis is my definition of composition.
Comment #7
dawehnerOh btw. the patch is much smaller now because we are using composition which decouples changes in one piece of code from their parent classes.
Comment #8
yched commented[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 ?
Comment #9
wim leersThat'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:
This is exactly the problem that traits solve. And AFAICT that's why PHP added it, see https://wiki.php.net/rfc/traits:
The only reason this patch is smaller is because it doesn't introduce
CacheableDependencyTraitand 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
Breadcrumbvalue object that has some of its own data, but then also implementsRefinableCacheableDependencyInterface. We don't want to write custom logic for that, so we just compose it using theRefinableCacheableDependencyTrait. That's it!Comment #11
dawehnerWell I just thought its out of scope :) Anyway great work!
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.
Comment #12
wim leersI'm fine with that :)
Glad you like it!
But sounds like you don't really like it? :P
Actually, your patch already supports that:
… 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.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?
Comment #13
yched commentedNot 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);
?
Comment #14
wim leers#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 theBreadcrumbvalue object no longer provides cacheability metadata itself, which means it no longer is aCacheableDependencyInterfaceimplementation.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....
Comment #15
dawehnerWell yeah, we want to keep implementing the interface this is for sure.
Well, then you could ask why we have the interface kinda in the first place to be honest ... you know what I mean.
Comment #16
wim leersInterfaces allow alternative implementations. We already have
UnchangingCacheableDependencyTraitwhich allows an object that is known to never change to easily implementCacheableDependencyInterface, 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?
Comment #17
dawehnerLet'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
Comment #18
wim leersI agree that makes no sense.
But, I do think that 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:
And that example is
AttachmentsInterface+AttachmentsTrait, introduced in #2407195: Move attachment processing to services and per-type response subclasses.Comment #19
yched commentedYeah, 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)
Comment #20
Crell commenteddawehner 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. :-)
Comment #21
wim leersDear god thank you yes!!!!!!!
I discussed this with catch almost 2 months ago and he agreed we should rename
CacheableMetadatatoCacheability. I fear it's too late now though :(That's not a hard question at all: it's
CacheableDependencyInterface. AndBreadcrumbin 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 CacheableableMetadatatoBreadcrumb implements CacheableDependencyInterface { use CacheableDependencyTrait; }.@Crell: how is
CacheableDependencyInterface+CacheableDependencyTraitdifferent fromAttachmentsInterface+AttachmentsTrait, which we added together in #2407195: Move attachment processing to services and per-type response subclasses?Comment #22
Crell commented#21:
"What I'm basically proposing here is..." - Yes please!
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.
Comment #23
Crell commentedOr in other words...
Comment #24
yched commentedYeah :-/ 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.
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"...) ?
Comment #25
yched commentedOpened #2561773: CacheableMetadata is misnamed
Comment #26
wim leers#25: The sole reason
CacheableMetadataexists 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 withCacheableMetadata.The main purpose of
CacheableMetadatais for computing the cacheability of various differentCacheableDependencyInterfaceobjects (access results, renderables, Entities, Contexts, et cetera) after they have been combined in some way to compute something else.Comment #27
yched commentedThanks @Wim, I replied in #2561773-7: CacheableMetadata is misnamed
Fine with RTBC here then :-)
Comment #30
jibranComment #32
catchI 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!
Comment #33
fgmThis needed an update to this change notice, though : https://www.drupal.org/node/2543936 , because setCacheContexts is replaced by addCacheContexts() with this patch. Added.