Инкапсуляция, Наследование, Полиморфизм

После краткого описания основ ООП в предыдущем моем посте, необходимо рассмотреть три основных столпа ООП: инкапсуляцию, наследование и полиморфизм более подробно. Без знания этих понятий, невозможно изучить полноценно ни один из объектно-ориентированных языков программирования.

Эти столпы программирования довольно тесно взаимосвязаны между собой, но я все же попробую объяснить их по отдельности. Надеюсь что это будет не очень сумбурно, и не собьет с толку новичков, а наоборот поможет им более быстро освоится в мире объектно-ориентированного программирования. Поясняющего текста в данном посте будет не так уж и много, потому что основную часть я вынес в комментарии к коду.


Инкапсуляция.

Инкапсуляция – возможность скрытия реализации каких либо частей модуля или объекта от внешнего мира (от клиента). Это означает, что манипулируя модификаторами доступа, можно скрыть или открыть только определенные свойства, методы или классы для того, чтобы ненужные для класса-клиента данные не были доступны.

Приведу пример инкапсуляции свойств на примере класса автомобиль:

Auto.cs:

using System;

namespace OOPLibrary.Auto
{
	/// <summary>
	/// Класс описывающий свойства автомобиля и его действия
	/// </summary>
	public class Auto
	{
		/// <summary>
		/// Хранит возраст автомобиля. Так как возраст нельзя менять из клиента,
		/// то это свойство объявлено с модификатором private.
		/// </summary>
		private int _age;

		/// <summary>
		/// Хранит признак стоит автомобиль или едет. Свойство используется только внутри класса Auto
		/// и не должно быть доступно в клиентах, потому оно так же объявлено с модификатором private.
		/// </summary>
		private bool _isMoving;

		/// <summary>
		/// Хранит цвет автомобиля. Его можно менять из клиентских классов,
		/// поэтому свойство объявлено с модификатором public.
		/// </summary>
		public string Color;

		/// <summary>
		/// Хранит имя автомобиля, допустим, что его тоже можно менять,
		/// поэтому тоже объявляем с модификатором public.
		/// </summary>
		public string Name;

		/// <summary>
		/// Конструктор класса. Тут задаются начальные свойства автомобиля.
		/// </summary>
		public Auto()
		{
			_age = 5;
			_isMoving = false;
			Color = "Красный";
			Name = "Мой автомобиль";
		}

		/// <summary>
		/// С помощью этого метода можно получить возраст автомобиля,
		/// так как свойство _age недоступно из клиентов.
		/// </summary>
		/// <returns>Возвращает возраст автомобиля.</returns>
		public string GetAge()
		{
			return "Этому автомобилю " + _age + " лет.";
		}

		/// <summary>
		/// Метод стартует автомобиль, если он еще не стартовал.
		/// </summary>
		public void Start()
		{
			if (_isMoving)
			{
				Console.WriteLine("Да и так уже едем.");
			}
			else
			{
				_isMoving = true;
				Console.WriteLine("Поехали...");
			}
		}

		/// <summary>
		/// Метод останавливает автомобиль, если он ехал.
		/// </summary>
		public void Stop()
		{
			if (_isMoving)
			{
				_isMoving = false;
				Console.WriteLine("Остановились.");
			}
			else
			{
				Console.WriteLine("И так уже стоим, куда же дальше?");
			}
		}

		/// <summary>
		/// Если автомобиль едет, то поворачиваем налево.
		/// </summary>
		public void MoveLeft()
		{
			if (_isMoving)
			{
				Console.WriteLine("Поворнули налево.");
			}
			else
			{
				Console.WriteLine("Да куда же поворачивать, мы же стоим.");
			}
		}
		/// <summary>
		/// Если автомобиль едет, то поворачиваем направо.
		/// </summary>
		public void MoveRight()
		{
			if (_isMoving)
			{
				Console.WriteLine("Поворнули направо.");
			}
			else
			{
				Console.WriteLine("Да куда же поворачивать, мы же стоим.");
			}
		}
	}
}

Данный класс, описывает автомобиль, его свойства и методы, причем некоторые свойства (_age и _isMoving) прячет от клиентов данного класса. Это и есть инкапсуляция, то есть данные свойства доступны внутри класса любым его методам и другим свойствам, но не видимы за его пределами, так как клиенты не должны их менять или получать их значения напрямую.

AutoManager.cs:

using System;

namespace OOPLibrary.Auto
{
	/// <summary>
	/// Класс управления автомобилем.
	/// </summary>
	public class AutoManager
	{
		/// <summary>
		/// Менеджер создает объект автомобиля и не дает доступа к нему из других классов,
		/// объявив его при помощи модификатора private.
		/// </summary>
		private Auto _myAuto;

		/// <summary>
		/// Конструктор менеджера автомобилей.
		/// </summary>
		public AutoManager()
		{
			// Создаем новый автомобиль
			_myAuto = new Auto();
		}

		/// <summary>
		/// Метод запускает проверку методов и свойств автомобиля.
		/// </summary>
		public void Run()
		{
			// Выводим имя автомобиля, которое было назначено по умолчанию в конструкторе класса Auto
			Console.WriteLine("Имя автомобиля: " + _myAuto.Name);
			// Выводим цвет автомобиля, которsq был назначен по умолчанию в конструкторе класса Auto
			Console.WriteLine("Цвет автомобиля: " + _myAuto.Color);
			// Выводим возраст автомобиля, который был назначен по умолчанию в конструкторе класса Auto
			Console.WriteLine(_myAuto.GetAge());

			// Если попытаться добраться до свойства _age напрямую, то мы получим ошибку компилятора,
			// так как оно объявлено с модификатором private в классе Auto
			//Console.WriteLine(_myAuto._age);   // Ошибка компилятора
			Console.WriteLine();

			// Меняем имя на новое
			_myAuto.Name = "Новое имя";
			// Меняем цвет автомобиля
			_myAuto.Color = "Синий";

			// Поменять возраст автомобиля не получится, так как он доступен только внутри класса Auto
			//_myAuto._age = 6;   // Ошибка компилятора

			// Выводим новое имя автомобиля
			Console.WriteLine("Новое имя автомобиля: " + _myAuto.Name);
			// Выводим новый цвет автомобиля
			Console.WriteLine("Новый цвет автомобиля: " + _myAuto.Color);
			Console.WriteLine();

			// Поворачиваем налево
			Console.Write("Попытаемся повернуть налево: ");
			_myAuto.MoveLeft();

			// Стартуем автомобиль
			Console.Write("Вспомнили, что автомобиль не заведен, и стартуем его: ");
			_myAuto.Start();

			// Поворачиваем налево
			Console.Write("Поворачиваем налево: ");
			_myAuto.MoveLeft();

			// Поворачиваем направо
			Console.Write("Поворачиваем направо: ");
			_myAuto.MoveRight();

			// Останавливаем автомобиль
			Console.Write("Останавливаем автомобиль: ");
			_myAuto.Stop();
			// Останавливаем автомобиль
			Console.Write("Пробуем еще раз остановить стоящий автомобиль: ");
			_myAuto.Stop();
		}
	}
}

Данный класс, является клиентским классом по отношению к классу Auto, поэтому ему будут доступны только публичные свойства класса Auto.

Program.cs:

using OOPLibrary.Auto;

namespace OOPLibrary
{
	public class Program
	{
		public static void Main()
		{
			// Создаем инстанс объекта менеджера автомобиля
			var autoMng = new AutoManager();

			// К свойству _myAuto класса AutoManager доступ невозможен, так как он объявлен как private.
			// autoMng._myAuto.Start();    // Ошибка компилятора
			// Стартуем менеджер
			autoMng.Run();
		}
	}
}

В основной программе (являющейся клиентом, как для класса Auto, так и для класса AutoManager), доступны оба класса (Auto и AutoManager) так как оба этих класса объявлены с модификатором public, но с данного класса также невозможен доступ к приватным свойствам обоих из вышеприведенных классов.

Результат работы данной программы:

OOPLibOutput

Вышеприведенный код хорошо документирован в комментариях, так что думаю большого смысла объяснять что он делает нету.

Наследование.

Наследование – возможность использовать общий код для всех потомков наследованных от общего родителя. При помощи наследования, можно вынести общие черты каких-то объектов и описать их в отдельном классе, а потом создать потомков от данного класса и в нем добавить свойства и методы присущие, только данному конкретному экземпляру.

К примеру можно описать какой либо абстрактный автомобиль, вынеся его общие свойства (мотор, имя, марку, год выпуска) в общий класс Auto, а потом сделать наследников от него, описывающих грузовой, легковой или спортивный автомобили. Таким образом общие свойства для всех типов автомобилей придется реализовать всего лишь один раз, в классе Auto, а все наследники получат их автоматически и расширят их свойствами и методами доступными только конкретному типу автомобиля.

Наследование очень тесно связанно с полиморфизмом, поэтому, в комментариях к следующим классам, можно увидеть примечания о том какая часть относится к полиморфизму. Саму суть полиморфизма рассмотрим немного ниже в этом посте.

Shape.cs:

