Skip to content

Instantly share code, notes, and snippets.

@cdanyl
Last active December 30, 2019 17:15
Show Gist options
  • Select an option

  • Save cdanyl/03b81c5968e6ab831eee36237019fdb8 to your computer and use it in GitHub Desktop.

Select an option

Save cdanyl/03b81c5968e6ab831eee36237019fdb8 to your computer and use it in GitHub Desktop.
Visitor Pattern
/*
* Visitor pattern solves the problem elegantly. Start by modifying our base Fruit class:
*/
interface FruitVisitor {
visit(fruit: Orange): void;
visit(fruit: Apple): void;
visit(fruit: Banana): void;
}
abstract class Fruit {
abstract accept(visitor: FruitVisitor): void
}
class Orange extends Fruit {
public accept(visitor: FruitVisitor): void {
visitor.visit(this);
}
}
class Apple extends Fruit {
public accept(visitor: FruitVisitor): void {
visitor.visit(this);
}
}
class Banana extends Fruit {
public accept(visitor: FruitVisitor): void {
visitor.visit(this);
}
}
/*
* It looks like we're copy pasting code, but note the derived classes are all calling different overloads (the Apple calls Visit(Apple), the Banana calls Visit(Banana), and so on).
* Implement the visitor:
*/
class FruitPartitioner implements FruitVisitor {
public oranges: Orange[] = [];
public apples: Apple[] = [];
public bananas: Banana[] = [];
public visit(fruit: Orange): void {
this.oranges.push(fruit);
}
public visit(fruit: Apple): void {
this.apples.push(fruit);
}
public visit(fruit: Banana): void {
this.bananas.push(fruit);
}
}
/*
* And let's say we create a Fruit[]:
*/
const fruits: Fruit[] = [];
fruits.push(new Orange());
fruits.push(new Apple());
fruits.push(new Banana());
fruits.push(new Banana());
fruits.push(new Orange());
/*
* Now you can partition your fruits without a type-test:
*/
const fruitPartitioner = new FruitPartitioner();
for (const fruit of fruits) {
fruit.accept(fruitPartitioner);
}
console.log(fruitPartitioner.oranges);
console.log(fruitPartitioner.apples);
console.log(fruitPartitioner.bananas);
/**
* Visitor Pattern in Typescript.
* So you've probably read a bajillion different explanations of the visitor pattern, and you're probably still saying "but when would you use it!"
* Traditionally, visitors are used to implement type-testing without sacrificing type-safety,
* so long as your types are well-defined up front and known in advance.
*/
/*
* Let's say we have a few classes as follows:
*/
abstract class Fruit {}
class Orange extends Fruit {}
class Apple extends Fruit {}
class Banana extends Fruit {}
/*
* And let's say we create a Fruit[]:
*/
const fruits: Fruit[] = [];
fruits.push(new Orange());
fruits.push(new Apple());
fruits.push(new Banana());
fruits.push(new Banana());
fruits.push(new Orange());
/*
* I want to partition the list in to three lists, each containing oranges, apples, or bananas.
* How would you do it? Well, the easy solution would be a type-test:
*/
const oranges: Orange[] = [];
const apples: Apple[] = [];
const bananas: Banana[] = [];
fruits.forEach((fruit) => {
if (fruit instanceof Orange) {
oranges.push(fruit);
} else if (fruit instanceof Apple) {
apples.push(fruit);
} else if (fruit instanceof Banana) {
bananas.push(fruit);
}
});
/*
* It works, but there are lots of problems with this code:
* For a start, its ugly.
* Its not type-safe, we won't catch type errors until runtime.
* Its not maintainable. If we add a new derived instance of Fruit, we need to do a global search for every place which performs a fruit type-test, otherwise we might miss types.
*/
console.log(oranges);
console.log(apples);
console.log(bananas);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment