
Настало время веселья, и что может быть лучше, чем создать праздничное настроение с помощью технического поста в блоге об объектно-ориентированном программировании (ООП)? В этом праздничном посте я расскажу о концепции наследования и о том, почему важно игнорировать реальность, чтобы использовать ее надлежащим образом.
Даже если бы Санта-Клаус был настоящим (а он вполне реален — я дважды проверял!), он не смог бы наследоваться от класса Human.
Что такое наследование?
В ООП наследование — это способ, которым один класс наследует характеристики и поведение другого класса. Это позволяет нам создать иерархию классов и представить отношение «есть-а». Например, класс «Собака» может наследоваться от класса «Млекопитающее», потому что собака — это тип млекопитающего.
Почему Санта-Клаус не может наследовать от человека
Хотя на первый взгляд может показаться логичным сделать Санта-Клауса наследником человека, есть несколько причин, по которым это не очень хорошая идея.
Наследование обычно используется для представления отношения «есть-а», что означает, что подкласс (в данном случае Санта-Клаус) является более конкретным типом надкласса (Человек). Однако Санта-Клаус не является типом человека — у него есть уникальные характеристики и способности, которые не применимы ко всем людям. Например, у Деда Мороза есть список непослушных и славных детей, волшебные сани, которые могут облететь весь мир за одну ночь, и способность доставлять подарки всем хорошим мальчикам и девочкам на Земле.
Вы можете подумать, что вот как это работает: у вас есть более общая база с более конкретными вещами поверх нее. Но реальность и программирование работают по-разному. Например, если мы попытаемся сделать так, чтобы Санта-Клаус наследовал от Человека, мы могли бы получить такие методы, как «getAge()» или «getHeight()», которые не имеют смысла для персонажа, которому сотни лет и который может уменьшаться. или расти, чтобы пройти через дымоходы.
Лучший подход
Лучшим подходом было бы создание отдельного класса для Санта-Клауса и определение его характеристик и поведения в этом классе. Таким образом, мы можем точно представить его и избежать путаницы или ненужных проверок по всему коду. В конце концов, мы бы не хотели, чтобы какой-нибудь непослушный код проник в наш хороший список!
Квадрат и прямоугольник
Другой пример неуместного наследования — попытка сделать класс «Квадрат» наследником класса «Прямоугольник». Хотя в реальной жизни квадрат является типом прямоугольника, он имеет свои уникальные характеристики (например, все стороны имеют одинаковую длину), которые не являются общими для всех прямоугольников. В этом случае для целей программирования было бы лучше создать для Square отдельный класс и самостоятельно определить его характеристики и поведение.
Вот пример того, что может случиться, когда мы делаем Square унаследованным от Rectangle:
class Rectangle {
constructor(public length: number, public width: number) {}
getArea(): number {
return this.length * this.width;
}
setLength(length: number): void {
this.length = length;
}
setWidth(width: number): void {
this.width = width;
}
}
class Square extends Rectangle {
constructor(sideLength: number) {
super(sideLength, sideLength);
}
}
const square = new Square(5);
console.log(square.getArea()); // 25
square.setLength(10);
console.log(square.getArea()); // 50
В этом коде у нас есть класс Rectangle с методом getArea() для вычисления площади прямоугольника, а также методы установки для изменения длины и ширины прямоугольника. У нас также есть класс Square, который наследуется от Rectangle, но переопределяет конструктор, чтобы установить длину и ширину равными (поскольку они равны в квадрате).
Сначала, когда мы создаем объект Square и вызываем метод getArea(), он работает, как и ожидалось, и возвращает правильное значение. Однако когда мы используем метод установки setLength() для изменения длины квадрата, это нарушает ожидаемое поведение квадрата.
В квадрате все стороны равны, поэтому изменение длины квадрата должно также изменить ширину. Однако, поскольку мы наследуем от класса Rectangle и неточно представляем характеристики квадрата, этого не происходит. В результате мы получаем неверное значение площади.
Лучшим подходом было бы создание отдельного класса для Square и определение его собственных методов установки, которые гарантируют, что все стороны останутся равными при изменении одной из них. Таким образом, мы можем избежать любых ошибок или нереалистичного поведения в нашем коде.
class Square {
constructor(public sideLength: number) {}
getArea(): number {
return Math.pow(this.sideLength, 2);
}
setSideLength(sideLength: number): void {
this.sideLength = sideLength;
}
}
const square = new Square(5);
console.log(square.getArea()); // 25
square.setSideLength(10);
console.log(square.getArea()); // 100
В этом исправленном коде у нас есть отдельный класс Square с собственным методом установки для изменения длины стороны квадрата. Когда мы используем этот метод установки для изменения длины стороны, он правильно обновляет все стороны квадрата и поддерживает ожидаемое поведение.
Кроме того, если бы мы попытались поддерживать квадрат как подкласс прямоугольника, нам пришлось бы включить в код множество условных операторов для учета квадрата или переопределить их в классе квадрата, и даже тогда мы все равно получили бы такие методы, как setLength() и setHeight().
Это привело бы к большому количеству дополнительного кода и сложности, что усложнило бы понимание и поддержку кодовой базы. Вместо этого гораздо проще и эффективнее создать отдельный класс для Square. Таким образом, мы можем четко представить уникальные характеристики квадрата и избежать ненужной сложности кода.
Окончательно
В некоторых случаях, например, в случае с Санта-Клаусом и Квадратом, лучше игнорировать концепции из реальной жизни в пользу того, что будет лучше работать для концепций программирования.
Итак, используйте это время и поступайте как Санта, проверяя свой список классов и разделяя их на хорошие и непослушные классы, а затем проверяя дважды.
Счастливых праздников и удачного кодирования!
Фото на обложке Mike Arney на Unsplash