пятница, 10 июля 2015 г.

Агрегация и композиция

Агрегация

Иногда в рамках одного класса нам необходимо создать один или несколько экземпляров другого класса (других классов). Например, если мы проектируем класс «Дом», то внутри экземпляра этого класса вполне могут существовать экземпляры классов «Стена», «Окно», «Дверь». Для реализации такой конструкции используется понятие «Агрегация». Т.е., агрегация – это вхождение экземпляра/ов одного класса в экземпляры другого класса в качестве полей.
    /// 
    /// Стена
    /// 
    public class Wall { }

    /// 
    /// Окно
    /// 
    public class Window { }

    /// 
    /// Дверь
    /// 
    public class Door { }

    /// 
    /// Дом
    /// 
    public class House
    {
        protected Wall _wall1;
        protected Wall _wall2;
        protected Wall _wall3;
        protected Wall _wall4;
        protected Window _window;
        protected Door _door;
    }

Композиция

Композиция – это более сложный случай агрегации. Отличие композиции от агрегации в том, что в случае композиции класс-контейнер не может обойтись (не имеет смысла) без экземпляров класса-содержимого. В описанном выше примере дом не может существовать без стен. Это композиция. Но, дом спокойно может обойтись без печной трубы (например, если в нём электрическое отопление) – это агрегация.

Агрегация массивов

Поля класса контейнера могут агрегировать не только одиночные экземпляры другого класса, но и массивы (любые коллекции, но об этом позже) этих экземпляров. Для этого надо создать поле типа «массив» и использовать его в дальнейшем.
    /// 
    /// Дом
    /// 
    public class House
    {
        /// 
        /// Массив стен дома
        /// 
        protected Wall[] _walls;

        /// 
        /// Массив окон дома
        /// 
        protected Window _windows;
        
        /// 
        /// А дверь в нарушение пожарной безопасности - одна
        /// 
        protected Door _door;
    }

Использование конструкторов

Если в классе-контейнере мы используем экземпляр другого класса, то перед использованием этот экземпляр надо создать. Для этого мы используем конструктор класса-содержимого. Если экземпляр класса-содержимого не создавать, то при попытке его использовать мы получим Exception.
        /// 
        /// Конструктор - инициализирует агрегированные поля класса
        /// 
        public House()
        {
            _walls = new Wall[4]
            {
                new Wall(), new Wall(), new Wall(), new Wall()
            };
            _windows = new Window[4] { new Window(), new Window(), new Window(), new Window() };
            _door = new Door();
        }

Агрегация или наследование?

В некоторых случаях рекомендуется использовать агрегацию вместо наследования для расширения функциональности класса. Так, скажем, если в классе А нам надо реализовать функциональность классов B и C, то нам будет затруднительно выбрать класс-предок для А. Логичным решением будет создание в классе А полей типа В и С. Впрочем, правильный подход к решению подобных задач мы рассмотрим позже.
    /// 
    /// Класс камеры
    /// 
    public class Camera
    {
        /// 
        /// Сфотографировать
        /// 
        public void Photo() { }
    }

    /// 
    /// Класс телефона
    /// 
    public class Phone
    {
        /// 
        /// Позвонить
        /// 
        public void Call() { }
    }


    /// 
    /// Смартфон
    /// 
    public class Smartphone
    {
        /// 
        /// Встроенная камера
        /// 
        protected Camera _camera;
        /// 
        /// Встроенный телефон
        /// 
        protected Phone _phone;

        /// 
        /// Конструктор
        /// 
        public Smartphone()
        {
            //Инициализация агрегированных полей 
            _camera = new Camera();
            _phone = new Phone();
        }

        /// 
        /// Сфотографировать
        /// 
        public void Photo() { _camera.Photo(); }

        /// 
        /// Позвонить
        /// 
        public void Call() { _phone.Call(); }
    }

Заключение


Агрегация и композиция позволяют включать экземпляры одного класса в экземпляры другого класса. Композиция отличается от агрегации тем, что в композиции экземпляр класса-контейнера не может существовать без экземпляра класса-содержимого.

Полиморфизм

Понятие полиморфизма

Под понятием «полиморфизм» в объектно-ориентированном программировании понимают возможность методов с одинаковым названием иметь разную реализацию (т.е. разные алгоритмы работы). Применение полиморфизма позволяет с одной стороны сделать интуитивно понятным интерфейс класса, а с другой – даёт возможность наделять класс-наследник отличной от класса-предка функциональностью.

Различают полиморфизм двух видов: внутри одного и того же класса (перегрузка методов класса) и между классом предком и классом наследником (переопределение и сокрытие методов). В данном посте мы рассмотрим различные возможные варианты полиморфизма методов и способы его применения.

Перегрузка методов

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

