I have learned about the visitor pattern in C# 4.0 in a Nutshell's 19th chapter which is on C#'s dynamic keyword. I am revamping what I did here to use the visitor instead of the observer pattern to give an example. This is story of four animals which are affected in different ways by visiting the site of a thermonuclear explosion:
- an asp named Lucifer,
- a bug named Gregor (winking at Kafka),
- a cat named Felix,
- and a dog named Spot.
This test passes:
[TestMethod]
public void Visiting_SiteOfThermonuclearExplosion_Should_Affect_Animals()
{
Asp lucifer = new Asp();
Bug gregor = new Bug();
Cat felix = new Cat();
Dog spot = new Dog();
Assert.AreEqual(lucifer.IsHiding, false);
Assert.AreEqual(lucifer.IsSickly, false);
Assert.AreEqual(gregor.IsHiding, true);
Assert.AreEqual(gregor.IsSickly, false);
Assert.AreEqual(felix.IsHiding, false);
Assert.AreEqual(felix.IsSickly, false);
Assert.AreEqual(spot.IsHiding, false);
Assert.AreEqual(spot.IsSickly, false);
Assert.AreEqual(spot.IsRoamingTheCountrysideFoamingAtTheMouth, false);
SiteOfThermonuclearExplosion siteOfThermonuclearExplosion =
new SiteOfThermonuclearExplosion();
lucifer = siteOfThermonuclearExplosion.WelcomeVisitor(lucifer);
gregor = siteOfThermonuclearExplosion.WelcomeVisitor(gregor);
felix = siteOfThermonuclearExplosion.WelcomeVisitor(felix);
spot = siteOfThermonuclearExplosion.WelcomeVisitor(spot);
Assert.AreEqual(lucifer.IsHiding, false);
Assert.AreEqual(lucifer.IsSickly, true);
Assert.AreEqual(gregor.IsHiding, false);
Assert.AreEqual(gregor.IsSickly, true);
Assert.AreEqual(felix.IsHiding, true);
Assert.AreEqual(felix.IsSickly, true);
Assert.AreEqual(spot.IsHiding, false);
Assert.AreEqual(spot.IsSickly, true);
Assert.AreEqual(spot.IsRoamingTheCountrysideFoamingAtTheMouth, true);
}
What preceded was the whole of our story for Lucifer, Gregor, Felix, and Spot. Perhaps we should step through it again, slowly. There are two chapters to our story:
- In chapter one, our heroes are born.
Asp lucifer = new Asp();
Bug gregor = new Bug();
Cat felix = new Cat();
Dog spot = new Dog();
Assert.AreEqual(lucifer.IsHiding, false);
Assert.AreEqual(lucifer.IsSickly, false);
Assert.AreEqual(gregor.IsHiding, true);
Assert.AreEqual(gregor.IsSickly, false);
Assert.AreEqual(felix.IsHiding, false);
Assert.AreEqual(felix.IsSickly, false);
Assert.AreEqual(spot.IsHiding, false);
Assert.AreEqual(spot.IsSickly, false);
Assert.AreEqual(spot.IsRoamingTheCountrysideFoamingAtTheMouth, false);
- In chapter two, our antagonist is visited by our heroes.
SiteOfThermonuclearExplosion siteOfThermonuclearExplosion =
new SiteOfThermonuclearExplosion();
lucifer = siteOfThermonuclearExplosion.WelcomeVisitor(lucifer);
gregor = siteOfThermonuclearExplosion.WelcomeVisitor(gregor);
felix = siteOfThermonuclearExplosion.WelcomeVisitor(felix);
spot = siteOfThermonuclearExplosion.WelcomeVisitor(spot);
Assert.AreEqual(lucifer.IsHiding, false);
Assert.AreEqual(lucifer.IsSickly, true);
Assert.AreEqual(gregor.IsHiding, false);
Assert.AreEqual(gregor.IsSickly, true);
Assert.AreEqual(felix.IsHiding, true);
Assert.AreEqual(felix.IsSickly, true);
Assert.AreEqual(spot.IsHiding, false);
Assert.AreEqual(spot.IsSickly, true);
Assert.AreEqual(spot.IsRoamingTheCountrysideFoamingAtTheMouth, true);
Let's talk through who our actors are. First off, our villain is defined as so:
using WhosWatchingMe.Core.Animals;
namespace WhosWatchingMe.Core
{
public class SiteOfThermonuclearExplosion :
SiteOfThermonuclearExplosionHelper<Animal>
{
protected override Animal GiveRadiationSickness(Animal animal)
{
animal.IsSickly = true;
return animal;
}
protected override Animal GiveRadiationSickness(Dog dog)
{
dog.IsRoamingTheCountrysideFoamingAtTheMouth = true;
dog.IsHiding = false;
return base.GiveRadiationSickness(dog);
}
}
}
Our villain has an assistant. It would be possible to skip making this parent if one were to skip using the override keyword. SiteOfThermonuclearExplosion would take a mildly different shape in such a scenario. It would work akin to the way the example here does its thing. I'll get into why I decided not to go this way in a moment.
namespace WhosWatchingMe.Core.Animals
{
public abstract class SiteOfThermonuclearExplosionHelper<T> where T : Animal
{
public T WelcomeVisitor<T>(T animal)
{
animal = GiveRadiationSickness((dynamic)animal);
return (T)animal;
}
protected abstract T GiveRadiationSickness(Animal animal);
protected virtual T GiveRadiationSickness(Dog dog)
{
return GiveRadiationSickness((Animal)dog);
}
}
}
Once again our four animals are:
- an asp named Lucifer,
namespace WhosWatchingMe.Core.Animals
{
public class Asp : Animal
{
public Asp()
{
}
}
}
Asp is our easiest object. Asp is a truly bland implementation of Animal. Lucifer will act as any Animal would by default. Gregor, Felix, and Spot are also of types inheriting from Animal as well, yet they behave differently which must mean that their types of Animal must do more than the Asp subclass of Animal which brings no polymorphism (overriding) to the base shape. Animal is where things gets interesting. The base class looks like so: namespace WhosWatchingMe.Core
{
public abstract class Animal
{
public virtual bool IsHiding { get; set; }
public virtual bool IsSickly { get; set; }
public Animal()
{
IsHiding = false;
IsSickly = false;
}
}
}
Animal carries getsetters for IsHiding and IsSickly and its constructor sets both as false. Lucifer starts our story neither hiding nor sickly. He never hides, but he does end up sick. How so? SiteOfThermonuclearExplosion sets IsSickly to true!
- a bug named Gregor (winking at Kafka),
namespace WhosWatchingMe.Core.Animals
{
public class Bug : Animal
{
public override bool IsSickly
{
get
{
return base.IsSickly;
}
set
{
base.IsSickly = value;
if (value)
{
IsHiding = false;
} else {
IsHiding = true;
}
}
}
public Bug()
{
IsHiding = true;
}
}
}
Bug has a constructor which makes it hide by default, the opposite of the default behavior Animal's constructor imposes. Bug also overrides the IsSickly getsetter on Animal to offers some additional behavior. Whenever a bug gets sick it stops hiding. If it gets better it will start hiding again. Gregor starts our story both hiding and healthy and ends up sick and exposed.
- a cat named Felix,
namespace WhosWatchingMe.Core.Animals
{
public class Cat : Animal
{
public override bool IsSickly
{
get
{
return base.IsSickly;
}
set
{
base.IsSickly = value;
IsHiding = value;
}
}
public Cat()
{
}
}
}
Cat is more complicated than Asp but not as complicated as Bug. It overrides the IsSickly state and there makes the IsHiding state match the IsSickly state. Felix does not hide and is not sick at the beginning of our story. Once he gets sick, he also hides as he is a Cat.
- and a dog named Spot.
namespace WhosWatchingMe.Core.Animals
{
public class Dog : Animal
{
public bool IsRoamingTheCountrysideFoamingAtTheMouth { get; set; }
public Dog()
{
IsRoamingTheCountrysideFoamingAtTheMouth = false;
}
}
}
Dog has an additional getsetter which its constructor sets to false. The magic around the dynamic keyword in SiteOfThermonuclearExplosion and SiteOfThermonuclearExplosionHelper allows for the Dog type to be cherry-picked in lieu of the Animal type to allow for setting the getsetter which Animal does not have. Spot begins life not hiding, feeling fine, and not deranged. Upon visiting the site of a nuclear explosion he becomes sick and goes berserk.
If we wanted to throw away SiteOfThermonuclearExplosionHelper then the last method in SiteOfThermonuclearExplosion would have to look like this:
protected Animal GiveRadiationSickness(Dog dog)
{
dog.IsRoamingTheCountrysideFoamingAtTheMouth = true;
dog.IsHiding = false;
dog.IsSickly = true;
return dog;
}
Honestly, in keeping with this we would likely have a method like so:
protected Dog GiveRadiationSickness(Dog dog)
{
dog.IsRoamingTheCountrysideFoamingAtTheMouth = true;
dog.IsHiding = false;
dog.IsSickly = true;
return dog;
}
Alas, if we just double-type the code for the base class effect we are guilty of breaking the don't repeat yourself rule. I should have seen this when I wrote this but I did not. I have this in the other blog posting:
public override void AdsorbRadiation()
{
this.IsSickly = true;
this.IsHiding = false;
this.IsRoamingTheCountrysideFoamingAtTheMouth = true;
}
It should be this instead:
public override void AdsorbRadiation()
{
base.AdsorbRadiation();
this.IsHiding = false;
this.IsRoamingTheCountrysideFoamingAtTheMouth = true;
}
The visitor is perhaps supposed to be the actor doing the manipulation and not the actors getting manipulated. I'm not sure. I might have that semantically backwards. I forgive myself though.
No comments:
Post a Comment