Re: Lahman, how ya doing?
- From: "H. S. Lahman" <h.lahman@xxxxxxxxxxx>
- Date: Sun, 08 May 2005 19:35:44 GMT
Responding to Hansen...
Hopefully it is a good book. B-) You might check out the Books section of my blog for some suggestions. Bruce Douglass has several books on the OOA/D-based use of event-based processing in R-T/E. One also has to be careful about books with 'UML' or a specific OOPL in the title; they are good at describing how to express your design in the UML or OOPL syntax but they tend to be short on advice about coming up with a good design in the first place.
Applying UML and Patterns, 2nd ed, by Craig Larman. The author has made clear in an early chapter that the book is about OOA, and what UML is introduced will serve that purpose.
I've read some of his other books but not that one, so I can't comment directly. His other books have been well-written and clear for their stated objectives.
I appreciate the help, though.
That's what these forums are for. Being retired after 40+ years in the business I have an oversupply of opinions and I need to get rid of them someplace.
And you're still paying attention to me. I get the feeling that the other readers of the forum see the thread now and then and think "Are they still going at it?"
Few people, as lurkers, follow the threads I get involved in because of the message lengths. It is a busy forum so it is tough to follow threads with long messages unless one has a vested interest in the specific thread.
Since I usually only get involved in threads with complex OOA/D issues, I think it is necessary to make lengthy explanations in that case for two reasons. One is that the OP usually hasn't a lot of formal OOA/D training or the issues wouldn't have come up in the first place, so there are gaps in the knowledge and it is hard to know where they are. Disconnects are common enough even when the bases are covered. The other is that OOA/D is subtle precisely because it is counterintuitive to the way software has been traditionally developed and the differences need to be underscored because most OPs developed software with traditional methods before moving to OO development.
I want to understand it. It looks like you're generating dumb events-- the event generator can't adapt to changes and it doesn't pass context-dependent information in them.
That's correct because I abstracted Timer to a very restricted task: generating scheduled events. The "smarts" of the simulation is in the intervals assigned, the ordering rules, the destinations, and the state machines. That deals with the requirements as I understood them: your processing was defined in terms of relative position in a "hard-wired" time stream. All of the computations and sequencing you have described so far can be accommodated by the way the events are registered with Timer.
So far I don't see anything that is context-dependent or needs adaptation. When we talked about things like hardware propagation delays before you didn't seem to think they were relevant (i.e., all the controller processing could be completed in a base clock tick interval (100 ms) in the real controller). You can simulate that easily by scaling real time (to accommodate your simulation computations) or simply invoking Timer.tick() when all the rest of the processing for a tick is done. So...
But speaking of maintainability, if it were to be used in a real-time system with Timer reading the system clock rather than incrementing a counter, could it easily accomodate that?
You'd get rid of simulated hardware, then. But some things like the controller logic and the chart recorders should be reuseable.
I think Timer would need to be revised, but as long as it spits out events with EventElement.tick_count set appropriately, maybe that's it.
Let's say the real controller has a problem on "witching" ticks where there is so much processing that it will overflow the tick interval and screw up the synchronization. The controller is going to have to deal with that explicitly somehow. That means the design will have certain priorities for what gets done in the tick interval and it will have to work around them.
In your case the hardware sampling is probably more critical than some of the data processing steps. Thus whether the computation of the average in Thermometer includes 60 samples, 59, or 61 may not matter as long as one has a sample count for the standard deviation. One possible solution to that problem is to assign a different event queue to the statistic computation and put it in a lower priority thread than the temperature sampling itself. Then it doesn't matter if it bleeds over the tick interval because it won't affect other processing that must get done within the tick interval. (There is no problem with synchronization to the next statistic computation because that is 50+ ticks in the future.)
However, that may introduce another problem. The filtering and other computations in the feedback loop that are done after the statistics may need be deferred as well. So you may not want to trigger them from Timer directly. Instead the computeStatistics action of Thermometer can generate that event when it is done. That will ensure the subsequent processing in the loop is synchronized to the actual samples rather than the Timer's ticks.
This is just a simple example and I have no idea whether it would work in your controller. But hopefully the basic idea is clear. Not all the events need to be generated by Timer; other synchronizations in a daisy-chain may be done simply on the basis of relative processing step. To figure out the best way of solving the problem one would have to identify all the dependencies through the feedback loop and separate those that were directly dependent on clock ticks from those that were only indirectly dependent on them.
A priority queue is an interesting idea. I suppose when that empties it goes to the normal queue. Some systems let your prioritize tasks on a scale of 0 to 7 or so, I wonder if they have eight queues.
Welcome to real-time. B-) I know R-T people dealing with hard R-T constraints who argued that WinCE was a unusable for R-T work because it only supported 32 priorities for threads.
However, I suspect in your case things are a whole lot simpler. For the hypothesized example, the event queues would be entirely separate object instances for the threads. Since computing the average is so rarely done one can be confident it will get done long before it should be triggered again. So Timer just enqueues the event on the right queue. The Timer events will get consumed (popped) immediately so everything effectively starts at the same time on the current tick. Hence there is no need to merge events back into a "normal" queue; the processing that was triggered just gets time-sliced based on priority.
But really, a lot of this just isn't much of a problem if timing information is passed to an object. E.g. in our real equipment we have some CAMAC counters that accumulate NIM pulses until they're stopped, read, and reset. And one channel is connected to a precision time standard. We're making a precision measurement, if the timing is off by 1/60 second that would be horrible. But for as long as the other channels count events from the charged particle detectors, the timing channel will be counting ticks from the time standard, and we can simply divide to find the counting rate to a precision that's limited by counting statistics but not by time resolution.
Unless there are gross timing errors that e.g. would cause the control system to go unstable or get too choppy to collect good data, I think just passing an elapsed time to the parts that need it is a simple and satisfactory solution.
Remember you already have a sort of priority in the order of event generation in Timer.
I assume that the CAMAC counters are pure hardware (i.e., the count is incremented in a hardware register.) At that point the simplest possible thing the driver would be to (1) write the stop register, (2) read the accumulator and save the value as an attribute in an existing object somewhere, and (3) write the reset register. That needs to happen /exactly/ on your 1/60th second tick no matter what else happens in the system. So I would put that <rather trivial> operation in the highest priority thread (i.e., put the relevant event queue manager in that thread).
That ensures that the count will be precise so long as the 1/60th event is consumed in a timely fashion. But you can get that for free from Timer by making that Timer event be the first one generated off the tick if it were a 1/60th tick.
The next problem is making sure the processing that needs that count get done accessing it before another 1/60th tick is processed. That may Just Work because the value in the attribute is quickly processed and converted to some other form or a history of values are stored until they are processed. I suspect one of these would be the case even if the rest of the feedback processing was pretty low priority.
However, if that isn't true there is a fallback position that is a variation on your proposal: pass the attribute value to subsequent processing rather than accessing it synchronously on an as-needed basis. This is a variation on the notion of "snapshot" data integrity. No matter how long the feedback loop processing takes, all you have to do is make sure that the first step extracts the value from the attribute and passes it to any subsequent processing via event data packets. Even at low priority that first step gets kicked off _on the same time tick_ so it should be able to access the attribute stored by the first event before the next tick no matter how low its priority is.
[In fact, you may have the opposite problem because the events are consumed in concurrent threads. That is, in theory the reader of the count attribute may read it before the CAMAC processing writes it. However, that is extremely unlikely so long as the CAMAC processing is kept as simple as above just because of the queue processing. And it would probably be easy to fix by putting the attribute access statement a couple of statements into the initial step's processing.]
I bring this up because it makes another subtle distinction. Passing the data to be processed is semantically different than passing timing information. By passing the data one is explicitly relying on the design strategy as-is rather than augmenting it. Thus the receiver just processes data as usual without needing to make decision about timing. (Hold this thought; it comes up again later in the message.)
The only situation that seems like a problem is if the total computation time on some "witching" ticks where the maximum number of events occur (e.g., a 1 tick sample, a 60 tick statistic, and a 7/60 adjustment all occur on the 420th tick) causes the total computation time to extend beyond the tick interval. That would screw up everything if one did all the processing. Is that what you had in mind?
Yep. In the real system, running on a ten year old computer, sometimes a scheduled time is missed. Whether it's because of witching ticks or delays when reading the GPIB or other, I'm not sure. But that happens on a small fraction of the total ticks, and the timing information is passed to the functions that need it.
OK. I described one general approach above, but in the end it depends on how the skip occurs and what significance it has.
Unrealistic as it may be, let's assume just for the sake of example that the statistics tick comes along but there are only 59 samples because one single tick event never got processed and we need exactly 60 samples in the statistic for consistency. Let's also assume that everything will be fine if you do two things: (1) don't invoke computeStatistics on the current tick and (2) do invoke computeStatistics on the next tick.
If that is all that needs to be done to keep synchronization, then your solution of having Timer defer the event generation to the next tick would work fine. There are lots of ways to do that, such has having Timer maintain an internal queue of deferred events (which could be a simple bitmap for events). You could also make Thermometer responsible for keeping track of how may samples it had since the last statistics
Thermometer might be a bad example in a real-time system because it would be replaced by physical components. The end result would be a voltage that is read periodically. But Controller, as a typical PID controller, has an integral and derivative term, and a low-pass filter, that all require a time interval. (And a proportional term that doesn't.)
I was referring to the software responsibilities in the Thermometer driver. The values need to be saved as they are sampled and periodically some basic statistics on those samples are done (as I thought you described it). That processing will be common to both the real controller and your simulation and both would be synchronized through Timer ticks.
computation. Then my code in Timer.tick for the event generation loop might look something like:
int eventTickCount = eventList[i].getTickCount(); bool isReady = eventList[i].getRecipient()->isReady(); if ((tick_count MOD eventTickCount) == 0) { if (isReady()) queueManager->push(&eventList[i]); else deferredBitmap |= (1 << i); // set deferral of event
Woah, what are you doing to deferredBitmap there? Looks kinky.
Again, my R-T/E background is showing. One gets used to bit-wise processing because hardware real estate is usually precious to the Hardware Guys and they cram multiple fields into a single register. So masking, shifting, and bitwise AND, OR, and XOR operations are a Way Of Life. A logical extension of that is that bitmaps become very natural to use. (I've used bitmaps that were megabytes in length to save memory.)
This code is just setting a bit for an event. The bit position in the bitmap is the index of the 1-bit boolean (true = set; false = reset or 0) for whether the event is deferred or not. I used the index of the event in eventList as the identity of the event in the bitmap. Alas, what isn't shown is an assumption in the declaration of deferredBitmap that the total number of events does not exceed the number of bits in the int or long associated with deferredBitmap. (If one needs more bits in the bitmap, then one needs macros for getting to the right bit and I was trying to keep it simple.)
[BTW, apropos of the point previously, I wasn't sure what you thought was kinky. So I addressed both the bitmap processing basics via indexed OR mask and the deferredBitmap variable assumption about bit count. That sort of coverage gets wordy too. B-)]
} else { if ((deferredBitmap &= (1 << i))) && isReady) { queueManager->push(&eventList[i]); deferredBitmap ^= (1 << i); // clear deferred event } }
[There is a drawback here because every receiver needs an isReady() method even if this notion of readiness is not relevant. (Worse, in C++ isReady needs to be a behavior of Object.) But I have a different point to make in the example, so ignore the details.]
I chose this implementation because it is logically almost identical to your solution where Thermometer decides when to do the computation. Indirectly Thermometer /is/ driving the decision here. There are, though, subtle differences.
Well, I didn't have Thermometer deciding when to do a calculation. Except in a manner of speaking when it was initialized with Timer. After that, Timer had a list of scheduling information which it updated and used to calculate elapsed times, which were included when events were sent. Thermometer would receive an event and read an elapsed time. There was no communication back to Timer, although I did have Timer itself track whether it had just sent an event there.
For Thermometer to register with Timer, it needs to include Timer's header file. That alone means Thermometer knows Timer. (One characterization of OOP dependency management is that dependencies -- knowing an object's specification -- should form a directed acyclic graph and one way that is manifested is that only the client needs the service's header file, not the other way around.)
In addition, Timer has to know what information to access from Thermometer. That's not a big deal per se because it is just a form of relationship navigation. The concern I have is that Timer must know how to extract that information specifically from each "block" (e.g., attribute names). The only way around that is to create a superclass for blocks and access it polymorphically. So far that seems easy but two things about it make me uncomfortable. One is the lack of commonality among the objects; the superclass is really artificial to make the coding convenient rather than being an identifiable problem space entity. In OOA one bends over backwards to abstract the problem space exclusively. The other is the possibility that the requirements affecting sequencing might change so that some objects need to provide unique information. That is admittedly unlikely in this case because the information needed is intrinsically pretty simple. However, the fact the the "blocks" are so different suggests that possibility.
Finally, I thought you indicated that the "block" had the responsibility to decide whether its processing should be skipped for the current tick. If so, then I think that is a substantial dependency since Thermometer necessarily needs to understand /how/ Timer does its scheduling just to make that decision.
It's not really important for anything I'm doing that a certain number of samples are collected, just so you know how many there are. And it's not so important that something is processed exactly at a particular time, just so long as you know what time it is processed.
Probably true. I think the main issue here is really good OOA/D technique, which is focused on long-term maintainability. There are lots of ways to formulate a correct solution. The OO paradigm limits the possible solutions compared to other techniques, but it doesn't eliminate methodological choices. One won't get OOA/D designs that are exactly the same from different OO methodologists. [What is the difference between a Methodologist and a Terrorist? One can negotiate with a Terrorist.]
One corollary of this separation of knowledge and behavior is manifested in two facts that are rarely mentioned in the OO literature but logically follows from encapsulation, separation of message and method, peer-to-peer collaboration, all the rest of that OO stuff. Behavior messages very rarely carry data in OO applications and behavior methods never return values in well-formed OO applications. The first is because knowledge access is synchronous so one accesses it directly from the source when it is needed. The second is because of the separation of message an method and encapsulation; returning a value that the caller uses would create an instant dependence on what the callee did.
I want to discuss that last paragraph a little more. The second point, that behavior messages never return values, is something I hadn't really thought of, but have been finding no reason to try to do.
If you meant you are finding no reason to have methods return values, then ignore this next bit. If you meant you are finding to no reason to try to not return value from behavior methods, then read on...
The reason is hierarchical dependencies. One can argue that the entire OO paradigm is directed at eliminating exactly these sorts of hierarchical implementation dependencies.
AClass::method1 (x)
tmp = x + 3
tmp = myBClass.doIt(tmp)
this.attr1 = tmp * 5This is a poorly formed OO method because it cannot be specified properly without also specifying exactly what BClass::doIt does. Nor can it be unit tested without having an implementation of BClass::doIt available. (One can stub return values in the test harness for a given 'x' test value, but that is just self-delusion; one is testing the test harness rather than the method.) In effect, AClass::method1 is a higher level node in a hierarchical functional decomposition tree that is coordinated lower level procedures. In such trees the the lower level procedures are literally extensions of the higher level procedure.
That's not a problem so long as the requirements don't change or one never invokes the higher level procedure in multiple contexts (i.e., it is called from exactly one parent higher level procedure). The reality, though, is that one reuses such higher level nodes because it is convenient to do so. That turns the tree into a lattice and one has the legendary Spaghetti Code. If one changes a lower level procedure to accommodate a change in requirements for one context of invocation of a higher level procedure, that change affects all contexts of invocation of that procedure. If the other contexts are not affected by the requirements change (i.e., they still want to do things the way the old requirements did them), then one has a problem and the entire tree has to be reformulated.
One resolves this the OO way by reorganizing the responsibilities:
ACLass::method1 (x)
tmp = x + 3
mtBCLass.doIt (tmp)AClass::method2 (x)
this.attr1 = x * 5Now BClass::doIt invokes AClass::method2. Now both of the AClass methods can be fully specified and tested without the presence of BClass::doIt. All one needs to demonstrate is that a message with the right value was sent to BClass::doIt (e.g., BClass::doIt was called with the right argument). What BClass does with that value is not the concern of Calais.
The daisy-chaining of the messages ensures that things get done in the right sequence (add 3, then modify result, then multiply result by 5). The requirements are still specified and validated the same way; they have just been allocated differently.
Note that this view of specification depends on several things methodologically: separation of message and method, peer-to-peer collaboration, responsibilities that represent intrinsic entity responsibilities, context-independence, implementation hiding, and a flexible view of logical indivisibility. IOW, all this good OO methodological stuff is designed to play together to eliminate hierarchical dependencies.
The first, though, that behavior messages very rarely carry data, that one accesses it directly from the source when it is needed, I need more on that. For starters, I thought behavior messages *do* carry data, or that they can. E.g. when a mouse down event is generated, you're going to want to know where it went down. But in the case we're discussing, I get the idea that if Controller needs to know an elapsed time it should ask Timer for the time, keep its own record of the previous time, and do its own calculation of elapsed time. Which is easily done. But then that requires that Controller knows that Timer exists, which I thought was something we're trying to avoid.
The basic problem being addressed is data integrity. In procedural development one of the Big Problems was global data. Global data still exists in OO applications for all practical purposes because there is almost always a relationship path between any two classes so any class can access knowledge in any other class. However, the OO paradigm provides mechanisms for managing access to that data.
The most prominent one is relationship instantiation and navigation. Relationships are crucially important to OO development because they severely limit what specific /objects/ one can reach via relationship navigation. In effect, relationship instantiation "hard-wires" participation in relationships so that one can't access a lot of the knowledge despite the paths apparent at the Class Model level. One simply can't get there from here. While enormously important to the OO paradigm, this aspect isn't too relevant here.
Another way that the OO paradigm addresses data, though, is timeliness. A big problem with global data is that it was being updated unexpectedly. One answer to that is the functional programming approach where one simply eliminates persistent state variables and /always/ passes data. Unfortunately that introduces another potential problem. Long chains of state passing may result in the data being out of date by the time it is actually processed. This is a serious problem in R-T/E systems where data is updated asynchronously by the hardware but it can be a problem anywhere.
The OO paradigm then decrees that one should access knowledge synchronously from whoever is responsible for it. That ensures that the data is up to date. The other side of that coin, though, is making sure that the data has been updated /before/ invoking the method that accesses it when that is necessary. The OO paradigm formalizes this through DbC. Recall that I mentioned that DbC can be used to rigorously determine where messages should be originated. The key insight there is that the precondition that must be satisfied is compound. In addition to simply the algorithmic step sequence (e.g., as in a use case), the condition includes the rules and policies for ensuring the data has been updated properly.
That paradigm does not work well if one passes data. To link data integrity to the precondition for executing the method that uses the data, the method must access the data directly from whoever is responsible for it. Otherwise the precondition would have to include whoever passed the data and in an asynchronous behavior model that gets very messy, especially if the data is passed methods through on a series of messages.
The exception is snapshots. Sometimes there are rules and policies in the problem space that place consistency constraints on multiple pieces of data or special constraints on integrity relative to particular behaviors. In that case it may be necessary to collect consistent data and use it even if it was been updated after its collection. That is sort of the problem we talked about above in thinking about the CAMAC samples and how they relate to subsequent processing. Special synchronization rules may actually /require/ that one copy knowledge or pass it in messages.
However, those are the exceptions rather than the rule. One does that only when one realizes that synchronous access may not work correctly because of other things that are going on in the solution. (One advantage of always trying to use synchronous access is that it is relative easy to recognize when it won't work because of the limited method scope and formulating DbC preconditions.)
Now, to your more specific comments. A mouse move message really doesn't count because it originates externally in the OS window manager code. It we have applied good OO application partitioning, it is a subsystem interface message and we deliberately design those interfaces to be pure data transfer interfaces. Such interfaces provide "firewall" decoupling between subsystems. So passing the position information is something we deliberately design in.
For the second part, I would try to find a way so that the object does not need to deal with a notion of elapsed time. That is inherently a scheduling issue and it would be bleeding cohesion from Timer for the object to deal with that. One way to do that in the overall context is the way I suggested above: pass the data itself. Now we are using the snapshot approach because the synchronization rules and our basic sequencing design may not provide timely data if we use synchronous access. Note that no /additional/ information or decisions are required; we just use the data passing mechanism rather than synchronous access to ensure DbC on data integrity is satisfied.
However, in any other situation where one could access consistent data synchronously, I would opt for asking Timer for any knowledge one needed.
[Caveat. This is the OOA view. When addressing nonfunctional requirements during OOP one may have to sacrifice pristine decoupling for practical optimization. Just as one might implement OOA state machines as direct method calls in a synchronous environment, one might need to avoid the context switches, etc. implied in direct access of data in the message sender by passing the data in the original event. IOW, the OOA solution addresses functional requirements while OOD/P address various nonfunctional requirements. Those are independent considerations and one may have to deliberately shoot oneself in the foot. The key is to get the functional requirements solution right in the OOA and then elaborate nonfunctional requirements on a case-by-case basis. Then the skeleton is at least right.]
The issue is not about providing the programmer options. There is only one way to correctly define the sequencing so the programmer doesn't have any options in that sense. The issue, as in all OO software, is how hard will it be to change the sequencing when the requirements change. Envision the various sorts of changes that might be made in the sequencing of Chart's responsibilities, especially relative to other object's responsibilities. I assert that for many such changes it will be a whole lot easier to do that by manipulating my statements than the implementations of Chart, Timer, and other objects. (In fact, I gave an example, as immediately below...)
In my implementation it would only be manipulating Chart, since by design Chart just internally takes on the task of doing what you did explicitly in main(). And if Chart's responsibilities change, you'd be mucking about with it anyway.
The problem is that proper sequencing is a broader issue than only one object. Consider:
Timer.add_event(&chart, E1, 1); Timer.add_event(&thermometer, E2, 10); Timer.add_event(&chart, E3, 20);
vs.
Timer.add_event(&chart, E1, 1); Timer.add_event(&chart, E3, 20); Timer.add_event(&thermometer, E2, 10);
In my solution this changes the ordering in which the E1, E2, and E3 responses are executed on the 20th tick, which could be significant to the results. How would you capture that change? I suspect you will have to change the implementations of at least two objects.
I can see that being used to give some basic prioritization, for an example.
I know you don't like my thought to issue update events to all the blocks at the end of an iteration, to move new output values into current values. But what you wrote above does suggest an easy way to provide for that.
Timer.add_event(&target, eEvolve1, 1); Timer.add_event(&controller, eEvolve1, 7); Timer.add_event(&target, eUpdate, 1); Timer.add_event(&controller, eUpdate, 7);
Instead of fiddling around with sorting the time evolution needs from the updating needs, just save all the update events for the end.
It is not so much that I don't like it as I am not convinced it is the best way, given alternatives. This gets into the issue of knowledge vs. behavior. Keeping a history of samples is a knowledge responsibility. I also happen to think it is important enough to the problem so that one should model is explicitly at a high level because it is crucial to synchronization, accuracy, or however one wants to view it.
If the history can be expressed purely in terms of knowledge (e.g., attributes for old vs. new values), then changing of that knowledge is a knowledge operation rather than a behavior operation. Since events are only useful in an asynchronous model of behavior communication, I wouldn't want to use events for that. [Doing so may also actually introduce synchronization issues (though unlikely here) because of the implied arbitrary delay between when an event is generated and when it is consumed in an asynchronous model.] Moving history values is obviously something that could be done just before they are modified. So I don't see a need for an event to trigger that; the move would be included in the action that did the modification.
That leaves making sure other objects access the right values. As I suggested, one can do that via relationships that are instantiated in a manner consistent with the "hard-wired" processing order. In fact, I would prefer that because it incorporates the synchronization rules (lead/follow :: old/new) as static structure. Whenever reasonable one should opt for static structure over dynamic behavior for enforcing problem space rules. It simplifies the application, usually improves performance, and improves reliability.
************* There is nothing wrong with me that could not be cured by a capful of Drano.
H. S. Lahman hsl@xxxxxxxxxxxxxxxxx Pathfinder Solutions -- Put MDA to Work http://www.pathfindermda.com blog: http://pathfinderpeople.blogs.com/hslahman (888)OOA-PATH
.
- Follow-Ups:
- Re: Lahman, how ya doing?
- From: Gregory L. Hansen
- Re: Lahman, how ya doing?
- References:
- Re: Lahman, how ya doing?
- From: H. S. Lahman
- Re: Lahman, how ya doing?
- From: Gregory L. Hansen
- Re: Lahman, how ya doing?
- From: H. S. Lahman
- Re: Lahman, how ya doing?
- From: Gregory L. Hansen
- Re: Lahman, how ya doing?
- Prev by Date: Re: Help! Difficulty understanding DB -> Object mapping
- Next by Date: Re: Lahman, how ya doing?
- Previous by thread: Re: Lahman, how ya doing?
- Next by thread: Re: Lahman, how ya doing?
- Index(es):
Relevant Pages
|