Let's reinforce the theory we've learned with one practical example that shows a typical application of subtype polymorphism.
Imagine the task of calculating the cost of travel insurance. The cost of insurance depends on a large number of factors. Some factors can affect how companies calculate the price by changing the formula itself.
In the case of insurance, there is likely one formula where the values are substituted, and the calculation happens in one go. The main thing for us now is the concept itself, not precise knowledge of the inner workings of the insurance business.
If you solve this problem head-on, it will look like a big mess of calculations with many conditional constructs. Over time, such a code becomes extremely difficult to read because of the many states you keep in mind:
// A few examples we came up with
if (age < 18) {
let cost = salary * age;
if (country === 'uganda') {
cost = cost / 2;
}
} else if (age >= 18 && age < 24) {
// ...
}
Is there any way to make the code clearer and easier to understand? Sometimes yes. Of all the factors involved in the calculation, you should try to find those that affect it globally. They manifest as a global if
at the top level.
Let's assume that in the case of insurance, it's age. We believe that age determines the formula for calculating the cost of insurance.
The next step is to look at the branches in this conditional design and specify the ranges. Let's say it looks like this:
- Under 18
- 18 to 24
- 24 to 65
- Older than 65
There's something important you need to take note of. Above, we agreed that each age group determines the algorithm for calculating the insurance cost. They're independent of each other, although the calculation process itself may be similar in places (and most likely will be).
Now let's move from the logical structure to the code. Each age group is the class responsible for calculating the cost for that group:
class LessThan18 {
// Parameters are the factors on which the calculation is based
calculate(params) {
// Here we count and return the result
}
}
// The name is not that great. In real code you should think of something more meaningful
class MoreThan18AndLessThan24 {
// The structure of the parameters must be 100% the same as the rest of the classes,
// since only in this case is polymorphism possible
calculate(params) {
// Here we count and return the result
}
}
// Other classes
The main thing we got was to divide the calculation process into independent blocks of code, which are easier to understand. Each such class is called a strategy (computation).
It's important for the strategy not to be an abstraction, an object with a state and lifetime. Therefore, we pass to the constructor not the data but the method itself.
In essence, it's an ordinary function packed into a class with only one purpose: to get subtype polymorphism. We can do this via the function dispatching by keys to simplify the code.
Next, the following question arises: how and where do we choose the implementation to work with? There are several options here.
We can delegate the choice of implementation to an external code. If we apply dependency inversion, that means we're working with a ready-made strategy:
calculateCost(strategy, params) {
strategy.calculate(params);
}
So far, we've only gotten away from the problem; we haven't solved it. Either way, there will be code somewhere that contains either a conditional construct or implements one of the dispatching methods we covered in previous lessons. In the simplest case, this code would look like this:
chooseCostInsuranceStrategy(user) {
if (user.getAge() < 18) {
return new LessThan18();
} else if (/* ... */) {
// some code
}
}
strategy = chooseCostInsuranceStrategy(user);
strategy.calculate(params);
As you can see from the examples above, there'll be more code using the strategy, but not as much as if you were to use the dispatching of functions by associative array keys.
It applies to virtually all situations where subtype polymorphism is involved in JavaScript. It is the price you have to pay for a division that simplifies code expansion and reduces its complexity.
On the other hand, it is easy to fall into the trap and, conversely, make the code even more complex than it was before the introduction of subtype polymorphism.
This polymorphism makes the code verbose and overly abstract if applied mindlessly. You don't need expansion as often as people say you do. Moreover, you can invert dependencies as you go along whenever you need to.
Recommended materials
Are there any more questions? Ask them in the Discussion section.
The Hexlet support team or other students will answer you.
For full access to the course you need a professional subscription.
A professional subscription will give you full access to all Hexlet courses, projects and lifetime access to the theory of lessons learned. You can cancel your subscription at any time.