Nick Brettel's eight puzzle design

From CSSEMediaWiki
(Difference between revisions)
Jump to: navigation, search
Line 84: Line 84:
  
 
So what is this ''addName()'' method?  I needed some way to set a participants name.  However, to set a pair's names, we would need two names, as opposed to the one required to set an individual's name.  But if ''IndividualRole'' and ''PairRole'' have different ''setName()'' methods (with a different number of arguments), then a ''Participant'' could only ''setName()'' if it knew whether its role is training or testing.  A ''Participant'' shouldn't need to know the types of its roles, just that it has roles, and what their interface is.  Simply put, I want to [[Program to the interface not the implementation]].  To do this, a uniform way to set name(s) is required, even though we don't know how many names.  I choose to do this by using a common interface: an ''addName()'' method sets one name, and it can just be called a couple of times to set a couple of names (for pairs).  I noticed later that this was a similar idea to the [[Builder pattern]] (I don't think I can go so far as to say this is an application of the pattern) - we add parts (names) until we have built a product, and then we normally get the product (in my case, we change the product by adding parts, so there is no need to get it - we already had it).  An alternative approach would have been to pass in a Collection of names.  However, I decided this was not as good, as the number of items in the collection would be very small (and a majority of the time participants are individual), so there would be a lot of unnecessary Collections, and they do seem like overkill.
 
So what is this ''addName()'' method?  I needed some way to set a participants name.  However, to set a pair's names, we would need two names, as opposed to the one required to set an individual's name.  But if ''IndividualRole'' and ''PairRole'' have different ''setName()'' methods (with a different number of arguments), then a ''Participant'' could only ''setName()'' if it knew whether its role is training or testing.  A ''Participant'' shouldn't need to know the types of its roles, just that it has roles, and what their interface is.  Simply put, I want to [[Program to the interface not the implementation]].  To do this, a uniform way to set name(s) is required, even though we don't know how many names.  I choose to do this by using a common interface: an ''addName()'' method sets one name, and it can just be called a couple of times to set a couple of names (for pairs).  I noticed later that this was a similar idea to the [[Builder pattern]] (I don't think I can go so far as to say this is an application of the pattern) - we add parts (names) until we have built a product, and then we normally get the product (in my case, we change the product by adding parts, so there is no need to get it - we already had it).  An alternative approach would have been to pass in a Collection of names.  However, I decided this was not as good, as the number of items in the collection would be very small (and a majority of the time participants are individual), so there would be a lot of unnecessary Collections, and they do seem like overkill.
 +
 +
=== Slide Show ===
 +
 +
The experiment consists of pages of instruction, and the task to perform.  Displaying these instructions, and the task itself, is analagous to a slide show.  We have a number of slides (or "cards", thinking like Java's ''CardLayout'') and for each, we show it, then move on.  A key abstraction that was identified in the refactoring process, was that all panels that are displayed to the user abide by the same interface.  Namely, all my panels follow a simple process - they show a card, until they are done.  I decided a ''Card'' abstract class was necessary that captured this common behaviour.
 +
 +
The entire experiment application is controlled by the ''ExperimentGUI''.  This is also the main ''JFrame'' that contains any ''JPanel'' that is displayed.  It keeps track of all the instruction ''Card''s that can be displayed, plus the ''EightPuzzlePanel'' card that contains the gui by which someone solves the puzzle.
 +
 +
[[image:card.gif]]
 +
 +
As you can see above, by providing a ''Card'' abstraction, the coupling is less tight - the ''ExperimentGUI'' depends on the abstraction rather than the specific panels.
 +
The abstract ''Card'' also gives us a uniform way to tell the ''ExperimentGUI'' to move to the next slide.  The ''Card'' stores the ''ExperimentGUI'' and uses it in the (concrete) ''done()'' method.  This method just calls the ''panelDone()'' method in ''ExperimentGUI''.
 +
 +
What the instruction ''Card''s are themselves is not very important.  ''ExplanationPanel'' is just a generic panel, with a text box and a next button, for displaying any textual instructions to the user.  The abstract ''StartPanel'' and ''StartTestingPanel'' classes are also shown.  These, much as the name suggests, are panels for instructions when the experiment begins, and just before testing, respectively.  We'll look at ''StartPanel'' a bit more later on.
 +
 +
To create a new ''Card'', all that is needed is to provide implementations for the abstract method ''show()'' (for any behaviour when the ''Card'' is first displayed) and call the ''done()'' method when finished.
 +
 +
 +
==== Dependency Inversion Principle, applied ====
 +
 +
How I ended up with this design deserves some explanation.  You can see what I had prior to refactoring at [[#Old Design]].  I was concerned with the three two-way dependencies - each panel was tightly coupled to the ''ExperimentGUI''.  Firstly I considered using the [[Observer pattern]].  I said at the time:
 +
  ''...I could make each "card" observable, to decrease the coupling between ExperimentGUI and a card. This doesn't fit the ObserverPattern that well: there is only one object doing the observing, and many being observed. However, it would get rid of an owner reference. Is it really worth it when there's only one Observer, ''ExperimentGUI''? I think so. [[Observer pattern]] may make things a bit more complicated, but that's better than having the unnecesary owner reference: a card doesn't need to know its owner, only how to say "I'm done".''
 +
 +
Then I realised, there had to be a better way.  Strike one.
 +
 +
My next thoughts were that I could use the [[Mediator pattern]].  I thought ''the interactions between the GUI and the Panels is well-defined (e.g. GUI says "show this panel", a panel says "I'm done"). But then, is the State really a state of the gui? Or a state of the Mediator? ''  Upon further examination, I realised that the [[Mediator pattern]] also didn't really fit.  Strike 2.  The interactions between objects are well-defined, but not very complex.  Applying mediator in its entirity would be overkill to the extreme, but looking at how the [[Mediator pattern]] was a good idea helped me to see the solution.
 +
 +
Finally, I realised, the ''ExperimentGUI'' was depending on the low-level details of each ''Card''. What I needed, was to listen to [[Bob Martin]] and follow the [[Dependency inversion principle]].  He states that what is needed is a dependency inversion, so in my case, the ''ExperimentGUI'' would no longer depend on the panels, but on the ''Card'' abstraction, and the panels would also depend on this ''Card'' abstraction.  Thus, the dependency is inverted.  This gives us our design above!
 +
 +
==== ExperimentGUI still depends on EightPuzzlePanel :-/ ====
 +
 +
You can see, however, that there is still one dependency to a subclass of the ''Card'' hierarchy - the ''ExperimentGUI'' must have an ''EightPuzzlePanel''.  This is due to the fact the ''EightPuzzlePanel'' is in charge of looking after the ''Participant''.  You could move the ''Participant'' to the ''ExperimentGUI'' controller class - but I think it makes more logical sense where it is.  The ''EightPuzzlePanel'' is the only thing that needs to know about the participant - as they do the experiment (by using the panel) data about the participant is recorded.
 +
Here again we have an object (''ExperimentGUI'') depending on the low-level details (''EightPuzzlePanel''), which doesn't fit with the [[Dependency inversion principle]].  This would not be too difficult to fix.  We need to represent the fact that we require some ''TaskCard'' (if you like) that is a card that represents the task to be completed by the ''Participant''(s).  A ''TaskCard'' would require a ''startTesting()'' method and a ''addName('' method.
 +
I considered doing this, but didn't for two reasons.  Firstly, it really isn't necessary yet.  [[Do the simplest thing that could possibly work]], [[Keep it simple]] and [[You ain't gonna need it]] were begging me not to.  It would make more sense only to do it once a different puzzle was actually required to be plugged in, instead of the eight-puzzle.  Secondly, I just ran out of time :)  But hopefully you can see it would be easy to refactor, when necessary, and the goal of making the experiment extensible would still be possible.

Revision as of 00:14, 23 September 2008

Contents

Intro

My project is on my Cosc411 experiment. This experiment was exploring whether people can solve a problem-solving task quicker when collaborating. Participants would solve #The Eight Puzzle five times, either individually or in pairs (training phase), then solve it five times alone (testing phase). The experiment was mostly automated (so they'd just read instructions, click next, etc.). Their performance was logged.

This introduced a few design challenges:

  • Single partipants and pairs of participants can solve the puzzle in the training phase, so pairs and individuals need to be treated the same, but (for logging) produce different output
  • The whole experiment follows a rigid process, so we need a nice way to define what we show and when.
  • Moreover, participants can take one of three paths. Individual, Collaborative, or Collaborative-resume (when the pair splits up after training, one person can continue, the other jumps to the same stage).
  • Individuals or Pairs need to get slightly different instructions, but they are very similar.
  • The experiment is largely GUI based, but we want to separate the GUI out as much as possible.

Furthermore, I would like to be able to provide a framework from which one can easily extend the experiment if needed in the future. For example:

  • can add participants who train in groups of three
  • can solve a different puzzle
  • can add an extra slide of instructions

All the code was written this semester (while taking Cosc427) and I tried to make a good design following the ideas we were taught. So effectively, I started with nothing. However, after the experiment was run I decided to move into a refactoring stage (specifically for this 427 project) with two goals:

  1. to further improve the design (duh)
  2. to refactor the design such that (in a hypothetical situation) it could be easily extended for future similar collaboration experiments

You can see the design as it was before refactoring at #Old Design.

Contents

Code

The code is written in Java 5.0 (stupid version numbers), and you can get it here: media:427projectCode.tgz.

To run it, run `java ExperimentGui`. You can run collaborative mode with `-c` or collaborative resume with `-cr` flags.

The Eight Puzzle

The eight-puzzle is a simple game where there are 8 tiles and one blank arranged in a 3x3 grid. A tile can be moved if it is adjacent to the blank, as if it is being slid across (left, right, up or down). There is a similar fifteen-puzzle where the pieces are in a 4x4 grid.

In fact, a version of the puzzle appeared on the original Macintosh System, thanks to Andy Hertzfeld, and remained on the OS for a long time following. Anyways, I digress. You can see a screenshot of the puzzle below.

8 Puzzle

See wikipedia for more details...

Old Design

This is what my design looked like when the program was actually used to run the experiment. After this, I put on a refactoring hat for the purposes of this Cosc427 assignment. I've shown it here so I can talk about some of the changes made in design.

Firstly, let me apologise that it is so big, and that you have to scroll sideways :).

And here it is:

OldDesign.gif

Tour of the Design

Here, you get a step-by-step tour through the design. This is the logical journey that digests the design in small bites. Just click the links below, in order, for a look at the main parts of the design. It'll be built up bit-by-bit, and then looking at the ../BigPicture should make more sense. Note this is not necessarily a chronological order of how it was designed, only the final design is shown, built up piecewise, in the sequence that (I think) is easiest to comprehend.

  1. #Logging Participants
  2. #Slide Show
  3. #Rigid Procedure
  4. #Varied Instructions

Logging Participants

The experiment tested the performance of participants who trained collaboratively, or individually. After training, all participants worked individually for testing. This introduced the first design challenge. Single partipants or pairs of participants can solve the puzzle in the training phase, so pairs and individuals need to be treated the same, but (for logging) produce different output. Furthermore, a pair becomes (*alarm bells*) a single participant once training is complete. This is my proposed design to model participants:

Participants.gif

To clarify the UML, Phase (seen in the diagram in the Participant class) is an enum (see Enum Idiom) that takes one of two values: TRAINING or TESTING. The variable currPhase stores the current phase (ie if the participant is training or testing). The bottom region of a class shows the "properties" (anything for which there is a getWhatever() method). The two properties in Participant (currRole and time) signify there are corresponding getters, but neither of them are actually stored as variable: they are derived attributes.

As you can see, the Participant class models one or more people taking part in the experiment, and they have two Roles: a trainingRole and a testingRole (the singular class name Participant may be misleading - it can model more than one person if they are working together - however, making it plural is more confusing). The ParticipantRole handles how the log data is output, as defined by the printData() method. A PairRole will print out data for both of the participants, while an IndividualRole will print it out for the single individual. Also, an individual has a single "name" (not meant literally - just some identifier), while pairs have a couple of names, one per person. The reason why an IndividualRole stores a wasCollaborative variable is because pairs may split into individuals, but we still need to know they were previously pairs, for the log output.

Observant readers will recognise this is strikingly similar to the Strategy pattern - or is it State pattern? Hmmm. I originally was of the opinion it was Strategy Pattern. Each different implementation of the printData() algorithm is encapsulated in its own class, and thus is interchangeable. This means we can vary the number of people working on the experiment together, and still output data for each person. However this varies from the typical Strategy. The roles don't just encapsulate an algorithm, they also store additional data about the "role" - ie the name(s) and wasCollaborative for individuals (see below), and there's the addName() method (more on that soon). However, considering the fact that some participants change from being pairs to singles, perhaps it is a State pattern. When changing state from a single to a pair, the behaviour changes, like when changing class. Although the trainingRole or testingRole attributes never change class, this is just due to the currPhase variable - conceptually we are still changing state. Thanks to Phil, Blair and Greg for pointing this out at our late-night (err early-morning) discussion session :).

The main advantages of this approach (regardless of whether it is State or Strategy) are that we treat single participants and pairs uniformally, although they have slightly different behaviour, we Avoid becomes, and its easy to extend this design if we needed, say, a TripleRole for groups of three. Additionally, as an added bonus, the data about each participants two roles are stored like a history. For more design pattern discussion, see #Design patterns.


[[Tell, don't ask and Law of Demeter are followed - the Participant wraps up calls to the role. To do this Recursion introduction is used: the participant just relays calls to printData() and addName() to methods in one of its roles (of the same name), depending on the currPhase. In this situation I think applying these maxims is definitely beneficial. The ParticipantRole becomes transparent so we don't even need to know that a participant has a role, as far as anyone using a Participant is concerned, it can addName() and printData(), and we don't care how. It is effectively InformationHiding. The alternative, something like participant.getCurrentRole().addName(), requires the client to know that the participant has a role, it has a getter to access it, it stores the names, and we can change them using the method addName()...

So what is this addName() method? I needed some way to set a participants name. However, to set a pair's names, we would need two names, as opposed to the one required to set an individual's name. But if IndividualRole and PairRole have different setName() methods (with a different number of arguments), then a Participant could only setName() if it knew whether its role is training or testing. A Participant shouldn't need to know the types of its roles, just that it has roles, and what their interface is. Simply put, I want to Program to the interface not the implementation. To do this, a uniform way to set name(s) is required, even though we don't know how many names. I choose to do this by using a common interface: an addName() method sets one name, and it can just be called a couple of times to set a couple of names (for pairs). I noticed later that this was a similar idea to the Builder pattern (I don't think I can go so far as to say this is an application of the pattern) - we add parts (names) until we have built a product, and then we normally get the product (in my case, we change the product by adding parts, so there is no need to get it - we already had it). An alternative approach would have been to pass in a Collection of names. However, I decided this was not as good, as the number of items in the collection would be very small (and a majority of the time participants are individual), so there would be a lot of unnecessary Collections, and they do seem like overkill.

Slide Show

The experiment consists of pages of instruction, and the task to perform. Displaying these instructions, and the task itself, is analagous to a slide show. We have a number of slides (or "cards", thinking like Java's CardLayout) and for each, we show it, then move on. A key abstraction that was identified in the refactoring process, was that all panels that are displayed to the user abide by the same interface. Namely, all my panels follow a simple process - they show a card, until they are done. I decided a Card abstract class was necessary that captured this common behaviour.

The entire experiment application is controlled by the ExperimentGUI. This is also the main JFrame that contains any JPanel that is displayed. It keeps track of all the instruction Cards that can be displayed, plus the EightPuzzlePanel card that contains the gui by which someone solves the puzzle.

Card.gif

As you can see above, by providing a Card abstraction, the coupling is less tight - the ExperimentGUI depends on the abstraction rather than the specific panels. The abstract Card also gives us a uniform way to tell the ExperimentGUI to move to the next slide. The Card stores the ExperimentGUI and uses it in the (concrete) done() method. This method just calls the panelDone() method in ExperimentGUI.

What the instruction Cards are themselves is not very important. ExplanationPanel is just a generic panel, with a text box and a next button, for displaying any textual instructions to the user. The abstract StartPanel and StartTestingPanel classes are also shown. These, much as the name suggests, are panels for instructions when the experiment begins, and just before testing, respectively. We'll look at StartPanel a bit more later on.

To create a new Card, all that is needed is to provide implementations for the abstract method show() (for any behaviour when the Card is first displayed) and call the done() method when finished.


Dependency Inversion Principle, applied

How I ended up with this design deserves some explanation. You can see what I had prior to refactoring at #Old Design. I was concerned with the three two-way dependencies - each panel was tightly coupled to the ExperimentGUI. Firstly I considered using the Observer pattern. I said at the time:

 ...I could make each "card" observable, to decrease the coupling between ExperimentGUI and a card. This doesn't fit the ObserverPattern that well: there is only one object doing the observing, and many being observed. However, it would get rid of an owner reference. Is it really worth it when there's only one Observer, ExperimentGUI? I think so. Observer pattern may make things a bit more complicated, but that's better than having the unnecesary owner reference: a card doesn't need to know its owner, only how to say "I'm done".

Then I realised, there had to be a better way. Strike one.

My next thoughts were that I could use the Mediator pattern. I thought the interactions between the GUI and the Panels is well-defined (e.g. GUI says "show this panel", a panel says "I'm done"). But then, is the State really a state of the gui? Or a state of the Mediator? Upon further examination, I realised that the Mediator pattern also didn't really fit. Strike 2. The interactions between objects are well-defined, but not very complex. Applying mediator in its entirity would be overkill to the extreme, but looking at how the Mediator pattern was a good idea helped me to see the solution.

Finally, I realised, the ExperimentGUI was depending on the low-level details of each Card. What I needed, was to listen to Bob Martin and follow the Dependency inversion principle. He states that what is needed is a dependency inversion, so in my case, the ExperimentGUI would no longer depend on the panels, but on the Card abstraction, and the panels would also depend on this Card abstraction. Thus, the dependency is inverted. This gives us our design above!

ExperimentGUI still depends on EightPuzzlePanel :-/

You can see, however, that there is still one dependency to a subclass of the Card hierarchy - the ExperimentGUI must have an EightPuzzlePanel. This is due to the fact the EightPuzzlePanel is in charge of looking after the Participant. You could move the Participant to the ExperimentGUI controller class - but I think it makes more logical sense where it is. The EightPuzzlePanel is the only thing that needs to know about the participant - as they do the experiment (by using the panel) data about the participant is recorded. Here again we have an object (ExperimentGUI) depending on the low-level details (EightPuzzlePanel), which doesn't fit with the Dependency inversion principle. This would not be too difficult to fix. We need to represent the fact that we require some TaskCard (if you like) that is a card that represents the task to be completed by the Participant(s). A TaskCard would require a startTesting() method and a addName( method. I considered doing this, but didn't for two reasons. Firstly, it really isn't necessary yet. Do the simplest thing that could possibly work, Keep it simple and You ain't gonna need it were begging me not to. It would make more sense only to do it once a different puzzle was actually required to be plugged in, instead of the eight-puzzle. Secondly, I just ran out of time :) But hopefully you can see it would be easy to refactor, when necessary, and the goal of making the experiment extensible would still be possible.

Personal tools