This is a bit of an invented example but I think it best illustrates my question: Say I'm creating a chess replay event API. Say I have a lot of different "events" I want to keep track of, in a specified order. Here might be some examples:
-
A move event — this contains the
previous
andnew
square. -
A timer event — this contains the
timestamp
that the timer was toggled between players -
A chat message event — this contains the
player ID
, themessage
andtime sent
…etc. The point is that the data model for each event is very different — there isn't much of a common interface.
I want to design an API that can store and expose essentially a List<Event>
to a client who can choose to process these different events as they wish. We don't know what clients will do with this information: Perhaps one client may need to do text analysis on the ChatMessageEvent
s, and one may consume and replays these events in the UI. The challenge is that ordering between events must be preserved, so I can't separate by methods like getMoveEvents
and getTimerEvents
since a TimerEvent
can happen between move events and the client may need that information.
I could expose a visitor to allow clients to handle each event type differently in the list, but I'm wondering if there's a better way to handle a situation like this.
Edit: I want to design this with one main priority: provide clients with an easy and flexible way to iterate through these events. In an ideal scenario, I would envision the end user writing handlers to the event types they care about, and then be able to iterate through without casting based on the runtime type.
Best Answer
I am under the strong impression you are overthinking this.
Then simply don't offer such methods in your API. Let the client filter out the events they need, and do not implement anything in your API which could become error prone.
This sounds overengineered. You described the requirement as getting something like a
List<Event>
, containing recorded events. For this, a simple methodList<Event> getEvents()
would be totally sufficient (maybe anIEnumerable<Event>
would be enough). For reasons of efficiency, it may be necessary to offer some methods for restricting the result set to certain conditions.Asking for a "better" (or "best", or "correct") approach is way too unspecific when you don't know any criteria for what you actually mean by "better". But how do find criteria for what is "better"? The only reliable way I know for this problem is:
Define some typical use cases for your API!
Do this in code. Write down a short function which tries to use your API, solving a real problem you know for sure the clients will encounter (even if the API does not exists or is not implemented yet).
It may turn out the client will need something like a property to distinguish event types. It may turn out the client needs something to get only the events from the last hour, or the last 100 events, since providing him always a full copy of all former events may not be effcient enough. It may turn out the client needs to get a notification whenever a new event is created.
You will only be able to decide this when you develop a clear idea of the context in which your API will be used.
If you add some code to this function which verifies the API's result, and place this code into a the context of a unit testing framework, then you are doing "Test Driven Development"
But even if you don't want to use TDD or don't like TDD, it is best to approach this from the client's perspective.
Don't add anything to your API where you have doubts if there will ever be a use case for. Chances are high noone will ever need that kind of function.
If you don't know enough about the use cases of the API to use this approach, you will probably do some more requirements analysis first - and that is something we cannot do for you.
Let me write something to your final edit, where you wrote
Casting based on the runtime type isn't necessarily an issue. It becomes only a problem when it makes extensions to the
Event
class hierarchy harder, because existing Client code would be forced to change with each extension.For example, let's say there is client code handling all chat events by a type test plus a cast for
ChatEvent
. If a new event type is added which is not a chat event, existing code will still work. If a new chat-like event is added, as a derivation ofChatEvent
, existing code will also still work as long as theChatEvent
type conforms to the LSP. For specific chat events, polymorphism can be used inside theChatEvent
part of the inheritance tree.So instead of avoiding type tests and casts superstitiously under all circumstances, because you have read in a text book "this is generally bad", reflect why and when this really causes any problems. And as I wrote above, writing some client code for some real use cases will help you to get a better understanding for this. This will allow you also to validate what will happen when your list of events get extended afterwards.