Обычно, в конечном итоге, наследование приводит к созданию последовательности классов, основанных на единой интерфейсе. Мы используем перевёрнутую схему дерева для объяснения этого:
Примечание ⑤: Здесь используется "общий язык моделей", который будет использоваться преимущественно в этой книге.
Для такой последовательности классов важной операцией является использование объектов производных классов как объектов базового класса. Это очень важно, так как это означает, что нам нужно писать одинаковый код, игнорируя детали конкретного типа, взаимодействуя только с базовым классом. Таким образом, этот код может быть отделён от информации о типах, что делает его проще для написания и понимания. Кроме того, если мы добавляем новый тип через наследование, например, "треугольник", то код, написанный для нового типа "геометрической фигуры", будет работать точно так же хорошо, как и для старых типов. То есть программа обладает способностью расширяться и адаптироваться.
На основе вышеупомянутого примера, предположим, что мы написали следующую функцию на Java:
void doStuff(Shape s) {
s.erase();
// ...
s.draw();
}
```Эта функция может взаимодействовать со всеми "геометрическими фигурами" (`Shape`), поэтому она полностью независима от любого конкретного типа объекта, который она должна рисовать (`draw`) или удалить (`erase`). Если мы будем использовать `doStuff()` функцию в других программах:```java
Circle c = new Circle();
Triangle t = new Triangle();
Line l = new Line();
doStuff(c);
doStuff(t);
doStuff(l);
Таким образом, вызовы doStuff()
будут автоматически работать правильно независимо от конкретного типа объекта.
Это действительно полезный прием программирования. Рассмотрим следующую строку кода:
doStuff(c);
Здесь ссылка на Circle
(круг) передается функции, которая ожидала ссылку на Shape
(фигуру). Поскольку круг является геометрической фигурой, doStuff()
сможет корректно обрабатывать её. То есть любое сообщение, которое doStuff()
может отправить Shape
, также может принимать Circle
. Поэтому это безопасно и не вызывает ошибок.
Мы называем процесс использования производных типов как базовых типов "Upcasting" (преобразование вверх). В этом контексте "cast" (преобразование) означает создание чего-либо по существующему образцу; а "up" (вверх) указывает направление наследования сверху вниз — то есть базовый класс находится в вершине, а производные классы расположены ниже. Таким образом, преобразование по базовому классу представляет собой процесс наследования сверху вниз, то есть "Upcasting". В объектно-ориентированном программировании обычно используются преобразования вверх по иерархии. Это хороший способ избежать необходимости выяснять точный тип. Посмотрите на код внутри doStuff()
:```cpp
s.erase();
// ...
s.draw();
Обратите внимание, что это не так выражено: "Если ты круг, делай то; если ты квадрат, делай это". Если бы код был таким, пришлось бы проверять все возможные типы фигур, такие как окружность, прямоугольник и т.д., что было бы очень сложно. Кроме того, каждый раз при добавлении нового типа фигуры, потребовалось бы его учитывать. В данном случае мы просто говорим: "Ты геометрическая фигура, и я знаю, что ты можешь удалиться с помощью метода `erase()`. Возьми на себя выполнение этого действия и контролируй все детали".
### 1.6.1 Динамическое связывание
Что особенно удивляет в коде `doStuff()`, так это то, что действие выполняется правильно и точно, хотя специальных указаний нет. Мы знаем, что вызов метода `draw()` для `Circle` выполняет другой код, чем вызов метода `draw()` для `Square` или `Line`. Однако когда сообщение `draw()` отправляется фигуре, которая является анонимной `Shape`, действует правильное действие, зависящее от фактического типа, связанного с этой `Shape`. Это действительно удивительно, поскольку компилятор Java не знает точного типа, когда он компилирует код для `doStuff()`. Хотя мы можем гарантировать, что будет вызван метод `erase()` для любой `Shape`, но не можем гарантировать, какой конкретный метод будет вызван для `Circle`, `Square` или `Line`. Тем не менее, конечное действие всегда верное. Как это возможно?Когда сообщение отправляется объекту, не зная его конкретного типа, но действие всё равно оказывается правильным, это называется полиморфизмом (polymorphism). Для объектно-ориентированных языков программирования метод, используемый для реализации полиморфизма, называется динамическим связыванием (dynamic binding). Компилятор и система выполнения берут на себя контроль над всеми деталями; нам достаточно знать, что произойдет, и более важно, как использовать это для помощи в проектировании программы.
Некоторые языки требуют использования специального ключевого слова для разрешения динамического связывания. В C++ это ключевое слово — `virtual`. В Java нет необходимости помнить добавление ключевого слова, поскольку динамическое связывание функций происходит автоматически. Поэтому можно быть уверенными, что объект примет правильное действие, даже если это включает преобразование вверх по иерархии.
## 1.6.2 Абстрактные базовые классы и интерфейсыПри проектировании программ мы часто хотим, чтобы базовый класс предоставлял свой собственный интерфейс только для своих производных классов. То есть, нам не хочется, чтобы кто-либо другой создавал объекты от этого базового класса; вместо этого мы хотим, чтобы они могли преобразовать его в производные типы для использования его интерфейса. Для достижения этой цели необходимо сделать этот класс "абстрактным" — используя ключевое слово `abstract`. Если кто-то попытается создать объект от абстрактного класса, компилятор заблокирует это действие. Этот инструмент может эффективно заставить придерживаться специального дизайна.Ключевое слово `abstract` также можно использовать для описания метода, который ещё не реализован — как "корня", указывающего: "Это интерфейсная функция, применимая ко всем типам, которые наследуются от данного класса, но пока она не имеет никакой формы реализации." Абстрактные методы могут быть созданы только внутри абстрактного класса. После того как класс был наследован, этот метод должен быть реализован, иначе наследуемый класс тоже станет "абстрактным". Создание абстрактного метода позволяет поместить метод в интерфейсе без необходимости предоставления возможно бессмысленного тела метода.
Ключевое слово `interface` (интерфейс) продвигает концепцию абстрактного класса ещё на шаг вперёд, полностью запрещая все определения методов. Интерфейс является довольно эффективным и широко используемым инструментом. Кроме того, если вам это нужно, вы можете объединять несколько интерфейсов вместе (невозможно наследовать от нескольких обычных `class` или `abstract class`).
Вы можете оставить комментарий после Вход в систему
Неприемлемый контент может быть отображен здесь и не будет показан на странице. Вы можете проверить и изменить его с помощью соответствующей функции редактирования.
Если вы подтверждаете, что содержание не содержит непристойной лексики/перенаправления на рекламу/насилия/вульгарной порнографии/нарушений/пиратства/ложного/незначительного или незаконного контента, связанного с национальными законами и предписаниями, вы можете нажать «Отправить» для подачи апелляции, и мы обработаем ее как можно скорее.
Опубликовать ( 0 )