Легче всего понять принцип работы перегрузки методов на примере. Рассмотрим следующую ситуацию. Нам необходимо создать класс, который будет реализовывать регистрацию юридического лица. Юридические лица, рассматриваемые в системе, будут двух типов: организация и индивидуальный предприниматель. Для ООО параметрами метода будут: название организации, ФИО руководителя, ИНН, КПП, юридический адрес. У ИП остаётся ИНН, но отсутствует такой параметр как «КПП», отсутствует название организации, вместо ФИО руководителя – ФИО самого ИП, вместо юридического адреса – адрес прописки.

Названия параметров для перегрузки роли не играют. Имеют значение лишь количество и типы параметров. Поэтому, у нас получится два метода:
1.      Registry (String, String, String, String, String);
2.      Registry (String, String, String);
Т.е., в нашем случае методы различаются количеством параметров, но может быть различие как в их типах (даже при равном количестве), так и в типе возвращаемых значений.

    /// 
    /// Контрагент
    /// 
    public class Contractor
    {
        /// 
        /// Регистрация юрлица
        /// 
        /// Название
        /// ФИО директора
        /// ИНН
        /// КПП
        /// Юридический адрес
        public void Registry (String name, String director, String inn, String kpp, String address)
        {
        }

        /// 
        /// Регистрация индивидуального предпринимателя
        /// 
        /// ФИО предпринимателя
        /// ИНН
        /// Адрес прописки
        public void Registry(String fio, String inn, String address)
        { 
        }
    }

Перегружать можно любые методы, в том числе и конструкторы. Это бывает удобно, если экземпляр класса может создаваться в нескольких вариантах. Рассмотрим следующий пример. В организации создаётся новый сотрудник (экземпляр класса Employee). В зависимости от должности, от него требуется предоставление разных параметров (например, для повара необходима медкнижка, а для охранника – лицензия на охранную деятельность).
    /// 
    /// Сотрудник
    /// 
    public class Employee 
    {
        /// 
        /// Найм повара
        /// 
        /// ФИО
        /// № медкнижки
        /// Разряд
        public Employee(String fio, String medDataNum, Int32 rank)
        { 
        }
        /// 
        /// Найм охранника
        /// 
        /// ФИО
        /// № лицензии
        public Employee(String fio, String license)
        { 
        }
    }

Полиморфизм в наследовании

В отношении наследования между классами полиморфизм используется для переопределения функционала класса-предка в классе-наследнике. Здесь требования к методам диаметрально противоположные: базовый и переопределяемый методы должны иметь строго одинаковую сигнатуру. Кроме того, метод базового класса должен быть помечен как виртуальный (virtual), а в классе-наследнике он должен быть либо переопределён (override), либо скрыт (new). Если в классе-наследнике метод не описан вообще, то вызывается метод базового класса.
Рассмотрим следующий пример. Существует класс «Прямоугольник», в котором площадь прямоугольника рассчитывается как произведение двух смежных сторон. Его наследником является класс «Прямоугольный треугольник», площадь которого рассчитывается как половина площади прямоугольника, образованного катетами треугольника. Метод расчёта площади переопределён в классе-наследнике с помощью конструкции override. Для того, чтобы не писать код расчёта площади прямоугольника заново, обратимся из переопределённого метода к базовому методу (методу базового класса) с помощью оператора base.
    /// 
    /// Прямоугольник
    /// 
    public class Rectangle
    {
        /// 
        /// Расчёт площади
        /// 
        /// Сторона 1
        /// Сторона 2
        /// Площадь прямоугольника
        public virtual Double GetArea(Double a, Double b)
        {
            return a * b;
        }
    }

    /// 
    /// Прямоугольный треугольник
    /// 
    public class RectTriangle : Rectangle
    {
        /// 
        /// Расчёт площади
        /// 
        /// Катет 1
        /// Катет 2
        /// Площадь прямоугольного треугольника
        public override double GetArea(double a, double b)
        {
            return base.GetArea(a, b) / 2;
        }
    }
В следующем примере мы используем в качестве базового класса «Произвольный треугольник», площадь которого рассчитывается по формуле Герона. Класс «Прямоугольный треугольник», являющийся наследником произвольного треугольника, использует для нахождения площади половину произведения катетов, т.е. никак не использует функциональность базового класса. В этом случае будет уместно скрыть базовый функционал с помощью модификатора new.
    /// 
    /// Произвольный треугольник
    /// 
    public class Triangle
    {
        /// 
        /// Сторона А
        /// 
        protected Double _a;

        /// 
        /// Сторона В
        /// 
        protected Double _b;

        /// 
        /// Сторона С
        /// 
        protected Double _c;

        /// 
        /// Расчёт площади по формуле Герона
        /// 
        /// 
        public virtual Double GetArea()
        {
            Double p = (_a + _b + _c) / 2;
            return Math.Sqrt(p * (p - _a) * (p - _b) * (p - _c));
        }
    }

    /// 
    /// Прямоугольный треугольник
    /// 
    public class RectTriangle : Triangle
    {
        /// 
        /// Расчёт площади при условии, что а и б - катеты
        /// 
        /// Площадь прямоугольного треугольника
        public new double GetArea()
        {
            return _a * _b / 2;
        }
    }
