Объект имеет свойства и поведение, которые инкапсулированы (скрыты) у него внутри. Сервисы, которые он предоставляет своим клиентам, составляют его контракт. Только контракт, определенный объектом, доступен клиентам. Реализация свойств и поведения объекта клиентов не касается. Инкапсуляция помогает провести различие между контрактом объекта и реализацией. Это имеет большое значение для разработки программ. Реализацию объекта можно изменить, не затрагивая клиентов. Инкапсуляция также уменьшает сложность, так как внутреннее содержание объекта скрыто от клиентов, которые не могут оказать влияние на реализацию.
На рис.6.6 приведена диаграмма классов UML, демонстрирующая несколько агрегативных связей и одно отношение наследования. Диаграмма классов показывает структуру определяемую при помощи агрегации, и стек, определяемый при помощи наследования. И то и другое основывается на связных списках. Цель примера заключается в том, чтобы продемонстрировать наследование и агрегацию, а не рабочую реализацию очередей и стеков. В классе Node
в строке (1) определены два поля: одно, обозначающее данные, и другое, обозначающее следующий узел из списка. Класс LinkedList
в строке (2) управляет списком с помощью ссылок head
и tail
. Узлы можно добавить как в начало, так и в конец списка, но удалить можно только с начала списка.
class Node { // (1)
private Object data; // Данные
private Node next; // Следующий узел
// Конструктор для инициализации данных и ссылки на следующий узел.
Node(Object data, Node next) {
this.data = data;
this.next = next;
}
// Методы
public void setData(Object obj) { data = obj; }
public Object getData() { return data; }
public void setNext(Node node) { next = node; }
public Node getNext() { return next; }
}
class LinkedList { // (2)
protected Node head = null;
protected Node tail = null;
// Методы
public boolean isEmpty() { return head == null; }
public void insertInFront(Object dataObj) {
if (isEmpty()) head = tail = new Node(dataObj, null);
else head = new Node(dataObj, head);
}
public void insertAtBack(Object dataObj) {
if (isEmpty())
head = tail = new Node(dataObj, null);
else {
tail.setNext(new Node(dataObj, null));
tail = tail.getNext();
}
}
public Object deleteFromFront() {
if (isEmpty()) return null;
Node removed = head;
if (head == tail) head = tail = null;
else head = head.getNext();
return removed.getData();
}
}
class QueueByAggregation { // (3)
private LinkedList qList;
// Конструктор
QueueByAggregation() {
qList = new LinkedList();
}
// Методы
public boolean isEmpty() { return qList.isEmpty(); }
public void enqueue(Object item) { qList.insertAtBack(item); }
public Object dequeue() {
if (qList.isEmpty()) return null;
else return qList.deleteFromFront();
}
public Object peek() {
return (qList.isEmpty() ? null : qList.head.getData());
}
}
class StackByInheritance extends LinkedList { // (4)
public void push(Object item) { insertInFront(item); }
public Object pop() {
if (isEmpty()) return null;
else return deleteFromFront();
}
public Object peek() {
return (isEmpty() ? null : head.getData());
}
}
public class Client { // (5)
public static void main(String[] args) {
String string1 = "Queues are boring to stand in!";
int length1 = string1.length();
QueueByAggregation queue = new QueueByAggregation();
for (int i = 0; i < length1; i++)
queue.enqueue(new Character(string1.charAt(i)));
while (!queue.isEmpty())
System.out.print((Character) queue.dequeue());
System.out.println();
String string2 = "!no tis ot nuf era skcatS";
int length2 = string2.length();
StackByInheritance stack = new StackByInheritance();
for (int i = 0; i < length2; i++)
stack.push(new Character(string2.charAt(i)));
stack.insertAtBack(new Character('!')); // (6)
while (!stack.isEmpty())
System.out.print((Character) stack.pop());
System.out.println();
}
}
Вывод программы:
Queues are boring to stand in!
Stacks are fun to sit on!!
Очень важно на этапе проектирования при моделировании взаимосвязей сделать выбор между наследованием и агрегацией. Вообще, рекомендуется использовать наследование, если отношение есть (is-a) явно поддерживается всеми вовлеченными объектами на протяжении их жизненного цикла; в других случаях лучшим выбором является агрегация. Роль часто путают с отношением есть. Например, пусть дан класс Employee
, лучшей идеей является применение наследования от этого класса для моделирования, которые могут выполнять сотрудники (например, менеджер или кассир), если эти периодически изменяются. Изменяющиеся роли следует рассмотреть как новый объект, представляющий новую роль каждый раз, когда это происходит.
Повторное использование кода также лучше достигается с помощью агрегации, когда нет связи есть. Поддержка искусственно созданного отношения есть обычно не лучший вариант. Это показано в примере 6.15 в строке (6). Поскольку класс StackByInheritance
из строки (4) является подклассом класса LinkedList
из строки (2), то любой унаследованный метод суперкласса можно вызвать у экземпляра подкласса. Также можно вызвать методы, которые противоречат абстракции, поливаемой подклассом, что показано в строке (6). Применение агрегации в такой ситуации приводит к лучшему решению, как показано в классе QueueByAggregation
в строке (3). Класс описывает операции очереди при помощи делегирования запросов нижележащему классу LinkedList
. Клиенты, реализующие очередь таким способом, не могут получить доступ к нижележащим классам, поэтому не могут нарушить абстракцию.
И наследование, и агрегация способствуют инкапсуляции, так как изменения реализации локализованы в классе. Изменение контракта суперкласса может отразится на подклассах (называется волновым эффектом) и также на клиентах, которые зависят определенного поведения подклассов.
Полиморфизм достигается через наследование и реализацию интерфейса. Код, построенный на полиморфном поведении, будет по-прежнему работать без изменения, если будут добавлены новые подклассы или новые классы, реализующие интерфейс. Если отношение общее-частное не очевидно, то полиморфизм лучше достигается через агрегацию и интерфейсную реализацию.
Полиморфизм и динамический поиск метода | Объектно-ориентированное программирование |