Thanks for the reply!
The decision to stick with the shared interface in principle is for educational reasons, as this implementation is the canonical way to implement the State pattern found on books about Design Patterns. Besides that, IMHO, removing the interface is a specific case decision, as this is the most semantic form to represent contracts and allows for abstractions to not depend on specific types, like the Pizza union type.
Finally, the goal of this design is to contribute to the whole architecture in its main mission: point the most correct way to extend the application. As such, it could be counterproductive to have really tight constraints enforced on the type system level. In my vision, the type system is another tool to indicate how to grow the implementation instead of a guard limiting the application's capabilities.
I can see that. My personal preference is to prevent the need for exceptions as much as possible; if there's some way to keep the application from getting in an invalid state before runtime, I'd rather see that instead.
I don't necessarily agree that it would limit the application's future capabilities. If, for some reason, the company later decides that's it's perfectly reasonable to deliver an un-baked pizza, you'd still need to make appropriate changes - remove the exception throwing logic and create the appropriate state object. If that method hadn't been previously available, you'd add it (maybe extract a "DeliverablePizza" interface) and then work the logic in similarly.
The key difference is that, with the method available but just throwing an exception, someone can call it and wonder why it doesn't work. The Pizza interface contract provides for delivery - why doesn't this object obey it's contract? Without the method provided, there's no confusion, it simply isn't possible to deliver a pizza that isn't in a deliverable state.
I think I probably focused on a different point than your first comment. I agree that avoid unnecessary exceptions that could lead to invalid states is better. In order to avoid it, one could still make this methods no-ops that log messages informing that they were called.
The point in using an interface is that in my team we usually see the software in a lifecycle of code -> stabilize -> protect -> experiment. In that way, introducing new methods, changing the interface, in a domain we already understand the behavior would make the need to introduce breaking changes to code that is already stabilized and protected instead of just replacing an implementation detail. Although, if it is a domain we don't have a grasp yet, this approach would certainly be used.
Along with this, interpreting the methods as commands send to the object, a specific command be accepted (i.e. the method being in the interface) but not processed (i.e. it is a no-op) is a common pattern when following CQS or in CQRS architecture, which aligns with the event-driven approach of this design.
It is really nice to have this kind of feedback once this was the main drive for me to start writing. Thank you.
In some cases a no-op is fine - for a stretch I was a big fan of the null object pattern - but in this particular case I'd honestly rather see an exception than a no-op. I can see myself being that idiot coder who skipped the docs and is tearing his hair out wondering why my un-baked pizza isn't delivering.
I'm also seeing myself as increasingly old fashioned though - I'm not a fan of dynamic languages because I want this kind of type safety we're discussing - so I may not be able to fully see your side of it.
We're a place where coders share, stay up-to-date and grow their careers.
We strive for transparency and don't collect excess data.