This will be in C++11, but the object design should be rather agnostic.
I have 3 interface classes, IEvent
, ICondition
, IRunnable
.
The main loop processes multiple kinds of IEvent
, ranging from "an Application has launched" to "User clicked position (x,y)". Then for each type of event, a list of ICondition
are notified through a listener interface. For example, ConditionUserClickedPos
implements IEventClickListener::onClick(int x, int y)
, which is called by the main loop.
If the position is within the conditions, one or more IRunnable
objects are called through IRunnable::execute(IEvent triggeringEvent)
, like class RunnableDrawCircleAtClickPosition
and class RunnableShowPrettyWindow
.
My problem is as follow :
Within IRunnable::execute(IEvent triggeringEvent)
implementation, I need to get the precise information of the event, but I don't know the exact class hierarchy of the triggeringEvent.
This can be easily solved with dynamic_cast, checking typeid or other reflective oop methods, but those usually breed rather smelly code. I'm also trying to keep the code extendable for new and unrelated events, conditions and runnables that I don't know about yet.
I could also declare a bunch of overloads for ::execute(...)
based on children class types, but this doesn't seem right either.
Is there a silver bullet that I've missed here?
1 Answer 1
My suggestion is below written in PHP pseudocode. So, we have many types of events, all implementing IAbstractEvent interface. When we need to process an event, we give this event to a factory method FactoryConstructor::constructFactory()
. This method creates abstract factory AbstractLoopProcessorFactory
descendant for us for processing events of this particular type.
When we have an event which name is DrawCircle
, factory method creates DrawCircleLoopProcessorFactory
for us. This factory can create concrete DrawCirlceConditionProcessor
and DrawCirlceRunnable
that can work with IDrawCircleEvent
event.
IDrawCircleEvent interface may have an arbitrary number of methods and arbitrary structure. Only it should extend IAbstractEvent
to allow our abstract factory to work. So, in DrawCirlceConditionProcessor you already know, that actual event passed to you is IDrawCircleEvent
and you can reference it as is.
A type cast is probably required because you have to be able to work with an event queue uniformly. But you trade it for logic separation.
interface IAbstractEvent
{
public function getName();
}
interface IDrawCircleEvent extends IAbstractEvent
{
}
interface IAbstractConditionProcessor
{
/**
* @return bool
*/
public function check(IAbstractEvent $event);
}
interface IDrawCircleConditionProcessor extends IAbstractConditionProcessor
{
}
interface IAbstractRunnable
{
/**
* @param IAbstractEvent $event
*
* @return void
*/
public function process(IAbstractEvent $event);
}
interface IDrawCircleRunnable extends IAbstractRunnable
{
}
abstract class AbstractLoopProcessorFactory
{
/**
* @param IAbstractEvent $event
*
* @return IAbstractConditionProcessor
*/
abstract function getConditionProcessor(IAbstractEvent $event);
/**
* @param IAbstractEvent $event
*
* @return IAbstractRunnable
*/
abstract function getRunnable(IAbstractEvent $event);
}
class DrawCircleLoopProcessorFactory extends AbstractLoopProcessorFactory
{
public function getConditionProcessor(IAbstractEvent $event)
{
return new DrawCirlceConditionProcessor($event);
}
public function getRunnable(IAbstractEvent $event)
{
return new DrawCirlceRunnable($event);
}
}
class FactoryConstructor
{
/**
* @param IAbstractEvent $event
*
* @return AbstractLoopProcessorFactory
* @throws Exception
*/
public static function constructFactory(IAbstractEvent $event)
{
switch ($event->getName()) {
case "DrawCircle":
return new DrawCircleLoopProcessorFactory();
default:
throw new Exception("Unknown event name {$event->getName()}");
}
}
}
class Application
{
public static function main()
{
/**
* @var IAbstractEvent[]
*/
$eventList = [];
foreach ($eventList as $event) {
$factory = FactoryConstructor::constructFactory($event);
$conditionChecker = $factory->getConditionProcessor();
if ($conditionChecker->check($event)) {
$runnable = $factory->getRunnable($event);
$runnable->process($event);
}
}
}
}
-
So the idea is to centralize the reflection in an abstract factory, then the concrete factories will know enough about the types they handle to avoid further manual casting. This is certainly better than having each event handle their own dynamic casting. I'll see how this can work for me.Eric– Eric2015年06月02日 22:23:12 +00:00Commented Jun 2, 2015 at 22:23
-
@Eric Yep. After you make decision, tell me the results if possible, please.Vladislav Rastrusny– Vladislav Rastrusny2015年06月03日 07:38:11 +00:00Commented Jun 3, 2015 at 7:38
-
I ran into some problems with this approach. In my design, the conditions depends on the events and not the other way around. The events should have no knowledge of what may happen when they are raised. Maybe my formulation of the problem wasn't clear enough, or I don't understand your implementation correctly. The constructFactory() call implies that the event know about "DrawCircle" before conditions are checked, while I was trying to have the "DrawCircle" decision taken based on the conditions.Eric– Eric2015年06月04日 03:52:56 +00:00Commented Jun 4, 2015 at 3:52
-
Also I am not sure that it truly solve my problem. In process(), how can I get the event (x,y) position from IAbstractEvent? How can a different runnable get "Application name which just launched" from IAbstractEvent as well? This will most probably involve some dynamic casting and runtime type checking.Eric– Eric2015年06月04日 03:55:50 +00:00Commented Jun 4, 2015 at 3:55
-
@Eric I will post some clarifications to my approach tonight.Vladislav Rastrusny– Vladislav Rastrusny2015年06月04日 07:20:22 +00:00Commented Jun 4, 2015 at 7:20
if(event.type === ...)
is much more palatable than astd::dynamic_cast
, at least for me.