namespace OOPLibrary.Shape
{
	/// <summary>
	/// Абстрактный класс фигур, инстанс данного класса создать невозможно,
	/// можно создавать только неабстрактных наследников
	/// </summary>
	public abstract class Shape
	{
		/// <summary>
		/// Свойство бумаги будет доступно во всех наследниках
		/// оно будет общим для всех наследников, поэтому
		/// в наследниках ненужно будет писать один и тот же код
		/// </summary>
		public string Paper { get { return "Белая бумага"; } }

		/// <summary>
		/// Свойство абстрактное, то есть в наследниках его нужно обязательно перекрыть,
		/// то есть описать логику повдеения этого свойства - это и есть полиморфизм
		/// </summary>
		public abstract string Name { get; }
		/// <summary>
		/// Метод абстрактный, то есть в наследниках его нужно обязательно перекрыть,
		/// то есть описать логику повдеения этого метода - это и есть полиморфизм
		/// </summary>
		public abstract void Draw();
	}
}

Класс Shape (Фигура), является абстрактным классом, и содержит внутри себя свойства и методы общие для всех фигур, которые могут быть созданы на основании данного класса (Круг, Прямоугольник, Треугольник и т.д.).

Circle.cs:

using System;

namespace OOPLibrary.Shape
{
	/// <summary>
	/// Класс описывающий круг наследованный от класса фигуры (Shape).
	/// Свойство Paper доступно и в классе Circle, хотя тут оно и не описано.
	/// </summary>
	public class Circle : Shape // наследование
	{
		// Свойства присущие только этому классу, которых не было у родителя
		// При приведении данного типа к базовому классу (Shape), эти свойства
		// будут недоступны. Пример будет показан в классе ShapeManager
		public int X1;
		public int Y1;
		public int Radius;

		// Конструктор для инициализации точек на координатной плоскости
		public Circle()
		{
			// точка центра круга
			X1 = 1; Y1 = 4;
			// радиус
			Radius = 3;
		}

		/// <summary>
		/// Имплементирует свойство из базового класса (наследование и полиморфизм)
		/// </summary>
		public override string Name
		{
			get { return "Круг"; }
		}
		/// <summary>
		/// Имплементирует метод из базового класса (наследование и полиморфизм)
		/// </summary>
		public override void Draw()
		{
			Console.WriteLine(String.Format("Рисуем круг с центром X1,Y1:({0},{1}) и радиусом {2}", X1, Y1, Radius));
		}
	}
}

Класс Circle (Круг) – унаследован от класса Shape, поэтому он получил все свойства и методы присущие в классе Shape. А так же данный класс добавляет свойства, которые присущи только кругу – это координаты центра круга и его радиус.

Rectangle.cs:

using System;

namespace OOPLibrary.Shape
{
	/// <summary>
	/// Класс описывающий прямоугольник наследованный от класса фигуры (Shape).
	/// Свойство Paper доступно и в классе Rectangle, хотя тут оно и не описано.
	/// </summary>
	public class Rectangle : Shape // наследование
	{
		// Свойства присущие только этому классу, которых не было у родителя
		// При приведении данного типа к базовому классу (Shape), эти свойства
		// будут недоступны. Пример будет показан в классе ShapeManager
		public int X1;
		public int Y1;
		public int X2;
		public int Y2;

		// Конструктор для инициализации точек на координатной плоскости
		public Rectangle()
		{
			// точка левого верхнего угла
			X1 = 1; Y1 = 4;
			// точка правого нижнего угла
			X2 = 6; Y2 = 1;
		}

		/// <summary>
		/// Имплементирует свойство из базового класса (наследование и полиморфизм)
		/// </summary>
		public override string Name
		{
			get { return "Прямоугольник"; }
		}
		/// <summary>
		/// Имплементирует метод из базового класса (наследование и полиморфизм)
		/// </summary>
		public override void Draw()
		{
			Console.WriteLine(String.Format("Рисуем прямоугольник X1,Y1:({0},{1}); X2,Y2:({2},{3});", X1, Y1, X2, Y2));
		}
	}
}

Класс Rectangle (Прямоугольник) добавляет к общим свойствам фигуры свои уникальные свойства, такие как координата верхнего левого и нижнего правого угла, на их основании на плоскости можно нарисовать прямоугольник. Если бы понадобилось сделать квадрат, а как известно это особый вид того же прямоугольника, можно было бы унаследовать класс квадрата, не от фигуры, а от прямоугольника, добавив проверку в свойства координат точек, чтобы стороны были равны, тогда дерево классов подвинулось бы еще не одну ветку ниже.

