Re: Text terminal rendering design
- From: "H. S. Lahman" <h.lahman@xxxxxxxxxxx>
- Date: Sun, 20 May 2007 20:59:26 GMT
Responding to Guild...
On the need for identity:
Since the client is external, the interface for your software
provides encapsulation and implementation decoupling. That
encapsulation hides the implementation of your software from
the client. So proper decoupling at the subsystem level /demands/
that the client should not know what objects implement your
software, much less where they are located in memory.
But how can the client ever know what objects implement my software? The client will inevitably have to call methods of my objects, but thanks to encapsulation that is the limit of the clients knowledge. The objects that the client has hold of could always easily be relaying those messages to a completely different object structure.
The client should NEVER be knowingly invoking methods of your UI objects. The client sends a message to your UI interface. Your UI interface then re-dispatches that message to some method in your UI. But the client should have no idea what method, if any, responds to the message.
<Hot Button>
Very bright people have devoted huge amounts of effort to providing enormously complex and cleverly designed interoperability infrastructures that allow remote object access. Alas, IMO that is wasted effort because it completely misses the point of why one does OO encapsulation in the first place. Remote object access trashes OO encapsulation by exposing the service implementation. In addition, it does so with an appalling penalty in performance overhead compared to a pure message interface.
[Caveat. I am talking about "direct" remote object access where the caller knows exactly what the object is. Interoperability infrastructures like CORBA provide decoupling through IDLs that allow one to generically specify What one wants done rather the Who should do it. That sort of infrastructure is very useful for polymorphic access in a distributed environment because it decouples the caller from whoever actually provides the service. Alas, that carries even more overhead than remote object access so one still needs to avoid it in favor of pure message interfaces until one really needs it.]
</Hot Button>
The interface provides two-way decoupling. The client can't know about objects in your subsystem, so it can't talk to them directly. Conversely, your UI service can't know about objects like Icon in the client so it can't talk to them.
Nonetheless there is necessarily a mapping between an Icon identity in the client and a Symbol identity in the UI service. Part of subsystem interface design is defining an identity scheme that both sides can live with. The most convenient and generic way people have come up with so far is an abstract identifier value. Once agreed upon, the client can interpret '14' as identifying the third Icon on the left while the UI can interpret it as the fourth Symbol from the bottom.
The corollary is that each subject matter must provide some sort of explicit mapping between '14' and its objects. There are lots of ways to do that, of which 2DArray is one possibility. That mapping can also be complex. For example, a single Account object in the "business" software might map into a Window object with associated Control objects in a GUI subsystem. In your case a single Icon object in the client may map into multiple Symbol objects in your UI software.
Bottom line: as soon as one provides message-based interfaces to decouple software units, one must come up with an identity mapping scheme across those interfaces.
On the utility of handles:
(If you pass back a handle, the semantics should be such that the
client doesn't know it is an object address, even if the client is
in the same address space.)
In what sense shouldn't the client know that it is an object address? It is always actually going to be an object address.
So is an OS Window Manger's window handle in an interface like Win32. But the OS Window Manager doesn't tell you how to use it, so it may as well be a brick. Without the class type the client can't do anything with it, even in the same address space. [The best you can do is cast it to an array address and see what happens after writing to the array. But that's another story from my misspent youth...]
Even if I use a symbol ID for the client and a Symbol internally, that symbol ID is going to be represented by an identifying object. In fact, it will be an object address simply because almost everything is represented by objects and objects are manipulated through object addresses. How do I tell the client to pretend this ID isn't an object address?
I've been looking at your article about Subsystem interfaces. You list these 5:
1. A message with no data other than a message type.
2. A message containing data passed by value.
3. A message with data passed by reference.
4. A message containing an object (e.g., a Java applet).
5. A message with an object passed by reference.
I have a feeling that you look at my giving references to Symbols to the client as a number 4 message. However, I look at it as a number 2 message.
That context here was about the degree of coupling in OO collaboration messages. I was mainly concerned with developers who blithely pass object references around applications in method calls and then wonder why maintenance keeps breaking stuff.
My issue here really isn't relevant because the client can't access the handle without the class type. In fact, the client really can't do anything semantically useful with the value itself. So I agree that passing the address as a handle to the client is just type 2 coupling, as you suggest.
Symbol is technically an object, but it acts purely as data in the interface that is given to the client. (It only has two methods, both doing nothing but translating the symbol to standard formats. The concept being the Symbol class is that the objects represent encoded symbols, which is a purely data concept.
In my current design Symbol objects for the graphics terminal contain references to Drawer objects, which are a display strategy object and certainly not data objects, but the Symbols still behave like data, especially to the client.
I know you will frown upon having a reference to a display strategy object within the object that it is to display, but in a graphics context a symbol is encoded imperatively, as a sequence of drawing operations, and that makes the code of the Drawer object itself part of the encoding of the symbol.
Sorry, but I don't know where you are going with this. The last two paragraphs seems to be about the implementation of the UI software, which doesn't seem to have anything to do with providing handles to the client.
To clarify, my problem is not with handles per se. As I mentioned, one really can't implement decoupling interfaces in asynchronous environments without them. My problem is with using them in this context. I suspect the client already has a reasonable way to identify Icons. Rather than having the client keep track of how your Symbol addresses map to Icon identity, I think it would be better for the client to just provide you with Icon identity and let you map that to Symbol identity internally.
Note that if one were to use handles, I think it would be better for the client to give your UI a handle to its Icon instance and let you map that as a unique identifier value to your Symbol addresses once you create the Symbols. That keeps the identity mapping problem in the UI without bothering the client with it.
[As a practical matter I don't think that is a good idea. Addresses tend to result in rather sparsely populated lookup tables. B-) Identity schemes tend to work better with closely grouped integers or something that will at least hash efficiently. The price of good decoupling subsystem interfaces is indirection and lookup overhead, so if one is going to choose an arbitrary identity mapping scheme, it should be efficient.]
As a practical matter one always requests a window from the OS
Window Manager exactly when one instantiates a Window object on the
GUI side.
However, that "practical matter" is actually driven by the way the
OS Window Manager's interface is designed. If one waits between instantiating the Window and making the OS Window Manager request,
there is a risk of getting out of synch and running into rude
referential integrity problems. So even though the window handle
is semantically quite abstract on the GUI side, the GUI-side
solution is being driven to some extent by it because of the
practical constraints of synchronization. IOW, the <service> tail
is wagging the <client> dog.
The influence of the OS Window Manager over the design is not quite clear. Surely you are not saying that the OS is forcing the GUI solution to have a 1:1 relationship between Window objects and OS handles, because Window objects could be designed to behave consistently even without an OS handle, though it would make the documentation more complex.
I am saying that the use of handles in the OS Window Manager interface is essentially forcing the software to instantiate a Window object in the same method scope as the Window Manager's create request is made.
The most obvious place to save the handle so that it maps unambiguously to a Window object is as an attribute of the Window object. But the OO Class Model has its own rules that must be satisfied, such as normalization. So you can't create a Window object with the windowHandle attribute uninitialized.
You can deliberately denormalize, but if you do you will have to provide executable code that will ensure that the denormalization doesn't get in trouble (i.e., to ensure nobody uses windowHandle until the Window Manager request is made). [The situation is analogous to instantiating unconditional relationships outside the scope where the participant is instantiated. One can do it, but it requires more work at the OOA/D level.]
FWIW, I think this is a tempest in a teapot. In the GUI example the bending of the application is not great because it is hard to imagine a sequence where one would not /want/ to instantiate the Window object and make the Window Manager create request at the same time. IOW, the two conditions I originally cited for using handles -- a 1:1 mapping and lockstep creation -- will quite naturally be satisfied.
I want to delegate data acquisition responsibilities because they
are really orthogonal to the terminal-based criteria that
determine what sort of [Symbol] it must instantiate.
But they are not orthogonal at all. Data acquisition depends heavily upon what data is needed, and different sorts of Symbols will always require different data. You cannot ever instantiate two distinct Symbol subclasses using the same data. And different data will always give you different Symbols, unless you perform some sort of conversion.
As I indicated in the other message, I think this is a disconnect about what I am delegating. What I am delegating is /accessing/ the same data from different places, not converting different data.
You have 3 ways to get the initialization data and N different
terminal contexts for which you instantiate a Symbol.
If those terminal contexts cannot share the same encoding scheme for Symbol objects then they will need different data to initialize the Symbols, though it might be a simple conversion. If they can share the same encoding scheme then they can share the same factory.
If you subclass [SymbolFactory] for all those combinations you have
3N subclasses. If you delegate data acquisition you reduce that to
N + 3 total subclasses.
Except that only works of the N factories could each use any of the 3 DataAcquisitionStrategies, but that is impossible. Unless the factory is doing some sort of conversion, the data it is given has to already be in the required form, and there is no way that all N factories are producing Symbols that store the data in the same form. There is no one universal symbol encoding, not even Unicode.
Even my two current target terminals require radically different symbol representation. No one DataAcquisitionStrategy would be acceptable for both a PDCurses symbol and a graphical symbol.
[That's not quite true because the attributes needed may be
different for different Symbols, which would lead to a
combinatorial number of strategies for the data acquisition. I am
assuming there are only M Symbol types with different attributes
where M < N. IOW, I am assuming the same symbol type can be eaten
by more than one terminal type.]
Despite what I said above, your assumption is correct. However, when two terminals actually share the same symbol type (ASCII is a popular one), then they can share the same factory as well as the same strategy.
The more I think about this problem, the more I lean towards the idea of a pipeline that schematically looks something like:
{Data}
|
|
V
[Transform1]
|
|
V
{Symbol}
|
|
V
[Transform2]
|
|
V
{Symbol}
|
|
V
[Transform3]
|
|
V
{Symbol}
|
|
V
[Terminal]
Each Transform process handles a different aspect of converting raw data into a format acceptable to a particular flavor of Terminal. So far I think I have detected the following kinds of transformation that might be necessary:
value conversions. These are things like ASCII/unicode or binary color codes. The scope is individual values. The Symbol itself is modified or a clone is produced.
syntactic mismatches. Color is provided as a single code but the terminal wants separate R/B/G values. The scope is multiple values. A new Symbol with different attributes is produced.
augmentation. The terminal needs more attributes so defaults must be supplied. A new Symbol with more attributes is produced.
I have no idea whether this list is exhaustive for your context, but it is good enough for example.
How many Transform stages there are will depend upon how different the terminal-ready Symbol needs to be compared to the raw input data. Note that reading from the DB or the client supplying message data would be a transform1 conversion.
The pipeline idea is attractive to me for a couple of reasons. After converting the raw data into some generic Symbol, all the transformations are symbol-to-symbol. The pipeline structure then allows one to easily skip transforms if they aren't relevant in getting to a particular terminal format. It also allows standardized mechanisms for doing thing like instantiating relationships.
What this does is reify the *:* mapping between Terminal and Symbol into a *:* mapping between Symbols. I am guessing you currently have more Symbol subclasses than you have Terminal subclasses, which would not be good. However, I think there is a more subtle form of pruning going on that may work out well.
To see that, assume we need all three transformations for a particular Terminal type. The number of Symbols that can be output from Transform3 will be limited to those suitable for direct consumption by Terminals. More important, none of the Symbols that could be inputs to Transform2 could be inputs to Transform3 because they haven't been through Transform2 yet. Nor can the the Transform3 inputs include any of the Transform3 outputs. So at each stage the possible inputs and outputs that we must map are substantially limited.
But once we isolate particular categories of transformation, I think we can do much better that this by taking advantage of invariants and parametric polymorphism. As an example, let's look at an augmentation transform.
We have N attributes in the source Symbol whose values we copy as-is. We have and additional M attributes in the output Symbol for which we must supply defaults. The default values can be specified in an external configuration file. To keep things simple, let's assume we are using a scripting-based OOPL that allows direct manipulation of the namespaces.
We can do this conversion in a single method for any input object with N attributes to any output object with N+M attributes. All we need to know are N, M, and the names of the attributes in each Symbol. That can also be provided in external configuration data. For example,
* R1 1
[AugmentationTransform] ------------------ [TransformSpec]
+ transform() + inputObjectClass
+ outputObjectClass
+ copyAttributes
+ newAttributes
+ copyCount
+ newCount
The copyAttributes data is basically a set of attribute name pairs for attributes that are to be copied from the input Symbol to the output Symbol. The newAttributes are essentially A-V pairs with new attribute name and default values. The TransformSpec objects are dumb data holders that can be initialized from external configuration data. The R1 relationship is instantiated based on pairing type attributes in the two Symbols. (That pairing can also be defined in external configuration data.)
For the transform() method we essentially have the following pseudocode:
transform()
// cast reference to input Symbol to the specific subclass
// inputObjectClass
// create new Symbol with outputObjectClass constructor
for i = 1 to copyCount
// copy old value to new for pair copyAttributes[i]
for i = i to newCount
// set attribute in new for A-V pair newAttributes[i]
If your OOPL doesn't support dynamic name substitution, things get more tedious with switch statements and whatnot. But it will still be a whole lot better than writing a gazillion methods.
I suspect that once the individual transformation types are isolated similar techniques could be used on all of them to reduce the number of combinations. (I didn't even think about the other transformations; augmentation was my first pick for an example.) The key is that once one starts to categorize the kinds of transformations needed, invariants that can be parameterized will tend to pop out. [The category in Invariants and Parametric Polymorphism in my blog may be helpful for the right mindset.]
*************
There is nothing wrong with me that could
not be cured by a capful of Drano.
H. S. Lahman
hsl@xxxxxxxxxxxxxxxxx
Pathfinder Solutions
http://www.pathfindermda.com
blog: http://pathfinderpeople.blogs.com/hslahman
"Model-Based Translation: The Next Step in Agile Development". Email
info@xxxxxxxxxxxxxxxxx for your copy.
Pathfinder is hiring: http://www.pathfindermda.com/about_us/careers_pos3.php.
(888)OOA-PATH
.
- Follow-Ups:
- Re: Text terminal rendering design
- From: Brendan Guild
- Re: Text terminal rendering design
- References:
- Text terminal rendering design
- From: Brendan Guild
- Re: Text terminal rendering design
- From: H. S. Lahman
- Re: Text terminal rendering design
- From: Brendan Guild
- Re: Text terminal rendering design
- From: H. S. Lahman
- Re: Text terminal rendering design
- From: Brendan Guild
- Re: Text terminal rendering design
- From: H. S. Lahman
- Re: Text terminal rendering design
- From: Brendan Guild
- Re: Text terminal rendering design
- From: H. S. Lahman
- Re: Text terminal rendering design
- From: Brendan Guild
- Text terminal rendering design
- Prev by Date: Re: Text terminal rendering design
- Next by Date: Re: Is OO anti-Math? (Re: Whose Fish?)
- Previous by thread: Re: Text terminal rendering design
- Next by thread: Re: Text terminal rendering design
- Index(es):