It's not about the architecture of triggering events and event handlers. It's really about how to make this system accessible by humans.
Amen.
Since you need build the app to support ongoing adaptability / expandability, the first thing to do is think about how to optimize, from the perspective of the people who will be doing this, the task of adding / modifying app behaviors.
That means working out a conceptual model for controlling the app that is easy to explain and understand, and then working out a set of cookbook steps that involve the minimum possible amount of input from the person doing the task -- i.e. if the task is structured so that any single piece of information or logic needs to be supplied in more than once place by a human, you probably haven't got the right design yet.
That said, I like BrowserUK's take on the matter, which seems to go in the same direction as tmoertel's notion: capture the relations as data rather than as blocks / objects / classes of code. That will generally mean less code, and less work for the humans who handle the "behavior modifications"; those are both good things.
| [reply] |
It's actually even more complicated than
that.
Still, I wouldn't change the approach: Create an external
specification that is tailored to capture your application's
complicated relationships in a way that is intuitive to humans; then,
translate the specification into the desired code, either at run time
or during a pre-processing step.
There are (about 20) different widget properties...,
and some aren't actually widget properties, but are based on widget
properties.) Every widget will have a value for every
property.
This is important information that will help us design our specification.
The handlers trigger based on the values for the
widget properties, ....
This, too, is good stuff to know. (Maybe you could tell us
what you're really doing?)
So, I really need to have a way to ask a given handler
if it needs to fire based on a given widget and the values for all the
properties.
Not necessarily. You might, for example, be better served by having
an object of specialized type whose responsibility it is to manage
widget property–to–handler relationships. Then you would
ask it what handlers ought to be fired, not the handlers
themselves. But that's a decision to be made later. For now,
let's just figure out how to specify the relationships.
If I'm reading your new constraints correctly, the relationship
we're concerned with is that between widget properties and handlers,
not widgets and handlers. So, let's start out with a simple matrix
that enumerates all of the possible points of intersection:
Property Handler
1 2 3 4 ...
======== ===========
A a b
B
C c
...
This looks much like what I suggested before, but now we have a new
interpretation. Each a or b in the matrix
represents a set of conditions that govern whether the associated
handler (given by column) may fire for the current widget based on its
current value of the associated property (given by row). In other
words, we can interpret the column under Handler 3 as saying, Handler
3 will fire for the current widget if the current widget's A property
satisfies condition b and its C property satisfies condition
c. Our rule, for now, is that all associated conditions
must be satisfied in order to fire the handler.
If that's all the more complicated your relationships are, then
we're done. But maybe the all-or-none rule is too limiting.
Maybe Handler 3 ought to fire if b(A) or if
c(C) is satisfied. Then, a simple matrix will not suffice;
we'll need a way of specifying expressions:
Handler Firing condition
======= ================
1 a(A)
2 ...
3 b(A) || c(C)
...
If we filled out this two-column table for all of your handlers, we
would have a lot of redundancy because, in your original question, you
said that, "there's only really about 4-5 different types of events,
just with different parameters." So, let's allow the handlers and
conditions to be parameterized (and we'll give the handlers names,
while we're at it):
Handler Firing condition
======= ===================
Page("Jim") a(A)
Email("foo-alert@...") b(A) || c(C, "foo")
Email("bar-alert@...") b(A) || c(C, "bar")
At this point, we have to be careful that our specification doesn't
devolve into a bunch of if-statement
equivalents. But I'm going to stop here because I'm just guessing
about what you really need and don't know which refinement to make
next. I don't have enough information to know whether it would be
better, for example, to factor out redundancy in our specification or
to leave it in. This is where knowing about what you're really doing
would be helpful.
Depending on your needs, reasonable refinements might include
factoring out common subexpressions, adding environmental context, or
adding a related specification table. Each involves trading one kind
of complexity for another, and whether it makes sense for you depends
on the costs of managing different kinds of complexity in your
application and programming culture. Only you know enough to make
these decisions at this point.
Cheers, Tom
| [reply] [d/l] [select] |