Duck Typing
it is a role which can be applied to some specific class in some specific moment of time
- this is the moment when we can extract the duck types
- duck type is not related to the interfaces or abstract classes, it is only about the specific class which plays specific role in specific moment of time
duck-typing feature ensures type safety
the technique in TypeScript is used to compare two objects by determining if they have the same type matching properties and objects members or not
For example, if we assign an object with two properties and a method and the second object is only assigned with two properties
- The typescript compiler raises a compile-time error in such situations when we create a variable of object1 and assign it a variable of the second object type
class Pigeon {
sound = "coos";
}
class Owl {
sound = "hoots";
}
class Penguin {
sound = "peeps";
swim() {
console.log("I'm a bird and i can swim");
}
}
let pigeon: Pigeon = new Owl(); // Works
let owl: Owl = new Pigeon(); // Works
let pigeon2: Pigeon = new Penguin(); // Works
let penguin: Penguin = new Pigeon(); // Compile time error
If It Walks Like a Duck and Talks Like a Duck Then It's a Duck
example
- the prepare method will dramatically increase in the number of dependencies
- it knows every class name, class method names and their arguments the worst is this type of code will only increase its size and dependencies number with time
class Mechanic {}
class TripCoordinator {}
class Driver {}
class Trip {
bicycles;
customers;
vehicle;
prepare(prepares: object[]) {
return prepares.map((preparer) => {
switch (preparer.constructor) {
case Mechanic:
return preparer.prepareBicycles(this.bicycles);
case TripCoordinator:
return preparer.buyFood(this.customers);
case Driver:
preparer.fillTank(this.vehicle);
return preparer.fillWaterTank(this.vehicle);
}
});
}
}- the prepare method will dramatically increase in the number of dependencies
Finding the Duck
- after identifying the problem, we need to minimize dependencies number to make the
Trip
functionality easily extensible without usage of switch case and other similar approaches - every preparer class is responsible for preparing something for the
Trip
- we can try to extract some
Preparer
abstraction and call it aduck type
- this becomes something similar to an interface
- we can try to extract some
class Trip {
prepare(prepares: {prepareTrip(trip: Trip)}[]) {
prepares.map((preparer) => {
preparer.prepareTrip(this);
});
}
}
class Driver {}
class Mechanic {
prepareTrip(trip: Trip) {
trip.bicycles.map((bicycle) => {
this.prepareBicycle(bicycle);
});
}
}
class TripCoordinator {
prepareTrip(trip: Trip) {
this.buyFood(trip);
}
}
Writing Code that Relies on Ducks
Recognizing Hidden Ducks
- You need to timely understand where the duck types are hidden and how to extract them, pay attention to the next places in the code:
- Case statements that switch on class
- similar to the switch..case situation, but here you check if you are working with instance of some specific class
- instanceof operator
- when trying to find out which exact class you are working with to apply specific steps to it, this may be a sign of hidden duck type
- Checking the method exists (if (obj.someMethod) { obj.someMethod() })
- It is also a good sign that there may be a hidden duck type when do not need to check the exact class but when you only need some specific method to exist
- Case statements that switch on class
- You need to timely understand where the duck types are hidden and how to extract them, pay attention to the next places in the code:
Placing Trust in Your Ducks
- Let client code trust the duck type, in lack of the trust client code means the next: "I know who you are, so I know what you do"
- Such knowledge transforms into tight coupling between classes which results into non extensible code
- Flexible applications built on top of objects which works on trust – your goal as a developer is to make those objects reliable, to let the trust work
Documenting Duck Types
- Preparer duck type and its open interface is a specific part of the design, but at the same time it is a virtual part of code, because it is neither a class nor a real interface
- Preparers are an abstraction, just a convention which gives you the powerful system design tool, but this abstraction makes code less obvious
- When you create a duck type, you must document and cover it with tests
Sharing Code between Ducks
- In the example shared is only prepareTrip method, but when you start using duck types, you may notice that some part of the functionality is common for all the types
- Share such functionality using mixins and other available approaches
Choosing Your Ducks Wisely
- The last point, tells us that you do not need to create duck types just to have them
- You need to find a balance between resources required for the refactoring, benefit gained, support simplicity and code clarity