> Inheritance is not a banned practice.
Maybe it should be. Go and Rust do not provide implementation inheritance, and I think that's for the best. Few language features have led to so much spaghetti code.
> It should just be your second choice when there's a better path through composition. Do you see one here?
I don't think this logic should be split over a graph of objects at all. This is highly cohesive code; it shouldn't be factored apart. If Metz made it clear that this was an example just for the purposes of illustration, that'd be one thing. However, the stance taken is "this is good code, and you should try to write all your code like this." It's not, though, and you shouldn't.
> Maybe it should be. Go and Rust do not provide implementation inheritance, and I think that's for the best. Few language features have led to so much spaghetti code.
I agree, but there are reasons inheritance becomes problematic. Just throwing the baby is not helpful. Go, Elixir, Rust are all relatively young languages and although they did away with inheritance, they make use of interfaces/protocols/traits, hinting at that idea very much worth preserving. That is, regardless of which language you work with, if you have access to facilities that can make the concept of interface work decently, use them. Older languages (like Ruby, Python, Java) tended to use the inheritance mechanism to accomplish the same.
> If Metz made it clear that this was an example just for the purposes of illustration
She did. Perhaps read the book's introduction. She explains why she wrote a whole book that uses a banal example as a teaching tool to illustrate how SOLID should map to real world OOP. Her painstakingly going through refactoring and providing reasons for each decisions is not, in fact, to teach us to write code that generates a song.
> I don't think this logic should be split over a graph of objects at all [...] However, the stance taken is "this is good code, and you should try to write all your code like this." It's not, though, and you shouldn't.
Fair enough, but these are your very own and very isolated opinions. As an OO skeptic myself, I'll side with others like me, along with the FP enthusiasts, who originally approached this book with reserve, but came out with positive impressions.
Regarding the object graph, whether in Go, Elixir, or Rust, "No Bottle", "One Bottle", "Six Pack", and "Many Bottles" are distinct things and should be represented accordingly. Conflating them is a violation of principles that also apply in those languages. A very common, yet equally banal example that should put the debate to rest about this is the trifecta: Shape.Area(), Square.Area(), and Circle.Area(). Of course, it remains the programmer's prerogative whether they indulge with if/else in their implementation, but it should still be considered the exception rather than the rule.