Re: Text terminal rendering design
- From: Brendan Guild <dont@xxxxxxx>
- Date: Sat, 19 May 2007 02:26:29 GMT
H. S. Lahman wrote:
Responding to Guild...
Your client software is external so it can't possibly know about
Symbol references.
That seems rather strong. The client software knows about Symbol
references if I pass Symbol references to it. How is that impossible?
Besides, it is evidently creating new ones or requesting existing
ones to be loaded from a database. To do that there must be some
sort of identifier to indicate which one the client wants. That
identifier also needs to map 1:1 to whatever the database uses for
an identifier.
It is true that in some cases we need an identifier value for a symbol
in addition to the actual Symbol object, but that is only seems to be
only an implementation detail of one special case of how symbols can be
acquired. If we are not getting the symbols from a database then we
might not need identifiers. A symbol could be specified by
construction, such as by overlapping existing symbols.
In effect, 2DArray is a mapping table between the identifier the
client understands and the object in memory, whose identity for
relationship navigation is its address on the heap. (Obviously it
would be convenient if the identifiers were consecutive integers.)
I had thought that the concept of the 2DArray was fairly clear, but now
I am very doubtful. You mentioned that the 2DArray was actually four-
dimensional in an earlier post and I had to struggle to find some
meaning in that. I must apologize for failing to understand your advice
in this case.
Are you proposing that 2DArray represent a set of 4-tuples
(id,x,y,Symbol), where each such tuple means that a coordinates (x,y)
on the screen there is a symbol represented to the client by id and
additionally represented by Symbol?
Wouldn't it be better to let 2DArray be just an actual 2DArray and then
have the mapping table be a separate data structure? In fact, I still
wonder why we need to bother with symbol IDs at all when we have
references to Symbol objects. I know you have tried to explain that to
me and I will continue to try to understand, but it seems so wasteful
and redundant.
Your only alternative is to provide the address to the client as
a handle when a symbol is instantiated that the client uses in all
subsequent communications.
This is what I am doing in my current design. The SymbolFactory returns
a reference to the constructed Symbol to the client and the client then
uses that as a handle to instantiate the relationship between that
Symbol and the Terminal as necessary.
That just moves the problem of associating memory addresses with
symbol identifiers back into the client.
That seem clear enough, though I hadn't thought of it as a problem. I
expect that the client will often need to work with Symbols as abstract
atomic values. Usually when a client object A is put into a
relationship with a symbol, it will not be expected to perform any sort
of analysis of the symbol; the relationship only exists so that the
symbol will be used as a UI element to stand for something that the
object A needs to represent to the user.
For that sort of thing, an address handle works just as well as any
other sort of symbol identifier.
That will get messy if the it takes awhile to instantiate a Symbol
or the client communications are asynchronous.
Can you explain in more detail how that makes it messy? I have an idea,
but I would like to confirm it.
The client has some client class Icon of objects that are in a 1:*
relationship with a symbol, i.e., Icon objects must always have an
associated symbol. If that symbol is represented according the the
client's view of the symbol, it is immediately available when each Icon
is being constructed. However, if the client uses an address handle it
must wait for the SymbolFactory to finish instantiation and that time
could leave an Icon in an inconsistent state where an asynchronous
process could access it.
On the other hand, the client could delay instantiating the Icon until
the Symbol had been instantiated.
* R1 gets data 1
[SymbolFactory] ------------------ [DataAcquistionStrategy]
+ createSymbol() + getInitializationData()
+ dataElement1
+ dataElement2
...
A
| R2
+--------------------+----------------+
| | |
[FromClientRequest] [FromOtherSymbol] [FromDB]
Now for all situations createSymbol() just looks like:
navigate R1
invoke constructor (dataElement1, ...)
Then I don't understand why we even have SymbolFactory if its job is so
trivial. Why does DataAcquisitionStrategy bother breaking up the data
into elements and offering each element individually instead of
directly constructing the Symbol and offering that? Clearly
DataAcquistionStrategy knows the nature of the symbol encoding it is
acquiring data for, if that data is going to be useful.
Unless SymbolFactory does something more than construct a wrapper for
the data in DataAcquisitionStrategy, isn't SymbolFactory just a useless
middleman?
You talk about factoring out accessing and initialization from
SymbolFactory into DataAcquistionStrategy. From my perspective I have
already done that in my current design: I've taken out all the data
manipulation and initialization from SymbolFactory and put it into
DataAcquistionStrategy, but then I noticed that SymbolFactory now has
no responsibilities at all, so I eliminate it entirely. Then, of
course, I changed the awkward name of DataAcquisitionStrategy to
SymbolFactory in honor of its origin.
I am not sure what you mean by 'identify' here. A 2D Array of
symbols is innate in any terminal implementation and it does not
need the client to identify it. For example, it will usually not
be reasonable to allow the client to switch back and forth
between several 2D Arrays.
But as I understood it the external client is requesting a
specific set of symbols for display.
You understood correctly, I just wasn't clear about your use of the
word 'identify'. I see now.
I am aware that the various means of creating symbols have
equally various implementations, but that is not enough
explanation for why the factory might be better as several
objects rather than one. In the current design a factory is
nothing but a stateless collection of methods that presented to
the client as means of building symbols for later rendering.
Splitting the one object into several means delivering that
collection of to the client methods as a collection of objects
rather than one object.
In a phrase: to avoid god objects. As another phrase:
divide-and-conquer. In more PC OO terms: to ensure high cohesion.
I assert that if behaviors are broken up and spread over multiple
objects, it will be easier later to connect the dots of flow of
control with messages.
I want to be clear about this. Are you advising me that my
SymbolFactory should not be an instance of the GoF Abstract Factory
pattern? The Abstract Factory pattern is about an interface for
creating families of related objects by providing various methods
within a single factory object.
The example for Abstract Factory has 'CreateScrollBar' and
'CreateWindow', two very different methods doing very different things
but within the same object because they are connected in that they
producing widgets for the same look-and-feel. In the same way the
various activities of my SymbolFactory are strongly connected because
they must produce symbols in the same encoding scheme.
I am arguing that creating a symbol in memory is about invoking a
constructor with a set of attribute values. That's all.
That is perfectly true, given those attribute values.
Getting the initialization data from the client, the database, or
as a conversion from another Symbol are different activities with
different business rules and policies. In this case those rules
and policies seem to be pretty crucial to the problem in hand. I
would like to see those isolated and encapsulated so one can deal
with each context independently. While getting the right
initialization data is the factory's responsibility, when that
becomes complicated it is time for delegation.
But I do not see how that makes it any less complicated. Surely the
factory will be less complicated because we are moving the complicated
part to another object, but the complicated part is still just as
complicated in its new home.
For example, to create a new symbol by converting an old one you
need the old Symbol in memory. So it needs to be created by a
SymbolFactory, just like the new Symbol. That's a three step
process: create old, convert old attributes, and create new.
Having the SymbolFactory for the new Symbol own the creation of
the old Symbol seems unnecessarily complicated -- especially if
the new and old Symbol only differ in a particular value of the
same character code (i.e., one invokes exactly the same
constructor with a different value).
Fortunately, my current design has each SymbolFactory only responsible
for creating Symbols in one particular encoding, so it certainly
wouldn't be responsible for creating the old Symbol. The old Symbol
would be given to the factory as input. The old Symbol must have been
created by some different factory.
When the client requests creation of a Symbol from the DB,
we initialize R1 to acquire the data from the DB and we invoke
DataAcquistionStrategy to get it. Then we invoke SymbolFactory to
create a Symbol using that data. Now we have a "native" Symbol in
memory. At that point somebody can check the terminal type and
realize a new "terminal" Symbol must created from it. So that
somebody instantiates R1 again for the conversion flavor and
repeats the process.
I am not sure what you are suggesting this as an alternative to. My
current design does almost exactly this, but instead of having
DataAcquisitionStrategy get data, it gets a Symbol object and we skip
the SymbolFactory step. The rest is entirely the same, including
allowing someone or anyone to check the terminal type and invoke a
converter.
Why is this better? Because it doesn't matter who makes the
decision about converting the symbol.
So I do not understand your meaning here when you are trying to explain
why it is better.
The code in SymbolFactory and DataAcquistionStrategy is exactly the
same not matter how many times the requirements change for who
needs to make that decision (or the criteria for how it is made).
I notice that you often talk about "SymbolFactory and
DataAcquisitionStrategy" together. That alone suggests that they are
rather tightly tied together.
They are tied together because DataAcquisitionStrategy needs to know
what data the corresponding factory needs. If you find a new source of
symbol data you not only need a new DataAcquisitionStrategy, but also
likely a new SymbolFactory to construct a new Symbol subclass to store
the new form of data that has been acquired.
Similarly, if you want a new encoding for symbols you need a new
subclass of Symbol and therefore a new subclass of SymbolFactory, and
so long as SymbolFactory is forbidden from doing anything interesting,
you need an entirely new strategy class to take the place of
DataAcquisitionStrategy and supply the correct data for the encoding.
To me these things seem so highly coupled that I doubt that they really
are separate concerns.
That also gets back to my assertion about the flow of control
being easier. Because the behaviors are isolated/separated AND one
has captured business rules in relationships, one can connect the
flow of control dots of the overall solution any way one wants
after objects have all been defined.
It is asking for a large amount, but it would be very helpful if you
could give a small example of how that could be used, especially
something that could not be done if SymbolFactory and
DataAcquisitionStrategy were not separate objects.
[Terminal]
| 1
| displayed for Client on
|
| R1
|
| *
[Symbol]
The R1 relationship contains only those Symbols that the external
Client has indicated it wants displayed. The collection class for
that * relationship becomes 2DArray when the Client also specifies
the position in the display for each of those desired elements.
That seems to be how things are in my current design.
A Symbol is only added to the R1 collection when it is created
However, that is certainly wrong. A Symbol is only added to the R1
collection when it needs to be displayed according to the client's UI
needs. Therefore, a Symbol can be created at any time before it needs
to be displayed and it can be added to the R1 collection many and
various times after being created.
All [SymbolFactory] needs to know is that there is a unconditional
relationship between Terminal and /every/ Symbol that it
instantiates for the Client. Presumably we don't get any create
requests for Symbols that are not to be displayed.
It is safe to assume that we don't get any create requests for Symbols
that are not ever to be displayed, but there is nothing inconsistent or
harmful about creating a symbol significantly in advance of displaying
it.
This could be important because the process of creating a symbol is far
longer and more complicated than the process of displaying it. Creating
can require database access and conversion procedures. Displaying is
expected to take a vanishingly small amount of time so as not to be
noticable to the user.
So to answer your question, there are lots of ways to instantiate
relationships outside of factories. But those are related to
dynamic contexts. When an object has unconditional relationships,
at least one participant must be provided to ensure referential
integrity and, in doing so, it is best to do it in a single
method's scope.
I could be wrong, but it seems things have gotten confused somehow. The
relationship between the terminal and a symbol has always been
described as being highly dynamic, hasn't it? Symbols appear and
disappear and move around on the screen in complex patters again and
again over the course of an application. Surely it is a perfect example
of a dynamic relationship.
I have discovered my mistake. I keep flailing around with my UML
diagrams. My first one had this:
0..* displays 1
[Terminal] ------------------>[Symbol]
Which is obviously horribly wrong, since a terminal will display more
than one symbol. Unfortunately, I tried to correct it by turning it
into the following in my next post:
1 R1 displays *
[Terminal]--------------------[Symbol]
Which is a lot closer to correct, but now I see that it is subtly
wrong. In fact it is equally ridiculous because it implies that every
symbol is continuously displayed. It should have been:
0..1 R1 displays *
[Terminal]----------------------[Symbol]
To allow for Symbols that are not displayed. If that is the source of
our misunderstanding then I strongly apologize.
The notion of drawing something is related to a display subject
matter. That is quite different than the intrinsic properties
associated with the notion of 'symbol'. A symbol exists in a problem
space quite independently of whether it is displayed on a computer.
Displaying things is a responsibility of objects that abstract the
display paradigm itself.
That is why my current design makes Terminal responsible for drawing.
You seemed to be suggesting that was bad.
Unfortunately, this necessarily requires that Terminal know the
subclass of each Symbol object that it is given, but I can certainly
see no way around that. At some point someone is going to have to cast
from Symbol to a subclass, no matter who is given the responsibility of
drawing. (Unless it is the Symbol itself, but you've made it clear that
is bad for other reasons.)
You originally attributed rendering of a symbol as a
responsibility of Terminal. If so, I would expect the drawSymbol
behavior to be there.
Then I am out of ideas for what you could have meant by this (from a
earlier post):
I am arguing that Terminal should not need to know what flavor of
Symbol it is accessing. It just navigates a relationship
1 R1 *
[Terminal] --------------- [Symbol]
to whatever is on the other end of R1. It is somebody else's job to
make sure the right flavor of [Symbol] is there (i.e., has the
responsibilities and implementations needed for the collaboration).
If you expect Terminal to draw Symbol, how can it get away without
knowing even the flavor of Symbol?
OTOH, if there is a fixed suite of things to draw, the idea of a
DisplayStrategy or RenderingStrategy owning a generic draw() behavior
is quite appealing.
That does sound appealing, but I cannot imagine how it could work. What
sort of fixed suite are you talking about? I think the problem may be
that the drawing process is too simple for a strategy pattern. After
all the conversion work has been done to carefully arrange for a symbol
in just the form that Terminal wants it, there is really only one way
to draw that symbol for any given Terminal.
This gets back to my opening issue in this message around
PDCurses. I could envision something like:
* R1 utilizes 1
[Terminal] -------------------- [DisplayStrategy]
+ draw()
A
| R2
+----------------+---...
| |
[PDCurses] [GUIIcon]
I cannot envision that. I think from that perspective my Terminal is my
DisplayStrategy. Terminal also has init() and shutdown(), but those are
not independent of drawing. PDCurses requires a very different init()
and shutdown() than a graphics library, so it would be meaningless and
inconsistent to plug a PDCurses display strategy into a Terminal with
the wrong init() or shutdown() methods. In fact, it is the nature of
Terminal in my current design that the PDCurses subclass has a
completely different implementation from the graphics subclass with no
parts interchangeable.
The graphics subclass does have its Drawer class, which is a strategy,
but that is only for that specific subclass of Terminal, not for
Terminals in general. In that case it is very useful because there is
no clear right way to draw a symbol using the graphics library.
PDCurses is providing all the infrastructure for a /full/ solution.
The problem is that the solution is only appropriate for part of your
problem. The techniques you need to manage /your/ solution extend
PDCurses in ways it was not designed to support.
I am not exactly trying to wrap my solution around PDCurses. I am
trying to create a general text terminal subsystem which has the
flexibility to be used on many different physical terminals. I am doing
this by supporting multiple symbol encodings and encoding conversions
and I am creating a subclass of Terminal for each type of physical
terminal I might encounter.
PDCurses only comes into it because if my design is at all effective
then I should be able to implement a Terminal subclass and associated
classes to make my subsystem smoothly work with PDCurses. After all,
PDCurses is a library designed to work with text terminals, so the job
should be trivial.
.
- Follow-Ups:
- Re: Text terminal rendering design
- From: H. S. Lahman
- 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
- Text terminal rendering design
- Prev by Date: Re: Is OO anti-Math? (Re: Whose Fish?)
- Next by Date: Re: Some theory
- Previous by thread: Re: Text terminal rendering design
- Next by thread: Re: Text terminal rendering design
- Index(es):
Relevant Pages
|