Inheritance and composition are both ways of code reuse in object-oriented. Knowing their characteristics allows us to write more concise code and design a better code structure.
This is a translated article by serhiirubets
"How should I use inheritance and composition" is a common question, not only JavaScript related, but in this article we will only discuss JavaScript related content and examples.
If you don't know what composition or inheritance is, I highly recommend you to read the related content, because this article is mainly about how to use and how to choose them, but to make sure we are in a channel, let's first understand composition and inheritance .
Inheritance is one of the core concepts of object-oriented programming that can help us avoid code duplication. The main idea is that we can create a base class that contains logic that can be reused by subclasses. Let's look at an example:
class Element {
remove() {}
setStyles() {}
}
class Form extends Element {}
class Button extends Element {}
We have created a base class "Element", and subclasses will inherit the common logic in Element.
Inheritance exists an is-a relationship: Form
is a Element
, Button
is also a Element
Composition : Unlike inheritance, composition uses a has-a relationship to collect different relationships together.
class Car {
constructor(engine, transmission) {
this.engine = engine;
this.transmission = transmission;
}
}
class Engine {
constructor(type) {
this.type = type;
}
}
class Transmission {
constructor(type) {
this.type = type;
}
}
const petrolEngine = new Engine('petrol');
const automaticTransmission = new Engine('automatic');
const passengerCar = new Car(petrolEngine, automaticTransmission) ;
Engine
Transmission
Car
,我们不能说Engine
Car
,但是Lets say Car
contains Engine
. Hope the above example helps you understand what inheritance is and what is composition .
Let's look at two different examples and compare the difference between using class methods to achieve inheritance and function methods to achieve composition.
Suppose we are using a file system and want to implement read, write and delete functions. We can create a class:
class FileService {
constructor(filename) {
this.filename = filename;
}
read() {}
write() {}
remove() {}
}
At present, it can meet the functions we want, and then we may want to add permission control, some users only have read permissions, others may have write permissions. What should we do? One solution is that we can divide the methods into different classes:
class FileService {
constructor(filename) {
this.filename = filename;
}
}
class FileReader extends FileService {
read() {}
}
class FileWriter extends FileService {
write() {}
}
class FileRemover extends FileService {
remove() {}
}
Now each user can use the permissions they need, but there is one more question, what if we need to assign read and write permissions to some people at the same time? What about assigning read and delete permissions at the same time? With the current implementation, we can't do it, how should we solve it?
The first solution that comes to mind might be: create a class for read and write, and a class for read and delete.
class FileReaderAndWriter extends FileService {
read() {}
write() {}
}
class FileReaderAndRemover extends FileService {
read() {}
remove() {}
}
Following this practice, we may also need the following classes: FileReader, FileWriter, FileRemove, FileReaderAndWriter, FileReaderAndRemover.
This is not a good implementation: first, we may have not only 3, but 10, 20 methods, and there needs to be a lot of combinations between them. The second is that there is duplicate logic in our class, the FileReader class contains the read method, and the FileReaderAndWriter also contains the same code.
This is not a good solution, is there any other way to implement it? Multiple inheritance? There is no such feature in JavaScript, and it is not a good solution: class A inherits class B, class B may inherit other classes..., such a design will be very confusing and not a good code structure.
How to solve it? A reasonable approach is to use composition : we split the method into separate function factories.
function createReader() {
return {
read() {
console.log(this.filename)
}
}
}
function createWriter() {
return {
write() {
console.log(this.filename)
}
}
}
In the above example, we have two functions that create objects that can be read and written. Now we can use them anywhere, or combine them:
class FileService {
constructor(filename) {
this.filename = filename ;
}
}
function createReadFileService (filename ) {
const file = new FileService(filename);
return {
...file,
...createReader()
}
}
function createWriteFileService (filename) {
const file = new FileService(filename);
return {
...file,
...createWriter(),
}
}
In the above example, we created read and write services, if we want to combine different permissions: read, write and delete, we can do it easily:
function createReadAndWriteFileService (filename) {
const file = new FileService(filename);
}
return {
...file,
...createReader(),
...createWriter()
}
const fileForReadAndWriter = createReadAndWriteFileService('test');
fileForReadAndWriter.read();
fileForReadAndWriter.write();
If we have 5, 10, 20 methods, we can combine them the way we want, without duplicate code issues, and without confusing code architecture.
Let's look at another example using a function, assuming we have a lot of employees, a taxi driver, a fitness trainer, and a driver:
function createDriver(name) {
return {
name,
canDrive: true,
}
}
function createManager(name) {
return {
name,
canManage: true
}
}
function createSportCoach(name) {
return {
name,
canSport: true
}
}
It seems that there is no problem, but suppose some employees are fitness trainers during the day and run for rent at night, how should we adjust it?
function createDriverAndSportCoach(name) {
return {
name,
canSport: true,
canDriver: true
}
}
It can be achieved, but as in the first example, if we have a mix of types, it will result in a lot of repetitive code. We can refactor by combining :
function createEmployee(name,age) {
return {
name,
age
}
}
function createDriver() {
return {
canDrive: true
}
}
function createManager() {
return {
canManage: true
}
}
function createSportCoach() {
return {
canSport: true
}
}
Now we can combine all job types as needed, no duplication of code, and easier understanding:
const driver = {
...createEmployee('Alex', 20),
...createDriver()
}
const manager = {
...createEmployee('Max', 25),
...createManager()
}
const sportCoach = {
...createEmployee('Bob', 23),
...createSportCoach()
}
const sportCoachAndDriver = {
...createEmployee('Robert', 27) ,
...createDriver(),
...createSportCoach()
}
Hope you can now understand the difference between inheritance and composition , in general, inheritance can be used for is-a
relationships and composition can be used for has-a
.
But in practice, inheritance is sometimes not a good solution: as in the example, the driver is an employee ( is-a
relationship), and the manager is also an employee. If we need to mix different parts, combine It is indeed more appropriate than inheritance.
Last but not least, inheritance and composition are good implementations, but you should use them correctly. Some scene combinations may be more appropriate, and vice versa.
Of course, we can combine inheritance and composition , like we have is-a
relationship, but want to add different values or methods: we can create some base class that provides all the common functionality for the instance, and then use composition to add other specific features.
Welcome to the public account of "Chaos Front End"
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。