Triangle.cs:

using System;

namespace OOPLibrary.Shape
{
	/// <summary>
	/// Класс описывающий треугольник наследованный от класса фигуры (Shape).
	/// Свойство Paper доступно и в классе Triangle, хотя тут оно и не описано.
	/// </summary>
	public class Triangle : Shape // наследование
	{
		// Свойства присущие только этому классу, которых не было у родителя
		// При приведении данного типа к базовому классу (Shape), эти свойства
		// будут недоступны. Пример будет показан в классе ShapeManager
		public int X1;
		public int Y1;
		public int X2;
		public int Y2;
		public int X3;
		public int Y3;

		// Конструктор для инициализации точек на координатной плоскости
		public Triangle()
		{
			X1 = 1; Y1 = 1;
			X2 = 1; Y2 = 4;
			X3 = 6; Y3 = 1;
		}

		/// <summary>
		/// Имплементирует свойство из базового класса (наследование и полиморфизм)
		/// </summary>
		public override string Name
		{
			get { return "Треугольник"; }
		}
		/// <summary>
		/// Имплементирует метод из базового класса (наследование и полиморфизм)
		/// </summary>
		public override void Draw()
		{
			Console.WriteLine(String.Format("Рисуем треугольник X1,Y1:({0},{1}); X2,Y2:({2},{3}); X3,Y3:({4},{5});", X1, Y1, X2, Y2, X3, Y3));
		}
	}
}

По аналогии с прямоугольником и кругом, создан класс Triangle (Треугольник), он тоже добавляет свои уникальные свойства.

На самом деле, во всех вышеприведенных классов, кроме наследования, показан также и полиморфизм. То есть абстрактные метод Draw и свойство Name были переопределены в каждом из наследников, а переопределение поведения и называется полиморфизмом.

Полиморфизм.

Полиморфизм – возможность подмены некоторых методов или свойств для задания им другого поведения.

В приведенном ниже классе, будут созданы объекты и помещены в коллекцию фигур, а далее при помощи полиморфизма будут вызваны методы конкретного объекта даже не глядя на то что все они помещены в список абстрактных фигур. Правда доступны будут только общие свойства и методы для всех экземпляров.

ShapeManager.cs:

using System;
using System.Collections.Generic;

namespace OOPLibrary.Shape
{
	/// <summary>
	/// Класс управления фигурами.
	/// </summary>
	public class ShapeManager
	{
		private Circle _circle;
		private Rectangle _rectangle;
		private Triangle _triangle;

		private List<Shape> _shapes;

		// Конструктор класса, в нем создаются первоначальные объекты
		public ShapeManager()
		{
			// Создаем объект круг
			_circle = new Circle();
			// Создаем объект прямоугольник
			_rectangle = new Rectangle();
			// Создаем объект треугольник
			_triangle = new Triangle();

			// Невозможно создать объект из абстрактного класса фигуры,
			// но можно создать объекты из его наследников: круга, прямоугольника и треугольника,
			// а также можно создать список и з абстрактных фигур, и положить в него объекты
			// наследников, но при проходе этого списка в цикле будут доступны только
			// общие поля и методы из абстрактоного класса Shape
			//Shape shape = new Shape();    // Ошибка компилятора

			// Создаем список фигур, в него можно положить те объекты,  // которые отнаследованы от класса Shape
			_shapes = new List<Shape>();
			_shapes.Add(_circle);        // добавляем круг в список фигур
			_shapes.Add(_rectangle);     // добавляем прямоугольник в список фигур
			_shapes.Add(_triangle);      // добавляем треугольник в список фигур
		}

		/// <summary>
		/// Метод вывводит информацию о круге
		/// </summary>
		private void ProntCircleInfo()
		{
			Console.WriteLine(_circle.Name);
			Console.WriteLine("X1: " + _circle.X1);
			Console.WriteLine("Y1: " + _circle.Y1);
			Console.WriteLine("Радиус: " + _circle.Radius);
			_circle.Draw();
			Console.WriteLine();
		}

		/// <summary>
		/// Метод вывводит информацию о прямоугольнике
		/// </summary>
		private void PrintRectangleInfo()
		{
			Console.WriteLine(_rectangle.Name);
			Console.WriteLine("X1: " + _rectangle.X1);
			Console.WriteLine("Y1: " + _rectangle.Y1);
			Console.WriteLine("X2: " + _rectangle.X2);
			Console.WriteLine("Y2: " + _rectangle.Y2);
			_rectangle.Draw();
			Console.WriteLine();
		}

