OO Python Character Animation Design Study
RobertLechte (Talk | contribs) (New page: Design Maxims Used Law of Demeter I made the Law of Demeter a big focus in this final redesign. I've found this to be a particularly helpful guide in structuring my code. In pa...) |
RobertLechte (Talk | contribs) |
||
Line 2: | Line 2: | ||
− | + | === Design Maxims Used === | |
− | Design Maxims Used | + | |
[[Law of Demeter]] | [[Law of Demeter]] | ||
Line 46: | Line 45: | ||
=== OpenGL Abstraction === | === OpenGL Abstraction === | ||
− | At the low level, the program still relies on the functional OpenGL library calls. I saw little use for a Graphics object (eg graphics.draw_point(x,y) ), which would require additional learning effort from programmers with little resulting benefit. Instead, I focussed on abstracting the less relevant parts of the OpenGL library such that workload with | + | At the low level, the program still relies on the functional OpenGL library calls. I saw little use for a Graphics object (eg graphics.draw_point(x,y) ), which would require additional learning effort from programmers with little resulting benefit. Instead, I focussed on abstracting the less relevant parts of the OpenGL library such that the difficult details of the OpenGL system are largely hidden. The hope is to allow the developer to create a graphics application more easily by dividing the graphics creation workload into the implementation of a display method for each program object. |
+ | |||
+ | Complex graphics can then be built up from a number of smaller primitive objects. Also, because each object rendered is an actual program object, integration with object-oriented application logic is far more seamless. | ||
+ | |||
+ | ==== Boilerplate ==== | ||
+ | |||
+ | GlutMachine and GlutApplication provide an abstraction for the mandatory parts (the "Hello World" parts) of an OpenGL application. Dividing the boilerplate into two classes means that the largely fixed parts of the boilerplate (for instance, the call to glutMainLoop() ) are kept separate from the more configurable parts of the boilerplate (such as the glClear call, which can take various parameters. | ||
+ | |||
+ | The existing implementation of GlutApplication is quite straightforward, | ||
Line 60: | Line 67: | ||
In particular, there are no 3D primitive objects. This is partially because a 3-item list serves perfectly adequately, and also because the focus of this program involves splitting 3d points and angles into separate axes, regarding each axis largely separately. Thus a Point class (or similar) was not warranted. | In particular, there are no 3D primitive objects. This is partially because a 3-item list serves perfectly adequately, and also because the focus of this program involves splitting 3d points and angles into separate axes, regarding each axis largely separately. Thus a Point class (or similar) was not warranted. | ||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− |
Revision as of 12:02, 30 September 2008
Contents |
Design Maxims Used
I made the Law of Demeter a big focus in this final redesign. I've found this to be a particularly helpful guide in structuring my code.
In particular, following the Law of Demeter stops one from "cheating" with one's interface definition. In early designs, I created a nice tidy interface for a class, but would do all the dirty work associated with that class by grabbing an object "through" the interface and then manipulating it. Obviously, this is counter-productive, and result in unnecessary coupling. The apparently "clean" interface only results in more dirt elsewhere.
Following the Law does add to the size of your interface, and results in some cascading getter methods, but provides a more accurate picture of the realities of what one is exposing. It also encourages the programmer to expose only the exact data necessary, rather than whole objects. This is good for reducing coupling.
In earlier designs, the Window code would set up the size it required for each of its viewports. I refactored the code to be more in accordance with this maxim by giving each viewport a default size, and writing a rudimentary layout manager to change or override sizes if required.
This change made the code much more extensible. After this change I wrote a subclass of Window called QuizWindow, which runs a user evaluation program. I was able to change the program from a single figure view with graphs, to a double figure view with quiz instructions, very simply and intuitively.
I simply constructed two FigureViewport objects, wrote a small QuizMaster object to run the quiz, along with a QuizViewport to display instructions, and it Just Worked. Because I adopted the Tell don't Ask philosophy, I could just tell the program to make me two FigureViewports, rather than having to faff about specifying their sizes manually.
In this program, viewports have to work quite closely together. For instance, a GraphViewport needs to update every time the animation frame changes or the highlighted joint changes, so that it can update the Graph display appropriately.
In my initial design, the constructor for a FigureViewport had a parameter whereby one would pass in the GraphViewports that needed notifying. This seemed unclean as it resulted in unnecessary coupling, so I refactored the code such that arbitary observers could be attached to a Figure Viewport. Any object that registered as an observer of a FigureViewport merely needed to implement one method (effectively implementing an interface, although Python doesn't have explicitly defined interfaces).
The resulted in greater extensibility for the Quiz program modification. The quiz layout features no graphs, and thus there are no listeners for the Figure Viewports in this situation. Instead of having to worry about passing in a null parameter or similar, I was able to completely ignore the listener functionality, which felt much cleaner and simpler.
OpenGL Abstraction
At the low level, the program still relies on the functional OpenGL library calls. I saw little use for a Graphics object (eg graphics.draw_point(x,y) ), which would require additional learning effort from programmers with little resulting benefit. Instead, I focussed on abstracting the less relevant parts of the OpenGL library such that the difficult details of the OpenGL system are largely hidden. The hope is to allow the developer to create a graphics application more easily by dividing the graphics creation workload into the implementation of a display method for each program object.
Complex graphics can then be built up from a number of smaller primitive objects. Also, because each object rendered is an actual program object, integration with object-oriented application logic is far more seamless.
Boilerplate
GlutMachine and GlutApplication provide an abstraction for the mandatory parts (the "Hello World" parts) of an OpenGL application. Dividing the boilerplate into two classes means that the largely fixed parts of the boilerplate (for instance, the call to glutMainLoop() ) are kept separate from the more configurable parts of the boilerplate (such as the glClear call, which can take various parameters.
The existing implementation of GlutApplication is quite straightforward,
Accomodations for Python Language Style
Python has some rough edges for its OO support. Properties are simple to define (this syntax is PropertyName = property(getter_method_name, setter_method_name), and really help tidy up one's code, but due to a bug/flaw the getters and setters cannot be subclassed unless one explicitly redefines the property in the subclass. Hopefully this is fixed in a later version.
There was also a bug with default parameter values: If default parameters are defined both in a superclass and a subclass, then the superclasses default values take precedence. Possibly I am misunderstanding something but in any case, the values can fairly easily be set explicitly in the constructor whenever one runs into this problem.
Python is not a pure OO language, so I considered it overkill to make objects for absolutely everything. Python relies heavily on lists. I did not think it sensible to wrap a collection object around these lists, since they are a well-established and tested language concept. Hence I have retained lists (and occasionally tuples) as the primary means of storing collections.
In particular, there are no 3D primitive objects. This is partially because a 3-item list serves perfectly adequately, and also because the focus of this program involves splitting 3d points and angles into separate axes, regarding each axis largely separately. Thus a Point class (or similar) was not warranted.