At the Logic Gates

How many times have you found yourself with a tangle of code that looks something like this?

/*
While the code is in ActionScript 3, it could easily 
be something you've seen in the likes of Java, C#, 
or almost any other similar language.
*/
 
package  {
	import flash.display.MovieClip;
	import flash.events.Event;
 
	/**
	 * ...
	 * @author The Horseman @ Scriptocalypse.com
	 */
	public class UtterMadness {
 
		private var bugsDance:Boolean = false;
		private var voiceOverCompleted:Boolean = false;
		private var fireworksExploded:Boolean = false
		private var bannerSign:MovieClip;
		private var youWinClip:MovieClip;
 
		public function UtterMadness() {
			this.playVoiceOver();
			this.makeBugsDance();
			this.shootFireworks();
			this.unfurlBannerSign();
		}
 
		private function onBugsDanceComplete(e:Event):void {
			bugsDance = true;
			if (bugsDance && fireworksExploded && voiceOverCompleted && bannerSign.currentFrame == bannerSign.totalFrames && youWinClip.currentFrame == 1) {
				youWinClip.gotoAndPlay("win");
				bugsDance = false;
				voiceOverCompleted = false;
				fireworksExploded = false;
				bannerSign.gotoAndPlay("goAway");				
			}
		}
 
		private function onVoiceOverComplete(e:Event):void {
			voiceOverCompleted = true;
			if (bugsDance && fireworksExploded && voiceOverCompleted && bannerSign.currentFrame == bannerSign.totalFrames && youWinClip.currentFrame == 1) {
				youWinClip.gotoAndPlay("win");
				bugsDance = false;
				voiceOverCompleted = false;
				fireworksExploded = false;
				bannerSign.gotoAndPlay("goAway");				
			}
		}
 
		private function onFireworksExploded(e:Event):void {
			fireworksExploded = true;
			if (bugsDance && fireworksExploded && voiceOverCompleted && bannerSign.currentFrame == bannerSign.totalFrames && youWinClip.currentFrame == 1) {
				youWinClip.gotoAndPlay("win");
				bugsDance = false;
				voiceOverCompleted = false;
				fireworksExploded = false;
				bannerSign.gotoAndPlay("goAway");				
			}
		}
 
		private function onBannerSignComplete(e:Event):void {
			if (bugsDance && fireworksExploded && voiceOverCompleted && bannerSign.currentFrame == bannerSign.totalFrames && youWinClip.currentFrame == 1) {
				youWinClip.gotoAndPlay("win");
				bugsDance = false;
				voiceOverCompleted = false;
				fireworksExploded = false;
				bannerSign.gotoAndPlay("goAway");				
			}
		}
 
	}
 
}

