Re: Text terminal rendering design



Responding to Guild...

My actual answer is at the end, but I have some concerns along the way to it...

My current design can be partially illustrated as follows:

0..* displays 1
[Terminal] ------------------>[Symbol]
+put() +ascii
+init() +unicode
+shutdown() +background
1 | +foreground
| uses symbols | 1
| from |
| |
0..*| |
v 0..* made by |
[SymbolFactory]<---------------------

The three major classes are Terminal, Symbol, and SymbolFactory. All three are abstract with many potential implementations.

The Terminal class is the objects responsible for the low level operations needed to present symbols to the user. It has a 2D array of Symbols and methods that allow a client to modify that array and display the array to the UI. In an actual implementation the Symbol objects might not be stored, just used to update a low-level operating system terminal using PDCurses or something similar.

Who is the client here and how is it modifying the array?

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.

If my assumption is correct, that sounds like a job for the Strategy pattern:

* 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.


The each Terminal has an associated SymbolFactory object. The SymbolFactory class has a variety of methods for creating and modifying Symbol objects. It produces Symbols of the specific subclass that the Terminal is expecting, performing conversion operations if necessary. For example, the SymbolFactory class has a setColor(Symbol, Color) method that returns a Symbol. The expected behaviour is to produce a symbol that looks the same as the given symbol but in the given color. If the given symbol is not the subclass of Symbol that this factory is expecting, the factory will perform a conversion before returning the requested symbol in the given color.

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.

The problem with returning a Symbol is the implied assumption that whoever invoked the factory also needs to collaborate with the Symbol. 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). So on general principles the factory should just do things like assigning pointer referential attributes without worrying about who will actually navigate those references during collaborations.

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 realize this doesn't seem to have much to do with your specific problem, but good solutions tend to fall out of getting a lot of small ducks lined up properly before one attacks the Big Problems.]

The Symbol class represents a symbol to be displayed. It has methods for converting the symbol to the closest ASCII and Unicode representations, as well as getting the foreground and background color of the symbol. This allows a SymbolFactory that is aware of ASCII or Unicode character sets to easily convert this symbol to the required format. The Symbol class also provides a method that reports the SymbolFactory that created it. This allows a factory or terminal to guess the subclass and nature of any given Symbol object if it was created by a known factory. For example, there is a singleton subclass of SymbolFactory called LineFactory for line drawing that only produces Symbols of the class LineSymbol and LineSymbol has methods that allow a Terminal to determine the geometry of the intended line and draw it, after testing that the factory of the given symbol is LineFactory.

If I understand this correctly, when SymbolFactory creates a new Symbol it may do so by converting an existing Symbol, right?

If so, then 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.

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.

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. So...


That is the abstract design. Now here is the specific case that is causing trouble:

[Terminal] [Symbol]
A A
| 0..* displays 1 |
[GraphicTerminal] ------------------> [GraphicSymbol]
| 0..* 1 | | 1
| | | | | | originates
| uses 1 0..* drawn by | | from
------------> [Drawer] <----------------- |
+draw(Symbol,x.y,w,h) | 0..*
v
[Symbol]

The idea is that not all terminals have to be implemented by an operating system text terminal; we can use operating system drawing commands instead to represent any arbitrary symbol. The possibilities are too numerous for any one implementation of GraphicTerminal to know how to draw every symbol, so it has a collection of Drawer objects that each know how to draw a subset of the symbols that the terminal may encounter. The factory assigns each symbol the appropriate Drawer object so that the Drawer knows how to draw the corresponding Symbol and then wraps the Symbol and Drawer together into a pair called a GraphicSymbol.

Quibble: isn't the multiplicity on the "uses" association * on the [Drawer] side?

This suggests that one needs to view display are a two-tiered process. In the higher level tier one needs to provide some set of data and access operations for different classes of symbols. The Strategy pattern I suggested about could provide that in the same way as your subclassing of [Symbol] does.

The second, lower level tier is related to different mechanics for rendering a given flavor of Symbol in the actual display. You applied subclassing to [Terminal] for than but I probably would have used a Strategy pattern for that as well.

Either way one is faced with a <semi-> combinatorial problem of relating subclasses from each generalization. If it is pretty close to combinatorial, then I think the Visitor pattern would probably be appropriate. Your introduction of the [Drawer] objects is a mechanism for reifying Visitor when the mapping is substantially less than combinatorial. So far, so good...


The best part of this design is that we can give a GraphicSymbol to a PDCurses terminal object and it will automatically translate the symbol to an appropriate ASCII code and display something useful. I can create terminal tools that look graphical when graphics are available and are fast and text-based when that is desired. So obviously I want to take this idea even further, but my wonderful abstract design suddenly fails me.

I'd like to create larger symbols that are spread over a rectangular area of the terminal corresponding to several ordinary symbols. I can do this by creating a subclass of GraphicSymbol that holds the overall symbol, as well as coordinates for the particular part of the symbol that each object represents. For example, if I wanted to draw a 4x4 'A' I would create four BigSymbol objects, (0,0), (0,1), (1,0), (1,1), each having a reference to the symbol 'A' and a Drawer that knows how to draw a quarter of a symbol. That's not the problem.

The problem is that the pixels of the graphical drawing area of the terminal is not necessarily divisible among the symbols that are to be drawn. For example, a screen might be 1024 pixels wide and we might want the terminal to be 80 symbols wide, giving us 12.8 pixels per symbol in width. The GraphicTerminal accommodates this by sometimes telling the Drawer to draw a symbol 12 pixels wide and sometimes telling it to draw 13 pixels wide. This is ordinarly not a problem for any of the participants, but in the case of BigSymbol it is a big problem.

I think you can resolve this by employing a variation on the Composite pattern something like the following:

[Drawer]
A
| R8
+------------------+--------------------+
| |
[BigDrawer] [AtomicDrawer]
+ scale() + draw()
| * | *
| |
| R3 | R6
| |
| walks | draws
| 1 1 R4 composed of * | 1
[BigSymbol] --------------------- [AtomicGraphicSymbol]
| + scaleFactor
| |
+-----------------+---------------------+
| R5
_
V
[GraphicSymbol]

The idea is that a BigSymbol is composed of regular (atomic) GraphicSymbols. A BigDrawer knows how to scale things for a given terminal. It does that by "walking" the regular graphic symbols that comprise the big symbol (R3 -> R4) using the available area provided by the terminal's graphic pane and assigning scaleFactor. (The default scaleFactor would be 1.) Once they are scaled, the BigDrawer "walks" R3 -> R4 -> R6 and invokes the appropriate AtomicDrawer for each element.

The R4 relationship is defined when the context arises for needing a larger symbol composed of smaller ones. The R6 relationship is instantiated the same way you assign your "drawn by" relationship. If there is a BigSymbol, then your "uses" relationship would be instantiated only for it, not the individual Drawers. That's to ensure nothing gets drawn before it is scaled. For GraphicSymbols that were not part of bigger symbols, your "uses" relationship would be instantiated as usual.

*************
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



.


Quantcast