My biggest problem with abstract classes is that they rely entirely upon inheritance, and I don't like inheritance that much either.
Some languages, like Rust and Go don't even have it, and they get along just fine. I've looked up a few articles about them to try and figure out why they're able to function without inheritance, and found some interesting reads:
- Here's one for GO
- Here's one for Rust (Rust heavily relies on traits, which can be emulated in Javascript through symbols)
Here's a quote from the Go article that I think is applicable here:
Inheritance in traditional object-oriented languages offers three features in one. When a Dog inherits from an Animal
- the Dog class reuses code from the Animal class,
- a variable x of type Animal can refer to either a Dog or an Animal,
- x.Eat() will choose an Eat method based on what type of object x refers to.
In object-oriented lingo, these features are known as code reuse, polymorphism and dynamic dispatch.
All of these are available in Go, using separate constructs:
- composition and embedding provide code reuse,
- interfaces take care of polymorphism and dynamic dispatch.
In Javascript, here's how you might solve those three things that inheritance provides
1. Code reuse (the Dog class reuses code from the Animal class).
Here's a simple way to reuse code without inheritance.
class Dog {
#animalTools
constructor() {
this.#animalTools = new AnimalTools({ speakSounds: 'Bark!' })
}
speak() {
this.#animalTools.speak()
}
// ...
}
Here, this AnimalTools() utility class provides a number of helpful functions to make it simply to implement any animal. A traditional inheritance approach might have magically provided a speak() method for you. In this approach, we're still explicitly showing what all of the methods are to a particular class, so it's easy to see where they come from. This is "composition over inheritance", and is generally preferred because it's more explicit.
However, if your class starts looking something like this:
class Dog {
#animalTools
constructor() {
...
}
speak() { this.#animalTools.speak() }
walk() { this.#animalTools.walk() }
run() { this.#animalTools.run() }
// ... 20 more methods like that
}
then maybe you're at the point where composition is just getting in the way and not providing much value. At this point, you have a few options to choose from:
- Instead of manually forwarding lots of logic from animalTools, just make animalTools a public property (and give it a better name). This change your API, such that users would have to do
dog.animalTools.speak()
(this may not be acceptable in some scenarios).
- Instead of having multiple classes (Dog, Cat, etc), have a single Animal class that takes Dog/Cat behavior objects in its constructor (this is the strategy pattern I showed previously). This doesn't work well if you're also wanting to add extra methods to the dog object, that are only available for dogs. There are different ways you could work around this issue, each with different pros and cons.
- Finally, when all of those other options don't work well, there's inheritance. Honestly, I haven't yet had a use case for inheritance as of yet, as I've always found some other options that could be used instead, but that doesn't mean such use cases don't exist.
2. Polymorphism (A variable x of type Animal can refer to either a Dog or an Animal)
Unless you're using instanceof everywhere in your codebase, this isn't an issue in Javascript to begin with. As long as your class has the correct methods, you should be able to pass a Dog or Cat into anything that expects an Animal, and things should just work - even if Dog and Cat don't strictly inherit from some Animal base class. This is duck typing.
3. Dynamic dispatch (x.Eat() will choose an Eat method based on what type of object x refers to)
This is just saying that x.eat() will call the "eat" method from the class that x
was constructed from. This is already how Javascript behaves, so nothing to worry about here.
So, if you end up in a scenario where inheritance really is the best solution and an abstract class would be the best way to do that kind of inheritance, then you're in a rare enough scenario that it's probably ok if there isn't explicit syntax support for this kind of thing, and it should be ok to use a solution like @aclaymore shows. That solution is verbose enough to ward people off from overusing it, but still easy enough to use and understand that when people actually need it, they can have it.
If other people have a different perspective towards inheritance, they can speak up too - my opinions are certainly not the only ones out there :).