Ранее рассмотренные классы демонстрировали способность объединять в себе несколько переменных различных встроенных примитивных типов (int, double, char, bool, string). Это позволяет успешно моделировать объекты, которые в процессе своего функционирования слабо взаимодействуют с другими объектами. Однако в большинстве систем именно такие взаимодействия и представляют наибольший интерес.
В языке UML сделана попытка классифицировать типы связей между объектами. Такая классификация существенно помогает в описании больших программных систем. Кроме того, существуют стандартные приемы реализации того или иного вида связи.
Описание
Обозначение
Ассоциация
объект связан с другими объектами (знает об их существовании). Автомобиль – водитель. Человек – супруг.
Композиция
объект (обязательно) состоит из других объектов (подобъектов) Подобъекты не могут существовать без объекта. Человек – сердце. Книга – автор.
Агрегация
объект (обязательно) состоит из других объектов (подобъектов). Подобъекты могут существовать самостоятельно или находиться в агрегации с другими объектами.
Таким образом, ассоциация – наиболее «слабый» тип связи, в наименьшей степени регламентирующий особенности связи.
Числа на концах линий связей называются кратностями связи.
Для реализации всех типов связей нужно в одном классе разместить переменную, ссылающуюся на объект (объекты) другого класса.
Для реализации ассоциации следует предусмотреть метод присваивания этой переменной ссылки на объект и метод, прерывающий (обнуляющий) эту связь. В конструкторе эту связь устанавливать не нужно.
Для реализации композиции следует в конструкторе класса создать объект и присвоить ссылку на него переменной. Открытый доступ к этому объекту реализовать только по чтению.
Для реализации агрегации следует в конструктор класса передать готовый объект и присвоить ссылку на него переменной. Реализовать также метод присваивания этой переменной ссылки на другой объект. Важно гарантировать невозможность присваивания этой переменной значения null.
Наследование (Inheritance)
Наследование – второй важнейший принцип ООП (после инкапсуляции). Он заключается в создании новых классов, расширяющих возможности уже имеющихся. Допустим, к этому моменту Вы располагаете достаточно функциональным классом Person, позволяющим успешно программно моделировать различные ситуации из мира людей. На следующем этапе Вы поняли, что многие последующие задачи будут использовать в качестве объектов студентов. Естественно, следует разработать класс Student. Однако понимание того что «студент является человеком» (то есть «человек» - общее понятие, а «студент» - частное), подсказывает, что создавать класс Student опять «с нуля» не разумно. Некоторую часть информации и возможностей студент «наследует» у человека. Существует два способа реализации наследования: а) классическое (реализует отношение «is_a») и б) модель делегирования внутреннему члену (отношение «has-a»). Наследование обеспечивает возможность повторного использования программного кода.
В дальнейших примерах будем использовать несколько измененный класс Person:
class Person
{ private string name; //protected!!!
private List<Person> acq; // список знакомых
public Person(string n)
{ name = n; acq = new List<Person>(); }
public string Name { get {return name;}}
public void GetAcq(Person p) // познакомиться
{ if (!acq.Contains(p)) acq.Add(p); }
public void UnGetAcq(Person p) //разорвать знакомство
{ if (acq.Contains(p)) acq.Remove(p); }
public string Greeting(Person p)
{ if (acq.Contains(p))
return String.Format("Hi, {0}!", p.Name);
else return String.Format("Hello, Mr. {0}!", p.Name);
}
}
Такой класс Person кроме имени, снабжает каждого человека множеством знакомых, и соответствующими возможностями знакомиться и прерывать знакомство. Заметим, что реализовано не обоюдное знакомство (поробуйте исправить это самостоятельно). Поведение класса Person можно протестировать следующим образом:
Person p1 = new Person("John");
Person p2 = new Person("Ann");
Console.WriteLine(p1.Greeting(p2));
p1.GetAcq(p2);
Console.WriteLine(p1.Greeting(p2));
Console.WriteLine(p2.Greeting(p1));
p1.UnGetAcq(p2);
Console.WriteLine(p1.Greeting(p2));
Допустим, Вам нужно реализовать программу, моделирующую некоторые черты поведения студентов. На данный момент класс Person мало приспособлен для реализации таких задач. Реализуем следующий класс Student:
public void GetCourse(String c) //студент выбирает курс
{ if (!courses.Contains(c)) courses.Add(c); }
public string SayCourses()
{ string s =
String.Format("{0}.изучает следующие курсы:\n",Name);
foreach(String c in courses) s+=c+'\n';
return s;
}
}
Самое важное здесь находится в заголовке класса.
class Student:Person
Такая конструкция обозначает, что класс Student наследует от класса Person все его переменные и методы (кроме конструктора!). Таким образом в классе Student кроме переменных year и courses неявно присутствуют переменные name и acq. Аналогично, кроме методов GetCourse и SayCourses, в классе неявно присутствуют методы GetAcq, UnGetAcq, Greeting и свойство Name.
Как уже было сказано, конструкторы не наследуются. Поэтому у класса Student только один конструктор.
Класс Person
Класс Student
Унаследованные элементы
Собственные элементы
Переменные
name
name
year
acq
acq
cources
Методы
Конструктор Person
Конструктор Student
Свойство Name
Свойство Name
Метод GetAcq
Метод GetAcq
Метод GetCourse
Метод UnGetAcq
Метод UnGetAcq
Метод SayCourses
Метод Greeting
Метод Greeting
Отношение наследования описывается несколькими синонимичными терминами. Когда класс B является наследником класса A, то класс A называют базовым, а класс B - производным. Иногда используют другие термины: предок и потомок или суперкласс и подкласс.
Теперь главный вопрос – зачем нужно наследование? По мере Вашего программистского опыта Вы будете находить все новые ответы на этот вопрос. Сейчас ограничимся следующими:
1. Наследование увеличивает степень повторного использования программного кода. Очевидно, что текст класса Student выглядит довольно компактно, по сравнению с его действительным содержимым.
2. Наследование способствует конструированию программного кода путем использования абстракции и специализации. Многие мыслительные процессы существенно опираются на оперирование общими и частными понятиями. Это помогает описывать мир на естественном языке. В программировании такая наследование позволяет сохранить такой стиль мышления, а следовательно и программного моделирования.
3. Наследование является основой полиморфизма. Объяснение этой причины будет дано несколько позже.
Пример Main, где используются базовые и производные возможности.
1 Person p1 = new Person("John");
2 Student s1 = new Student("Vasya", 2);
3 Student s2 = new Student("Kolya", 2);
4 s1.GetAcq(s2);
5 s1.GetAcq(p1);
6 s1.GetCourse("ООП");
7 s1.GetCourse("БД");
8 s1.SayCourses();
9 p1.GetAcq(s1);
В строке 4 объектом s1 вызывается унаследованный метод GetAcq. Тип его формального параметра – Person. Однако в качестве фактического значения передается переменная типа Student. Это не является ошибкой. Здесь действует следующее правило совместимости типов:
Переменным базового класса можно присваивать ссылки на объекты производных классов.
Это правило имеет интуитивно понятное объяснение – студент является частным случаем человека и, поэтому, для него допустимо то, что допустимо для человека.