Повторное
использование
классов.
Повторное
использование
кода (англ.
code reuse) —
методология
проектирования
компьютерных
и других
систем. Она
заключающаяся
в том, что
система
(компьютерная
программа,
программный
модуль)
частично
либо
полностью должна
составляться
из частей,
написанных
ранее компонентов
и/или частей
другой
системы, и эти
компоненты
должны
применяться
более одного
раза (если не
в рамках
одного
проекта, то
хотя бы
разных).
Повторное
использование
— основная
методология,
которая
применяется
для
сокращения
трудозатрат
при разработке
сложных
систем.
Суть
идеи, которую
обозначают
словом reuse,
можно
выразить
следующим
образом:
При решении задач используйте существующий стандартный набор инструментов и предоставляйте результаты своего труда в виде максимально общего набора инструментов, оформленного в соответствии со стандартами.
То
есть в виде
простых, но
достаточно
мощных и
общих
функций
(классов),
снабженных
документацией,
с простыми
очевидными
примерами
использования
и
обозначенными
путями интеграции
с другими
инструментами
(системами).
Самый
распространённый
случай
повторного
использования
кода — библиотеки
программ.
Библиотеки
предоставляют
общую
достаточно
универсальную
функциональность,
покрывающую
избранную
предметную
область. Примеры:
библиотека
функций для
работы с
комплексными
числами,
библиотека
функций для
работы с
3D-графикой,
библиотека
для
использования
протокола TCP/IP,
библиотека
для работы с
базами
данных.
Разработчики
новой
программы могут
использовать
существующие
библиотеки для
решения
своих задач и
не
«изобретать
велосипеды».
Одной
из наиболее
притягательных
возможностей
языка Java
является
возможность
повторного
использования
кода. Но что
действительно
"революционно",
так это
наличие
возможности
выполнять не
только
простое копирование
и изменение
этого кода.
Композиция
— это
объединение
объектов,
которые
общаются
между собой с
помощью
только
внешних интерфейсов
(black box reuse). Друг для
друга
объекты
являются
«чёрными ящиками».
Синтаксис
композиции:
До сих пор,
композиция
достаточно
часто
использовалась,
Вы просто
помещали
ссылку на
объект внутрь
нового
класса.
Для
примера,
представьте
себе, что Вы
хотите
получить
объект,
который
хранит
различные
объекты типа String, пару
примитивных
типов и
объект
другого класса.
Для не
примитивных
объектов Вы
помещаете
ссылки
внутри
вашего
класса, но
примитивные
типы Вы
определяете
напрямую.
Наследование
(inheritance)
— это
формирование
подкласса,
который наследует
практически
все поля и
методы
родительского
класса. Не
наследуются:
·
конструкторы
·
статические
члены
Следует
иметь ввиду,
что не все
наследованные
члены
родительского
класса видны
в методах
дочериного
класса.
В
дочерином
классе можно:
·
добавлять
в класс новые
поля и методы
·
перегружать
(overload) наследованные
методы
·
переопределять
(override) наследованные
методы
Наследование
часто
считают
одним из
основных
принципов
ООП.
Синтаксис
наследования:
Наследование
является
неотъемлемой
частью Java,
впрочем, как
и других ОО
языков
программирования.
Вы
всегда
осуществляете
операцию
наследования,
когда
создаете
класс, даже
если ваш
класс не
является
наследником
какого-либо
другого,
потому, что
Вы неявно
наследуете
стандартный
корневой
класс Java Object.
Когда
Вы
наследуете,
Вы
"говорите": "Этот
класс такой
же, как тот
старый
класс!"
Вы излагаете
эту фразу в
коде давая
классу имя,
как обычно,
но до того,
как начнете
работать с
телом класса,
добавляете
ключевое
слово extends
следующее до
имени
базового
класса. Когда
вы сделаете
это, вы
автоматически
получите все
поля данных и
методы
базового
класса.
Упражнение 1.
Попробуйте
практически
доказать
справедливость
вышеперечисленных
утверждений.
Внутри
каждого
объекта
производного
типа имеется
подобъект
базового
типа (наследованная
часть!). Если
this –
указатель на
«текущий»
объект, то super
- указатель
на
наследованный
подобъект этого
дочериного
объекта.
Инициализация
этого
подобъекта
происходит
до
инициализации
объекта
производного
типа. Java
автоматически
вставляет
вызов
конструктора
базового
класса в
конструктор
производного
класса.
Упражнение 2.
Доказать,
что
инициализация
каждого
объекта
происходит в
следующей
последовательности:
1) статичные
поля
2) наследованный
подобъект
3) не
статичные
поля
4) конструктор
Конструктор
с
аргументами:
Если Вы
хотите
вызвать
конструктор
базового
класса,
который
имеет аргументы,
Вы должны
просто
использовать
ключевое
слово super и
передать ему
список
аргументов.
Упражнение 3.
Объединение
композиции и
наследования:
Совместное
использование
композиции и
наследования
часто и
широко
используется
при
программировании.
Выборочная
композиция
против
наследования.
Композиция
в основном
используется,
когда Вам
нужно
использовать
возможности
существующего
класса, но не
использовать
его
интерфейс.
Это
значит, что
Вы внедряете
объект, так,
что вы можете
использовать
его для
получения
доступа к
функциональности
внедряемого
объекта в
вашем новом
классе, но
пользователь
вашего
нового
класса видит
интерфейс
вашего
нового
класса
раньше, чем
интерфейс
внедряемого
объекта.
Что
бы добиться
такого
эффекта, Вы
должны включать
private
объекты
существующих
классов
внутрь вашего
нового
класса.
Иногда
требуется
разрешить
пользователю
класса
получить
доступ к
вашему
новому
классу
напрямую; что
бы сделать
это, нужно
сделать
объекты public.
Эти объекты
используют
реализацию
скрытия
самих себя,
так что такой
подход
достаточно
безопасен.
Если
пользователь
знает, что Вы
собрали этот
класс из
различных
частей, то интерфейс
этого класса
будет для
него более
легок в
понимании.
Пример.
При
наследовании,
Вы берете
существующий
класс и
создаете
специальную
его версию.
В
основном это
означает, что
Вы берете
главный,
целевой
класс и
приспосабливаете
его для
частных нужд.
Немного
поразмыслив,
Вы увидите,
что нет
разницы при
создании
класса car
используя
объект vehicle - car не
содержит vehicle,
он и есть vehicle.
Отсюда
связь он и
есть
используется
в наследовании,
а содержит
при
композиции.
В
идеальном
мире, private объекты
всегда
являются
действительно
private,
но в реальных
проектах, где
вы пытаетесь
во многих
местах
скрыть от
внешнего
мира нечто,
Вам часто
нужна
возможность
получить к
нему доступ
из классов
наследников.
Ключевое
слово protected
поэтому не
такая уж, и
ненужная
назойливость
или догма.
Оно
объявляет
"Этот объект
частный (private),
если к нему
пытается
подобраться
пользователь,
но он
доступен для
всех
остальных находящихся
в том же
самом пакете (package)". То есть,
protected
в Java
автоматически
означает friendly.
В Java
ключевое
слово final имеет
слегка
разные
значения в
зависимости
от контекста,
но в основном,
оно
определяется
так "Это не
может быть
изменено". Вы
можете
хотеть
запретить
изменения по
двум
причинам:
дизайн или эффективность.
Поскольку
эти две
причины
слегка различаются,
то
существует
возможность
неправильного
употребления
ключевого
слова final.
При
использовании
final с
объектами, а
не с
примитивными
типами получается
несколько не
тот эффект. С
примитивами, final создает
константу
значения, а с
объектами -
ссылку, final
создает
ссылку -
константу.
Как только
ссылка
инициализируется
на какой-то
объект, она
уже не может
быть
впоследствии
перенаправлена
на другой
объект.
Однако сам
объект может
быть
модифицирован;
Java не
предоставляет
способа
создать
объект - константу.
(Однако,
Вы можете
написать
свой
собственный
класс с
эффектом
константы.)
Эти же
ограничения накладываются
и на массивы,
поскольку
они тоже
объекты.
Пример.
Java позволяет
создавать
пустые
(чистые) final
объекты (blank final), это
такие поля
данных,
которые были
объявлены
как final
но при этом
не были
инициализированы
значением. Во
всех случаях,
пустая final
переменная должна
быть
инициализирована
до ее
использования,
и компилятор
обеспечивает
это условие.
Тем не менее,
пустые final
поля
предоставляют
большую
гибкость при использовании
модификатора
final, к
примеру, final
поле внутри
класса может
быть разным
для каждой
копии
объекта.
Пример.
Java
позволяет Вам
так же
создавать и
аргументы final
определением
их таким
образом
прямо в
списке
аргументов. Это
означает, что
внутри
метода Вы не
сможете
изменить
этот
аргумент или
его ссылку.
Существует
две причины
для final
методов.
Первая -
закрытие
методов, от
возможной
модификации
при наследовании
класса. Такой
подход применяется
если Вы
хотите быть
уверенны, что
этот метод не
будет
переопределен
в дочерних
классах и
поведение
класса не
изменится.
Вторая
причина - final
методы более
эффективны.
Если Вы делаете
метод с
модификатором
final, Вы тем
самым
разрешаете
компилятору
все вызовы
этого метода
превратить
во внутренние
(inline)
вызовы. Но,
все-таки не
следует
слишком уж доверять
компилятору
и создавать final методы,
только, если
они
действительно
небольшие и
Вы
действительно
хотите
запретить их
изменение
при
наследовании.
Любой private
метод
косвенным
образом final.
Поскольку Вы
не можете
получить
доступ к private
методу, Вы не
можете
переопределить
его (даже
если
компилятор
не выдаст сообщения
об ошибке при
переопределении,
Вы все равно
не сможете
переопределить
его, Вы
просто
создадите
новый метод).
Вы можете добавить
спецификатор
final к private
методу, но
это не
добавит ему
никаких
дополнительных
возможностей.
Когда Вы объявляете
целый класс final (путем
добавления в
его
определение
ключевого
слова final),
Вы тем самым
заявляете,
что не хотите
наследовать
от этого
класса или
что бы кто-то
другой мог
наследовать
от него.
Другими словами,
по некоторым
причинам в
вашем классе
не должны
делаться
какие-либо
изменения, или
по причинам
безопасности
не могут быть
созданы
подклассы. В
другом же
случае, причиной
сделать
класс final
может
послужить
эффективность
выполнения
кода класса,
но здесь
нужно быть
уверенным,
что все, что
внутри
класса уже
оптимизировано
как можно максимально.
Оба
метода, и
наследование
и композиция
позволяют
Вам создать
новый тип из
уже существующего
типа. Обычно,
Вы
используете композицию
для
повторного
использования
существующих
типов как
части имплементации
нового типа,
а наследование
когда Вам
необходимо
повторно
использовать
интерфейс. В
силу того,
что дочерний
класс имеет
интерфейс
базового
класса, он
может быть
приведен к
базовому
типу.
Относитесь
с
осторожностью
к наследованию
в
объектно-ориентированном
программировании,
когда Вы
начинаете
разработку
нового
проекта
используйте
лучше
композицию, а
после того,
как ваш код
будет
доведен до
совершенства,
измените
композицию
на
наследование,
если это конечно
необходимо. Композиция
имеет
тенденцию к
большей
гибкости. Вы
так же можете
изменять
поведение
объектов с
композицией
во время
исполнения.
Повторное
использование
композиции и
наследования
оказывает
огромную
помощь для
быстрой
разработки
проектов, Вы
обычно хотите
изменить
иерархию
ваших
классов, до
того, как
другие
программисты
станут работать
с вашим
проектом и с
вашими
классами. Вашим
преимуществом
при этом
будет наследование,
где каждый
класс не
большой по
размеру (но и
не слишком
маленький, что бы не
потерять
функциональность)
и выполняет узкую
задачу.
Наиболее
важный аспект наследования
заключается
вовсе не в снабжении
нового класса новыми методами.
А заключается
он в отношении
между новым классом
и базовым
классом. Данное отношение
можно определить
так "Новый
класс имеет тип
существующего
класса."
Это описание, не просто
причудливая
форма раскрытия
сущности
наследования,
такая форма поддерживается
напрямую
языком Java.
В примере,
рассматриваемый
базовый класс называется
Instrument и представляет
музыкальные
инструменты,
а дочерний
класс называется
Wind (духовые
инструменты).
Поскольку
наследование
подразумевает,
что все
методы в базовом классе так же
доступны
и в дочернем
классе, то любое
сообщение,
которое может быть
послано базовому классу, так же
доступно
и в дочернем.
Если класс Instrument имеет
метод play( ), то
и Wind
так же
может его использовать.
Это означает,
что мы
можем точно так
же сказать,
что объект
Wind так же
и типа Instrument. Следующий
пример показывает,
как компилятор
поддерживает
это высказывание:
//: c06:Wind.java
// Наследование
и приведение
к базовому
типу.
import java.util.*;
class Instrument
{
public void play() {}
static void tune(Instrument
i) {
// ...
i.play();
}
}
// Объект
Wind так же Instrument
// потому
что они
имеют общий интерфейс:
class Wind extends
Instrument {
public static void main(String[] args) {
Wind flute = new Wind();
Instrument.tune(flute); // Upcasting
}
} ///:~
Что действительно
интересно
в этом примере, так это
то, что
метод tune( ) поддерживает
ссылку на Instrument.
Однако, в Wind.main( ) метод
tune( ) вызывается
с передачей
ссылки на Wind.
Из этого
следует, что Java специфична
с проверкой
типов, это выглядит
достаточно
странно, если метод
принимающий
в качестве
параметра
один тип,
вдруг спокойно принимает
другой, но так
пока вы
не поймете,
что объект
Wind так же
является
и объектом
типа Instrument, и в нем
нет метода
tune( ) который
можно было бы
вызвать для Instrument.
Внутри
tune( ), код
работает
с типами Instrument и с чем угодно
от него
произошедшим,
а факт конвертации
ссылки на Wind в ссылку
на Instrument
называется
приведением к базовому
типу (upcasting).
Вы так
же можете
осуществить
обратную
приведению
к базовому
типу операцию,
называемую
приведение
базового
типа к дочернему
(downcasting).
Wind flute = new
Wind();
Instrument i = flute; // upcasting
Пример
// Перегрузка,
вместо приведедния
к базовому
типу.
class Note {
private int value;
private Note(int val) { value = val; }
public static final Note
MIDDLE_C = new
Note(0),
C_SHARP = new
Note(1),
B_FLAT = new
Note(2);
} // И
т.д.
class Instrument
{
public void play(Note n) {
System.out.println("Instrument.play()");
}
}
class Wind extends
Instrument {
public void play(Note n) {
System.out.println("Wind.play()");
}
}
class Stringed extends
Instrument {
public void play(Note n) {
System.out.println("Stringed.play()");
}
}
class Brass extends Instrument {
public void play(Note n) {
System.out.println("Brass.play()");
}
}
public class Music2 {
public static void tune(Wind i) {
i.play(Note.MIDDLE_C);
}
public static void tune(Stringed
i) {
i.play(Note.MIDDLE_C);
}
public static void tune(Brass i) {
i.play(Note.MIDDLE_C);
}
public static void main(String[] args) {
Wind flute = new Wind();
Stringed violin = new Stringed();
Brass frenchHorn = new Brass();
tune(flute); // Раннее связывание!!!
tune(violin);
tune(frenchHorn);
}
} ///:~
Ура, работает,
но при
этом возникает
большая работа
по переписки
кода: Вы
должны писать типо-зависимые
методы, для каждого
нового класса Instrument, которые
Вы добавите.
А это означает,
что во-первых
нужно больше программировать,
во-вторых,
если Вы
захотите
добавить
новый метод по
типу tune( ) или просто новый тип
инструмента,
то придется
проделать
много работы.
А не было
бы намного
лучше, если бы
Вы написали
один метод,
который получает
в качестве
аргумента
базовый
класс, а не каждый
по отдельности
дочерний
класс?
Было бы,
но не
было бы
хорошо, если бы
Вы смогли
забыть, что есть
какие-то дочерние
классы и написали бы ваш
код только
для базового
класса?
Именно
это полиморфизм
и позволяет
делать. Но все
равно, многие программисты
пришедшие
из процедурного
программирования
имеют небольшие
проблемы
при работе
с полиморфизмом.