Различие между сокрытием и переопределением будет показано в следующем примере. Используем в качестве базового класса «Произвольный треугольник», а в качестве наследников «Прямоугольный треугольник» и «Равносторонний треугольник». Площадь произвольного треугольника будет рассчитываться по формуле Герона, площадь прямоугольного треугольника – через половину произведения катетов, а площадь равностороннего треугольника – как половина произведения стороны на высоту.

Метод нахождения площади в прямоугольном треугольнике мы скрываем, а в равностороннем – переопределяем. Также, мы переопределяем конструкторы в классах-наследниках, потому что для прямоугольного треугольника необязательно указывать гипотенузу, а для равностороннего треугольника достаточно указать одну сторону.
    /// 
    /// Произвольный треугольник
    /// 
    public class Triangle
    {
        /// 
        /// Сторона А
        /// 
        protected Double _a;

        /// 
        /// Сторона В
        /// 
        protected Double _b;

        /// 
        /// Сторона С
        /// 
        protected Double _c;

        /// 
        /// Конструктор
        /// 
        /// Сторона 1
        /// Сторона 2
        /// Сторона 3
        public Triangle(Double a, Double b, Double c)
        {
            _a = a;
            _b = b;
            _c = c;
        }

        /// 
        /// Расчёт площади по формуле Герона
        /// 
        /// 
        public virtual Double GetArea()
        {
            Double p = (_a + _b + _c) / 2;
            return Math.Sqrt(p * (p - _a) * (p - _b) * (p - _c));
        }
    }

    /// 
    /// Прямоугольный треугольник
    /// 
    public class RectTriangle : Triangle
    {
        /// 
        /// Конструктор
        /// 
        /// Катет 1
        /// Катет 2        
        public RectTriangle(Double a, Double b) : base(a, b, Math.Sqrt(a * a + b * b)) { }

        /// 
        /// Расчёт площади при условии, что а и б - катеты
        /// 
        /// Площадь прямоугольного треугольника
        public new double GetArea()
        {
            return _a * _b / 2;
        }
    }

    /// 
    /// Равносторонний треугольник
    /// 
    public class SameSizesTriangle : Triangle
    {
        /// 
        /// Конструктор
        /// 
        /// Сторона треугольника
        public SameSizesTriangle(Double a) : base(a, a, a) { }

        /// 
        /// Расчёт площади треугольника
        /// 
        /// Площадь равностороннего треугольника
        public override double GetArea()
        {
            Double h = Math.Sqrt(3) / 2 * _a;
            return h * _a / 2;
        }
    }
Экземпляры классов треугольников мы можем создавать как «напрямую», так и используя наследование. Именно при использовании наследования становится очевидна разница между переопределением и сокрытием методов. Если обратиться к экземпляру класса-наследника как к экземпляру класса-предка, то переопределённый метод сработает сам по себе (т.е. сработает метод из класса-наследника), а вместо сокрытого метода будет работать метод базового класса. Это принципиальное различие, которое помогает строить иерархию наследования классов.
    class Program
    {
        static void Main(string[] args)
        {
            //Создание обычного треугольника
            Triangle t1 = new Triangle(4, 4, 5);
            //Создание прямоугольного треугольника
            Triangle t2 = new RectTriangle(4, 4);
            //Создание равностороннего треугольника
            Triangle t3 = new SameSizesTriangle(4);

            //Расчёт площади обычного треугольника
            Console.WriteLine("Площадь треугольника равна {0}.", t1.GetArea());
            //Расчёт площади прямоугольного треугольника (работает метод базового класса)
            Console.WriteLine("Площадь треугольника равна {0}.", t2.GetArea());
            //Расчёт площади равностороннего треугольника (работает переопределённый метод)
            Console.WriteLine("Площадь треугольника равна {0}.", t3.GetArea());

            Console.ReadLine();
        }
    }
Полный код примера, иллюстрирующего использование сокрытия и переопределения описан ниже. В нём мы задаём массив экземпляров «Произвольный треугольник», но заполняем его всеми видами треугольников. Затем в цикле перебираем массив и выводим площадь треугольника в консоль.
    class Program
    {
        static void Main(string[] args)
        {
            //Создаём массив треугольников и заполняем его
            Triangle[] triangles = new Triangle[3]
            {
                new Triangle(4,4,5),
                new RectTriangle(4,4),
                new SameSizesTriangle(4)
            };

            //Циклом перебираем элементы массива и выводим площади в консоль
            for (Int32 i = 0; i < 3; i++)
            {
                Console.WriteLine(triangles[i].GetArea());
            }
            Console.ReadLine();
        }
    }

Заключение


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