2009 July 03
Object Oriented Design - Beyond "Hello World"
Introduction - Why this guide?
When people are trying their first real project in AS3, they could often use a more concrete tutorial about object oriented programming. In the last couple of weeks, I've seen many different people ask questions about the slideshows they are trying to program. Slideshows are a great example to explore object oriented design, since they CAN be accomplished in timeline based procedural code, but can also be wonderfully implemented in a very object oriented manner. I have created a slideshow framework which is designed to be extremely easy to extend. YAXS (Yet Another eXtensible Slideshow) is NOT a finished product that you can just plug in, configure and use. Rather, it is a set of classes that make it very easy to build exactly the slideshow you really want.
YAXS: The code
The code is available in a zip file here. The ASDocs are included, but for convenience, I have also put them online.
One of the guiding principles of object oriented design is encapsulation. This means that each class has well defined and closely related functionality. A Slideshow for instance only handles holding slides and user interaction. It lets other classes handle loading slides, transitioning from one slide to another, and determining which slide to show next.
Another important principle is abstraction. Abstraction is divorcing design from implementation. YAXS uses interfaces as abstractions so that particular implementations can be slotted in without having to change the code that uses them. For instance, ITransition defines a class which manipulates two slides to transition from one to another.
The design process
Many people advocate creating UML diagrams to determine what classes you need, map out use-cases, and define method signatures. I think this process is a bit paradoxical. It's regimented enough that someone new to object oriented programming can follow the process and get some results, but that same heavy process makes object oriented development seem cumbersome and unnecessary. Personally, I do not use a formal process. After a while, you get a pretty good instinct for the classes that will be necessary. And even when you get it a little wrong, correcting it is not a big deal. Let's sketch out the design with a top-down approach.
We want to make a slideshow. Let's assume we'll have a Slideshow class. A Slideshow needs to show a slide, and have methods to change slides (previous and next, maybe arbitrary index). That's about it. Now, we get a little more detailed and examine what it takes to do that.
We keep talking about doing stuff with slides, so Slide is a pretty good candidate for a class. It turns out that after building the whole thing, Slide could have just been DisplayObject, but I left it in as its own class in case of future functionality that required a particular type.
To show slides we need to get slides. Now, we could have Slideshow handle loading slides and managing its own collection, but I wanted to enable maximum flexibility. It seems likely that different slideshow implementations might want to get slides from different sources. When you see an opportunity to swap out different implementations, an interface is probably a good idea. In this case, ISlideProvider. ISlideProvider defines methods related to getting slides from an unspecified source. The particular implementations will fill in the details regarding that source. In the provided code, there is an ArraySlideProvider which is very simple and gets slides from an array. There is also XMLSlideProvider which parses an XML node and loads slides based on that. Even that does not handle loading the actual XML, allowing a configuration process/class to pass data into the XMLSlideProvider.
Once we have slides, we want to do stuff with them. We want to transition from one to another. As with ISlideProvider, the particular implementation of the transitions is likely to vary from project to project. ITransitionProvider defines a method to get a transition from one slide to another. That method must return an instance of yet another interface, ITransition. ITransition then defines a method to initiate that transition. Unfortunately, interfaces don't allow you to specify which events are dispatched when, but things that implement ITransition should dispatch an event when they are done. This event could be Event.COMPLETE, but it could be very convenient to get the slides involved from the event, so why not have a new Event type that includes that information.
There. Without even having really started actual coding, we've sketched out what we'll need to build. Now, start to fill in the details. At this point, you usually realize that something doesn't quite fit or that you need something else. That's fine. Try to keep the principles of encapsulation and abstraction in mind as you refine your design.
Extending
Here we come to the section I have not prepared before writing this tutorial. Let's extend the framework in a meaningful way. How about a CircularSlideProvider? This will be an ISlideProvider that never gets to the end, but instead wraps around to the other end. We'll start with ArraySlideProvider since it does most of what we want. Then we'll override a few methods.
/**
* always return true, since we're looping
* @return true
*/
public override function hasNextSlide():Boolean {
return true;
}
/**
* always return true, since we're looping
* @return true
*/
public override function hasPrevSlide():Boolean {
return true;
}
Now let's do a little actual work. We'll override the navigation functions to provide looped slides.
/**
* get Slide after current slide, and advances current pointer.
* @return Slide
*/
public override function nextSlide():Slide
{
current++;
if (current >= slides.length) {
current = 0;
}
return slides[current];
}
/**
* get Slide before current slide, and adjusts current pointer.
* @return Slide
*/
public override function prevSlide():Slide
{
current--;
if (current < 0) {
current = slides.length - 1;
}
return slides[current];
}
/**
* Get the slide at index i. This performs no bounds checking and may throw exceptions
* @param i:int
* @return Slide
*/
public override function getSlide(i:int):Slide
{
current = i % slides.length;
return slides[current];
}
Oops. The compiler is complaining that current is private in ArraySlideProvider. So change it to "protected" so subclasses can access it. All done. Swap in your new CircularSlideProvider and watch your infinite slideshow. I've included CircularSlideShow and the changes necessary for it in the download. Other exercises to try: RandomSlideProvider, FlickrSlideProvider, RandomTransitionProvider, PixelDissolveTransition. If you create one of these, please send it to me at steve@cosmodro.me
