Re: Text terminal rendering design
- From: Brendan Guild <dont@xxxxxxx>
- Date: Wed, 16 May 2007 22:51:09 GMT
Here is a more accurate representation of my current design, corrected
for errors I realized while reading H. S. Lahman's post.
1 R9 displays *
[Terminal]--------------------[Symbol]
+put() +ascii
+init() +unicode
+shutdown() +background
* | +foreground
| | *
R10 | | makes
| uses symbols |
| from |
1 | 1 made by R11 |
[SymbolFactory]---------------------+
H. S. Lahman wrote in
X%H2i.23729$b67.22235@trnddc06:">news:X%H2i.23729$b67.22235@trnddc06:
Who is the client here and how is it modifying the array?
The client is an application with need of a UI that works by
displaying a 2D array of symbols to the user. The client knows
what symbols it needs to display and the coordinates at which each
symbol should be displayed.
The client calls methods of a SymbolFactory object to create a Symbol
and then it calls a method to write the symbol to the Terminal's array
at some coordinates. That method is as simple as put(x,y,Symbol).
If it is just assigning a method to a Symbol, then I would be
inclined to manage that separately from the other responsibilities
[Terminal] seems to have. That's because [Terminal] already seems
to have a lot to do. In particular, that assignment seems to
depend strongly on which Symbol one has in hand.
In the case of most text terminals, a symbol is represented by a single
integer with sufficient bits to hold all the information needed to
display it. PDCurses represents symbols in this way, and in my current
design a PDCurses-based Terminal can expect Symbol objects which
directly contain that integer, so the Terminal has almost nothing to do
but pass that integer on to PDCurses.
* displays R1 1
[Symbol] -------------------- [DisplayStrategy]
+ displaySymbol
A
| R2
+---------+-----...
| |
... ...
IOW, whoever the client is that is making the assignments for your
2D array would instantiate R1 for the right flavor of display
strategy. The [Terminal]'s job would be simplified to just sending
a message to the strategy at the end of the relationship path when
it is time to display a symbol.
That makes sense as a design, but it seems to have the drawback of
forcing the client to be aware of the mechanism by which the symbol is
being displayed. If I want to change from using PDCurses to a graphical
library then the strategy for each Symbol would have to be changed
accordingly.
I want the client to be above such implementation details, so it cannot
be the client that instantiates this R1 relationship between the symbol
and the strategy as you suggest.
On the other hand, I believe my Drawer class fits the strategy pattern
as you intend above. In that case the relationship is instantiated by
the SymbolFactory, and the factory is supplied by the Terminal, which
is the ultimate origin for implementation details. Because the
SymbolFactory is aware of the implementation details it knows when we
need a strategy and when we just need an int.
The SymbolFactory seems reasonable but I have a problem with it
returning a Symbol object. The key characteristic of this problem
seems to be that there are a whole flock of complex dynamic
relationships. In that kind of situation it is usually best to
focus factories on instantiating relationships, such as my R1
above. Let it create a ConvertedSymbol or whatever and instantiate
relationships between it an other objects (e.g., Terminal) that
are used later in collaborations.
You mean to have SymbolFactory methods that take coordinates as well as
symbol details so that when the symbol is created it will be directly
and automatically given to the corresponding Terminal for display. The
main reason that I have no such method in SymbolFactory is that the
methods of SymbolFactory are expected to be relatively slow,
complicated operations involving object construction and data
conversion.
The SymbolFactory interface is designed to take symbol details in many
forms, hopefully including whatever form the client has available to
supply. The role of the SymbolFactory is then to perform potentially
complex conversion operations to put the symbol into a form that the
Terminal is expecting so that the Terminal can quickly and easy display
the symbol.
So the desired behaviour of the client is to call a factory method to
create a Symbol once and then instantiate and destroy the Terminal's
relationship with that Symbol as many times as needed. Would you
suggest that the SymbolFactory use memoization to mimick that
efficiency?
The problem with returning a Symbol is the implied assumption that
whoever invoked the factory also needs to collaborate with the
Symbol.
You are correct that the client is not expected to collaborate with the
Symbol. In my current design the client is only responsible for storing
the Symbols that it wants to use and then giving them to the Terminal
when each symbol needs to be displayed.
Invoking a factory is primarily about what to create and
when to create it while collaborating with an object is about what
to do with it and when to do it in the overall solution flow of
control. Those quite often represent different business rules and
policies so it might be necessary to separate them (either now or
during later maintenance).
The actual display of the symbols requires some sort of concrete
implementation, which I have called Terminal. In order to display a
symbol every implementation needs two clearly distinct pieces of
information: The appearance of the symbol and the position on the
screen.
The client has to supply both pieces of information. The position on
the screen can be safely and universally represented as a pair of ints,
so I just demand the client supply them as method arguments. However, I
very much do not want the client to know the specific format of the
appearance of the symbol, so I give it a factory that it can use to
request that symbols be constructed. This allows the client to work
with the symbol's appearance as a black box, even organizing it into
data structures or passing it around, all without needing to know the
format that the implementation uses.
The client is even given methods to examine the factory and ask it
questions about what sort of symbols it supports. For example, some
SymbolFactory objects may be able to create Symbols that contain a
representation of a color, while others will always produce colorless
symbols. This allows the client adjust the symbols that it requests so
that the UI will be meaningful in black & white.
So while the SymbolFactory seems to fit into the GoF Abstract Factory
pattern, it is perhaps not what you would expect. Each SymbolFactory
object represents an encoding scheme for the appearance of a symbol.
I am also not keen of SymbolFactory modifying existing Symbols. If
the Symbol already exists and one needs to do something like
changing its color, then that should be done directly through a
Symbol setter rather than through a middleman like SymbolFactory.
Let the factory encapsulate only rules and policies for
instantiation and leave dynamic context to collaborations.
I see your point. Perhaps my reason for allowing SymbolFactory to
modify as well as create is not a good one, but I'll explain it even
so.
Certain encodings are more suited to certain symbols than other
symbols, and for any encoding there are always symbols that cannot be
represented at all. However, one can almost always find a best match in
the encoding you are using for the symbol you want to represent. For
example, there might be a client that feels it needs to represent
symbols in unicode, but unfortunately the only terminal available on a
certain platform is ASCII.
In order to operate in such a situation, I added a method to
SymbolFactory called adapt(Symbol). All it does is construct a Symbol
in the appropriate encoding by finding a best match for the given
symbol. Once I had that, it was obvious that I would want a version of
that method for constructing the appropriate encoding but with a given
color.
If I understand this correctly, when SymbolFactory creates a new
Symbol it may do so by converting an existing Symbol, right?
Exactly.
I am not keen on Symbol owning the conversion operations. The
conversion is how SymbolFactory creates a new Symbol, so it is
logical for it to understand the conversion rules. Conversely, it
doesn't seem reasonable that a given Symbol should know how to
convert to some other Symbol. If you need different conversion
implementations, then subclass SymbolFactory and instantiate a
relationship to the right source Symbol.
Symbol doesn't contain conversion operations for converting to
arbitrary other formats, only two standard formats: ASCII and Unicode.
I chose those two this because ASCII is what I expect to be using and
Unicode is general enough to represent almost any symbol. The reason I
felt that every Symbol had to have standard conversion methods was so
that every SymbolFactory object would always be able to construct an
appropriate Symbol, even if it is given a Symbol using an encoding that
had not been invented when the SymbolFactory was created.
So I can create a new subclass of SymbolFactory with my own special
representation scheme for symbols that no one but I knows, and then I
can give the Symbols that I create to the factory for any terminal and
it will make a good effort to display what I intended, by first
converting my symbols to ASCII or Unicode and then converting again to
whatever the terminal needs.
I am also not keen on Symbol keeping track of what factory created
it. If the client needs to to process different flavors of Symbol
differently, then it needs some other mechanism for doing the
right thing than trying to determine its subclass, however
indirectly that may be.
That is probably the biggest weakness of my design.
However, in this case I think the problem is easily resolved by
the application of Strategy I suggested above. When a Symbol is
instantiated, SymbolFactory can instantiate the R2 relationship
because if already needs to know the particular flavor of Symbol
to create it. Then one always gets the right display package when
it is time to display.
That does seem to be a good idea for display, but as things are the
Terminal always knows the specific subclass of Symbol that it will be
dealing with because it can only deal with one. By having the Symbol
store the SymbolFactory that created it, Terminal can ensure that the
Symbol is using the right encoding and if not it can use the factory to
automatically convert before it starts the actual display procedure.
The real problem is in SymbolFactory which has a strong habit of
determining a specific subclass out of a list of options. It could
always convert to ASCII or Unicode without a dynamic cast, but often it
can create a better encoding by using special knowledge about other
encoding schemes. The most obvious example is that the factory might be
given a symbol that is already in the target encoding, in which case
converting to ASCII and then back might be disastrous.
Quibble: isn't the multiplicity on the "uses" association * on the
[Drawer] side?
It seems that my UML is a bit rusty. In all the diagrams I gave, I put
the multiplicity on the wrong ends of the associations. The diagram
should have been like this:
1 represents
[Terminal] [Symbol]---------+
R3 A R5 A | R6
| 1 R4 displays * | * }
[GraphicTerminal]-------------------[GraphicSymbol]----+
| 1 * |
| draws|
| R7 | R8
| uses * 1 contains |
+-------------[Drawer]----------------+
+draw(Symbol,x.y,w,h)
Each Drawer is associated with exactly 1 GraphicTerminal, but a
GraphicTerminal may use many Drawers. I hope that didn't cause too much
confusion.
I will need time to ponder the implications of the rest of your advice.
.
- 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
- Text terminal rendering design
- Prev by Date: Re: Evaluating Models (Re: Whose Fish?)
- Next by Date: Do you need a Casandra -T-))
- Previous by thread: Re: Text terminal rendering design
- Next by thread: Re: Text terminal rendering design
- Index(es):
Relevant Pages
|