Sometimes, you need to make certain things happen as a result of some action or event in the program. In this hypothetical scenario, we have a MovieClip in the “youWinClip” variable that we’re clearly very interested in playing, but only if the following conditions are true:

  • Some Bugs on the screen have danced.
  • Some voice-over has completed playing.
  • Some fireworks have gone off.
  • The “bannerSign” MovieClip is on its final animation frame.
  • The “youWinClip” is not already playing (let’s assume that’s what was meant by currentFrame == 1

So our solution up above is fine and dandy for now, if a bit verbose and repetative. Let’s expand on this, though. What if you’re working for a client who decides that there need to be more conditionals? Like for example, somewhere in the middle of all this mess they want you to set up a listener for a boulder tumbling down the hillside, which may or may not happen before or after anything else. But before you can do any of this, they add a requirement that the “youWinClip” must only play every 3rd time that all the previous requirements have taken place.

So you add some more code to this class and it looks something like the following:

package  {
	import flash.display.MovieClip;
	import flash.events.Event;
 
	/**
	 * ...
	 * @author The Horseman @ Scriptocalypse.com
	 */
	public class UtterMadness {
 
		private var bugsDance:Boolean = false;
		private var voiceOverCompleted:Boolean = false;
		private var fireworksExploded:Boolean = false
		private var bannerSign:MovieClip;
		private var youWinClip:MovieClip;
		private var boulderRolled:Boolean;
		private var numberOfTimeConditionMet:int = 0;
		private var maxTimesToMeetCondition:int = 3;
 
 
		public function UtterMadness() {
 
		}
 
		private function onBugsDanceComplete(e:Event):void {
			bugsDance = true;
			if (bugsDance && fireworksExploded && voiceOverCompleted && 
				bannerSign.currentFrame == bannerSign.totalFrames && 
				youWinClip.currentFrame == 1 && boulderRolled) {
 
					numberOfTimeConditionMet++
					if(numberOfTimeConditionMet == maxTimesToMeetCondition){
						youWinClip.gotoAndPlay("win");
						bannerSign.gotoAndPlay("goAway");	
						numberOfTimeConditionMet = 0;
					}
					bugsDance = false;
					boulderRolled = false;
					voiceOverCompleted = false;
					fireworksExploded = false;
 
			}
		}
 
		private function onVoiceOverComplete(e:Event):void {
			voiceOverCompleted = true;
			if (bugsDance && fireworksExploded && voiceOverCompleted && 
				bannerSign.currentFrame == bannerSign.totalFrames && 
				youWinClip.currentFrame == 1 && boulderRolled) {
 
					numberOfTimeConditionMet++
					if(numberOfTimeConditionMet == maxTimesToMeetCondition){
						youWinClip.gotoAndPlay("win");
						bannerSign.gotoAndPlay("goAway");	
						numberOfTimeConditionMet = 0;
					}	
					bugsDance = false;
					boulderRolled = false;
					voiceOverCompleted = false;
					fireworksExploded = false;
 
			}
		}
 
		private function onFireworksExploded(e:Event):void {
			fireworksExploded = true;
			if (bugsDance && fireworksExploded && voiceOverCompleted && 
				bannerSign.currentFrame == bannerSign.totalFrames && 
				youWinClip.currentFrame == 1 && boulderRolled) {
 
					numberOfTimeConditionMet++
					if(numberOfTimeConditionMet == maxTimesToMeetCondition){
						youWinClip.gotoAndPlay("win");
						bannerSign.gotoAndPlay("goAway");	
						numberOfTimeConditionMet = 0;
					}	
					bugsDance = false;
					boulderRolled = false;
					voiceOverCompleted = false;
					fireworksExploded = false;
 
			}
		}
 
		private function onBannerSignComplete(e:Event):void {
			if (bugsDance && fireworksExploded && voiceOverCompleted && 
				bannerSign.currentFrame == bannerSign.totalFrames && 
				youWinClip.currentFrame == 1 && boulderRolled) {
 
					numberOfTimeConditionMet++
					if(numberOfTimeConditionMet == maxTimesToMeetCondition){
						youWinClip.gotoAndPlay("win");
						bannerSign.gotoAndPlay("goAway");	
						numberOfTimeConditionMet = 0;
					}		
 
					bugsDance = false;
					voiceOverCompleted = false;
					fireworksExploded = false;
 
			}
		}
 
		private function onBoulderRoll(e:Event):void {
			boulderRolled = true;
			if (bugsDance && fireworksExploded && voiceOverCompleted && 
				bannerSign.currentFrame == bannerSign.totalFrames && 
				youWinClip.currentFrame == 1 && boulderRolled) {
 
					numberOfTimeConditionMet++
					if(numberOfTimeConditionMet == maxTimesToMeetCondition){
						youWinClip.gotoAndPlay("win");
						bannerSign.gotoAndPlay("goAway");	
						numberOfTimeConditionMet = 0;
					}	
					bugsDance = false;
					boulderRolled = false;
					voiceOverCompleted = false;
					fireworksExploded = false;
 
			}
		}		
	}	
}

Wouldn’t you say this is getting ridiculous? All this copy/pasting is becoming as tedious for me to execute as it is for you to read. Additionally, the more often you have to repeat yourself the more likely you are to make a mistake in your code. Just think of what might happen if you forgot to re-set boulderRolled in onBannerSignComplete. Oh wait, we just did! Have fun debugging that one.

Did I tell you there were more changes on the way? Yeah, the client has some more requests. You’ll have to fit them into that hornet’s nest somewhere. Hope you don’t mind poring over your wall of text for the next week… or is it month? Or year?

Maybe if you just move the on-true results of those checks to its own function, that will make things easier?

 
		/////////////////////////////////////////////////////
		// starting near the end for the sake of sanity...
		/////////////////////////////////////////////////////
 
		private function onBoulderRoll(e:Event):void {
			boulderRolled = true;
			if (bugsDance && fireworksExploded && voiceOverCompleted && 
				bannerSign.currentFrame == bannerSign.totalFrames && 
				youWinClip.currentFrame == 1 && boulderRolled) {
 
					numberOfTimeConditionMet++
					if(numberOfTimeConditionMet == maxTimesToMeetCondition){
						// move all this code to its own function.
						this.youWin();
					}	
			}
		}
 
		private function youWin():void {
			boulderRolled = false;
			youWinClip.gotoAndPlay("win");
			bugsDance = false;
			voiceOverCompleted = false;
			fireworksExploded = false;
			bannerSign.gotoAndPlay("goAway");	
			numberOfTimeConditionMet = 0;
		}

That does in fact help to solve the issue of having to re-initialize our booleans and play our “goAway” and “win” animations, but it’s still a great deal of copy-pasting. Also, you have to remember to account for each new boolean statement in every function as the client adds them.

I’m sure the electrical engineers in the audience, as well as honest-to-goodness CompSci majors are rolling their eyes right now. They see something blindingly obvious that we’ve missed so far. We’re in desperate need of a logic gate. What if we were to put some of this Object Oriented Programming to good use and make ourselves a nice little module that we can plug into our code any time we need to test the truth of a logic statement?

The last time you’ll see the boolean comparisons : In your logic gate
Edit: This approach is admittedly slightly ham-fisted.

package  {
	import flash.display.MovieClip;
	import flash.events.Event;
 
	/**
	 * ...
	 * @author The Horseman @ Scriptocalypse.com
	 */
	public class WinCondition {
 
		private var bugsDanceEvent:Event;
		private var voiceOverCompletedEvent:Event;
		private var fireworksEvent:Event;
		private var boulderRolledEvent:Event;
		private var bannerEvent:Event;
 
		private var numberOfTimeConditionMet:int = 0;
		private var maxTimesToMeetCondition:int = 3;
 
		public function WinCondition() {
 
		}
 
 
		// call this to determine whether it's okay to proceed.
		public function permission(e:Event, winClip:MovieClip):Boolean {
			var allow:Boolean = false;
 
			// string references to our properties...
			var events:Array = ["bugsDanceEvent", "voiceOverCompletedEvent", "fireworksEvent", "boulderRolledEvent", "bannerEvent"];
			// the expected event types we will encounter, mapped to the property references...
			var types:Array = ["danceType", "VOType", "fireworksType", "boulderType", "bannerType"];
 
			// eg: e.type == "VOType" will return an index of 1...
			var index:int = types.indexOf(e.type);
 
			// events[1] == "voiceOverCompletedEvent"...
			// therefore we're assigning to this["voiceOverCompletedEvent"]
			if (index >= 0) this[events[index]] = e;
 
			if (bugsDanceEvent && voiceOverCompletedEvent && 
				fireworksEvent && boulderRolledEvent && 
				bannerEvent && 	winClip.currentFrame == 1) {
 
					numberOfTimeConditionMet++;
 
					bugsDanceEvent = null;
					voiceOverCompletedEvent = null;
					fireworksEvent = null;
					boulderRolledEvent = null;
					bannerEvent = null;
			}
 
			if (numberOfTimeConditionMet == maxTimesToMeetCondition) {
				allow = true;
				numberOfTimeConditionMet = 0;
			}
 
			return allow;
		}	
 
	}
 
}

Notice how this new class “WinCondition” has precisely one function that returns a boolean value. Instead of keeping all the permissions code in the UtterMadness class, we’re keeping it in one place that will be unobtrusive, and easy to maintain. We don’t have to look at, think about, or care about any of that noise unless we specifically need to. I don’t know about you, but if I were to have to look at, think about, and otherwise be distracted by all that mess every time I wander through the class file for UtterMadness, I’m pretty sure I’d need some horse tranquilizers to keep my blood pressure in check.

Here’s what UtterMadness looks like with about 80% less madness

package  {
	import flash.display.MovieClip;
	import flash.events.Event;
 
	/**
	 * ...
	 * @author The Horseman @ Scriptocalypse.com
	 */
	public class UtterMadness {
 
		private var bannerSign:MovieClip;
		private var youWinClip:MovieClip;
		private var _winCondition:WinCondition;
 
 
		public function UtterMadness() {
			_winCondition = new WinCondition();
		}
 
		// e.type == "danceType";
		private function onBugsDanceComplete(e:Event):void {
			if (_winCondition(e, youWinClip)) this.youWin();	
		}
 
		// e.type == "VOType"
		private function onVoiceOverComplete(e:Event):void {
			if (_winCondition(e, youWinClip)) this.youWin();	
		}
 
		// e.type == "fireworksType"
		private function onFireworksExploded(e:Event):void {
			if (_winCondition(e, youWinClip)) this.youWin();	
		}
 
		// e.type == "bannerType"
		private function onBannerSignComplete(e:Event):void {
			if (_winCondition(e, youWinClip)) this.youWin();	
		}
 
		// e.type == "boulderType"
		private function onBoulderRoll(e:Event):void {
			if (_winCondition(e, youWinClip)) this.youWin();	
		}
 
		private function youWin():void {
			youWinClip.gotoAndPlay("win");
			bannerSign.gotoAndPlay("goAway");
		}
 
	}
 
}

Notice that I kept the youWin function in the main UtterMadness class. While it’s possible to place it in the permissions class and not bother with the Boolean return value, I find that I’d rather give the logic gate no more responsibility than to return the truth of an input. This way, I never have to open the WinCondition class again unless I’m modifying the conditions. If I were to place the ‘youWin’ logic in the WinCondition class, I’d have a second reason to modify or edit the file. Given how capricious our theoretical client is, it’s likely we’d have to add or change the youWin behavior many more times.

Changing code logic without a good reason is to be avoided because every time you edit or change a class or add more responsibilities to it, you increase the likelihood of injecting an error. For my money, “because we need to change what happens when you win” is not a good reason to modify or change “how you determine when you win”.

Tags: , ,

  1. No comments yet.