		/// <summary>
		/// Метод вывводит информацию о треуголнике
		/// </summary>
		private void PrintTriangleInfo()
		{
			Console.WriteLine(_triangle.Name);
			Console.WriteLine("X1: " + _triangle.X1);
			Console.WriteLine("Y1: " + _triangle.Y1);
			Console.WriteLine("X2: " + _triangle.X2);
			Console.WriteLine("Y2: " + _triangle.Y2);
			Console.WriteLine("X3: " + _triangle.X3);
			Console.WriteLine("Y3: " + _triangle.Y3);
			_triangle.Draw();
			Console.WriteLine();
		}

		/// <summary>
		/// Метод выводит общую информацию для всех наследников,
		/// находящихся в списке фигур
		/// </summary>
		private void PrintCommonShapeInfo()
		{
			foreach (var shape in _shapes)
			{
				// общие свойства доступны для всех фигур (Name и Paper).
				// независимо от того какая конкретная фигура, будет находится
				// в данный моемент в переменно shape доступ к ее полям,
				// например X1, Y1, Radius будут недоступны без предварительного
				// явного приведения к конкретному типу (Circle, Triangle...)
				Console.WriteLine(String.Format("{0} будет нарисован на '{1}'...", shape.Name, shape.Paper));
				// метод доступен для всех фигур, но при помощи полиморфизма,
				// он перекрты в каждом наследнике, таким образом,
				// чтобы каждый объект мог реализовать свой механизм отрисовки самомго себя.
				// поэтому в runtime будет подставлен нужный метод конкретного наследника,
				// несмотря на то, что список хранит абстрактные фигуры
				shape.Draw();
				Console.WriteLine();
			}
		}
		/// <summary>
		/// Метод запускает проверку методов и свойств фигур и их наследников.
		/// </summary>
		public void Run()
		{
			// выводим информацию, доступную для круга
			ProntCircleInfo();
			// выводим информацию, доступную для прямоугольника
			PrintRectangleInfo();
			// выводим информацию, доступную для треугольника
			PrintTriangleInfo();
			// проверяем возможности полиморфизма
			PrintCommonShapeInfo();
		}
	}
}

В комментариях к свойствам и методам довольно хорошо расписано применение и предназначение каждого из членов класса, поэтому не имеет большого смысла углубляться в это более детально.

Program.cs:

using OOPLibrary.Shape;

namespace OOPLibrary
{
	public class Program
	{
		public static void Main()
		{
			// Создаем инстанс объекта менеджера фигур
			var shapeMng = new ShapeManager();
			// Стартуем менеджер
			shapeMng.Run();
		}
	}
}

Результат работы данной программы:

ShapeOutput

Вот так вот вкратце можно описать все три столпа ООП. Ничего сложного и сверхъестественного, нужно только немного опыта, чтобы научится правильно определять свойства и методы реальных классов из реальных задач, но это все приходит с опытом, а тут мы всего лишь рассмотрели основы объектно-ориентированного программирования.

  1. Трэкбеков пока нет.
  1. abnata
    3 сентября 2013 8:06 | №1

    очень хорошо.

  2. #2
    26 января 2014 14:22 | №2

    Спасибо. Очень понятно и доступно написано.

  3. недовольный
    25 мая 2014 3:24 | №3

    архитектура страницы ужасно сделана

    из-за ширины основного блока в 600 пикс пример кода почти не читаем

    постоянно надо скролить в сторону что бы увидеть комментарий кода

    неудивительно что сайт не популярен

    прошу задумайтесь о том что сайт в первую очередь должен быть удобным

    а потом уже красивым

  4. Марк
    28 мая 2014 15:14 | №4

    Большое спасибо за этот понятный пример. Очень понятный, и доходчивый.

  5. 28 мая 2014 15:45 | №5

    не за что, рад что помогает :)

  6. Chenod
    23 января 2015 0:55 | №6

    Автор! Огромное спасибо за объяснение и код, но в нем я лично нашел 2 ошибки: В первом коде вместо class Auto () написано public Auto (), что является довольно грубой ошибкой, и опечатка, которую из-за слипшихся глаз, я не могу найти заново, уж не серчайте. А так статья очень хорошая!

  7. Chenod
    23 января 2015 1:06 | №7

    Прошу прощения с public натупил, там не должно быть class

  8. 23 января 2015 10:32 | №8

    это конструктор, там не должно быть слова class :)

    в любом случае спасибо за вдумчивое чтение и комментарий

:D :) ^_^ :( :o 8) ;-( :lol: xD :wink: :evil: :p :whistle: :woot: :sleep: =] :sick: :straight: :ninja: :love: :kiss: :angel: :bandit: :alien: