Interestingly, I've had to make a similar choice in a design I worked on recently. One approach is not strictly superior to another.
In your example, given no additional information, I would probably choose virtual methods - particularly since my intuition leads me to believe you would use inheritance to model different types of experiments, and you don't need the ability to have multiple subscribers (as events allow).
Here are some of my general observations about choosing between these patterns:
Events are great:
- when you don't need the caller to return any information, and when you want extensibility without requiring subclassing.
- if you want to allow the caller to have multiple subscribers that can listen and respond to the event.
- because they naturally arrange themselves into the template method pattern - which helps to avoid introducing the fragile base class problem.
The biggest problem with events is that managing the lifetime of subscribers can get tricky, and you can introduce leaks and even functional defects when subscribers stay subscribed for longer than necessary. The second biggest problem is that allowing multiple subscribers can create a confusing implementation where individual subscribers step on each other - or exhibit order dependencies.
Virtual methods work well:
- when you only want inheritors to be able to alter the behavior of a class.
- when you need to return information from the call (which event's don't support easily)
- when you only want a single subscriber to a particular extension point
- when you want derivatives of derivatives to be able to override behavior.
The biggest problem with virtual methods is that you can easily introduce the fragile base class problem into your implementation. Virtual methods are essentially a contract with derived classes that you must document clearly so that inheritors can provide a meaningful implementation.
The second biggest problem with virtual methods, is that it can introduce a deep or broad tree of inheritance just to customize how a behavior of a class is customized in a particular case. This may be ok, but I generally try avoiding inheritance if there isn't a clear is-a
relationship in the problem domain.
There is another solution you could consider using: the strategy pattern. Have your class support assignment of an object (or delegate) to define how Render, Init, Move, etc should behave. Your base class can supply default implementations, but allow external consumers to alter the behavior. While similar to event, the advantage is that you can return value to the calling code and you can enforce only a single subscriber.