Разное

Инкапсуляция это в программировании: Инкапсуляция в Си++ и Си / Хабр

ООП в картинках / Хабр

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


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

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

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

В силу своей природы, объектно-ориентированное программирование лучше всего объяснять на примерах. Как и обещал, нашими пациентами будут трансформеры. Я не трансформеролог, и комиксов не читал, посему в примерах буду руководствоваться википедией и фантазией.

Классы и объекты

Сразу лирическое отступление: объектно-ориентированный подход возможен и без классов, но мы будем рассматривать, извиняюсь за каламбур, классическую схему, где классы — наше всё.

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

Таким образом, класс — это описание того, какими свойствами и поведением будет обладать объект. А объект — это экземпляр с собственным состоянием этих свойств.

Мы говорим «свойства и поведение», но звучит это как-то абстрактно и непонятно. Привычнее для программиста будет звучать так: «переменные и функции». На самом деле «свойства» — это такие же обычные переменные, просто они являются атрибутами какого-то объекта (их называют полями объекта). Аналогично «поведение» — это функции объекта (их называют методами), которые тоже являются атрибутами объекта. Разница между методом объекта и обычной функцией лишь в том, что метод имеет доступ к собственному состоянию через поля.

Итого, имеем методы и свойства, которые являются атрибутами. Как работать с атрибутами? В большинстве ЯП оператор обращения к атрибуту — это точка (кроме PHP и Perl). Выглядит это примерно вот так (псевдокод):

// объявление класса с помощью ключевого слова class
class Transformer(){
    // объявление поля x
    int x

    // объявление метода конструктора (сюда нам чуть ниже передадут 0)
    function constructor(int x){
        // инициализация поля x 
        // (переданный конструктору 0 превращается в свойство объекта)
        this.x = x
    }
	
    // объявление метода run
    function run(){
        // обращение к собственному атрибуту через this
        this.x += 1
    }
}

// а теперь клиентский код:

// создаем новый экземпляр трансформера с начальной позицией 0
optimus = new Transformer(0)

optimus.run() // приказываем Оптимусу бежать
print optimus.x // выведет 1
optimus.run() // приказывает Оптимусу еще раз бежать
print optimus.x // выведет 2

В картинках я буду использовать такие обозначения:

Я не стал использовать UML-диаграммы, посчитав их недостаточно наглядными, хоть и более гибкими.

Анимация №1

Что мы видим из кода?

1. this — это специальная локальная переменная (внутри методов), которая позволяет объекту обращаться из своих методов к собственным атрибутам. Обращаю внимание, что только к собственным, то бишь, когда трансформер вызывает свой метод, либо меняет собственное состояние. Если снаружи обращение будет выглядеть так: optimus.x, то изнутри, если Оптимус захочет сам обратиться к своему полю x, в его методе обращение будет звучать так: this.x, то есть «я (Оптимус) обращаюсь к своему атрибуту x«. В большинстве языков эта переменная называется this, но встречаются и исключения (например, self)

2. constructor — это специальный метод, который автоматически вызывается при создании объекта. Конструктор может принимать любые аргументы, как и любой другой метод. В каждом языке конструктор обозначается своим именем. Где-то это специально зарезервированные имена типа __construct или __init__, а где-то имя конструктора должно совпадать с именем класса. Назначение конструкторов — произвести первоначальную инициализацию объекта, заполнить нужные поля.

3. new — это ключевое слово, которое необходимо использовать для создания нового экземпляра какого-либо класса. В этот момент создается объект и вызывается конструктор. В нашем примере, конструктору передается 0 в качестве стартовой позиции трансформера (это и есть вышеупомянутая инициализация). Ключевое слово new в некоторых языках отсутствует, и конструктор вызывается автоматически при попытке вызвать класс как функцию, например так: Transformer().

4. Методы constructor и run работают с внутренним состоянием, а во всем остальном не отличаются от обычных функций. Даже синтаксис объявления совпадает.

5. Классы могут обладать методами, которым не нужно состояние и, как следствие, создание объекта. В этом случае метод делают статическим.

SRP

(Single Responsibility Principle / Принцип единственной ответственности / Первый принцип SOLID). С ним вы, наверняка, уже знакомы из других парадигм: «одна функция должна выполнять только одно законченное действие». Этот принцип справедлив и для классов: «Один класс должен отвечать за какую-то одну задачу». К сожалению с классами сложнее определить грань, которую нужно пересечь, чтобы принцип нарушался.

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

Ассоциация

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

Предположим, что наш трансформер оборудован пушкой. Хотя нет, лучше двумя пушками. В каждой руке. Пушки одинаковые (принадлежат к одному классу, или, если будет угодно, выполненные по одному чертежу), обе одинаково умеют стрелять и перезаряжаться, но в каждой есть свое хранилище боеприпасов (собственное состояние). Как теперь это описать в ООП? С помощью ассоциации:

class Gun(){ // объявляем класс Пушка
    int ammo_count // объявляем количество боеприпасов

    function constructor(){ // конструктор
        this.reload() // вызываем собственный метод "перезарядить"
    }

    function fire(){ // объявляем метод пушки "стрелять"
        this.ammo_count -= 1 // расходуем боеприпас из собственного магазина
    }

    function reload(){ // объявляем метод "перезарядить"
        this.ammo_count = 10 // забиваем собственный магазин боеприпасами
    }
}

class Transformer(){ // объявляем класс Трансформер
    Gun gun_left // объявляем поле "левая пушка" типа Пушка
    Gun gun_right // объявляем поле "правая пушка" тоже типа Пушка
    
    /*
    теперь конструктор Трансформера принимает
    в качестве аргументов две уже конкретные созданные пушки,
    которые передаются извне
    */
    function constructor(Gun gun_left, Gun gun_right){
        this.gun_left = gun_left // устанавливаем левую пушку на борт
        this.gun_right = gun_right // устанавливаем правую пушку на борт
    }
    
    // объявляем метод Трансформер "стрелять", который сначала стреляет...
    function fire(){
        // левой пушкой, вызывая ее метод "стрелять"
        this.gun_left.fire()
        // а затем правой пушкой, вызывая такой же метод "стрелять"
        this.gun_right.fire()
    }
}

gun1 = new Gun() // создаем первую пушку
gun2 = new Gun() // создаем вторую пушку
optimus = new Transformer(gun1, gun2) // создаем трансформера, передавая ему обе пушки

Анимация №2

this.gun_left.fire() и this.gun_right.fire() — это обращения к дочерним объектам, которые происходят так же через точки. По первой точке мы обращаемся к атрибуту себя (this.gun_right), получая объект пушки, а по второй точке обращаемся к методу объекта пушки (this.gun_right.fire()).

Итог: робота сделали, табельное оружие выдали, теперь разберемся, что тут происходит. В данном коде один объект стал составной частью другого объекта. Это и есть ассоциация. Она в свою очередь бывает двух видов:

1. Композиция — случай, когда на фабрике трансформеров, собирая Оптимуса, обе пушки ему намертво приколачивают к рукам гвоздями, и после смерти Оптимуса, пушки умирают вместе с ним. Другими словами, жизненный цикл дочернего объекта совпадает с жизненным циклом родительского.

2. Агрегация — случай, когда пушка выдается как пистолет в руку, и после смерти Оптимуса этот пистолет может подобрать его боевой товарищ Олег, а затем взять в свою руку, либо сдать в ломбард. То бишь жизненный цикл дочернего объекта не зависит от жизненного цикла родительского, и может использоваться другими объектами.

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

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

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

Что если, мы не хотим штамповать одинаковых трансформеров, а хотим сделать общий каркас, но с разным обвесом? ООП позволяет нам такую шалость путем разделения логики на сходства и различия с последующим выносом сходств в родительский класс, а различий в классы-потомки. Как это выглядит?

Оптимус Прайм и Мегатрон — оба трансформеры, но один является автоботом, а второй десептиконом. Допустим, что различия между автоботами и десептиконами будут заключаться только в том, что автоботы трансформируются в автомобили, а десептиконы — в авиацию. Все остальные свойства и поведение не будут иметь никакой разницы. В таком случае можно спроектировать систему наследования так: общие черты (бег, стрельба) будут описаны в базовом классе «Трансформер», а различия (трансформация) в двух дочерних классах «Автобот» и «Десептикон».

class Transformer(){ // базовый класс
    function run(){
        // код, отвечающий за бег
    }
    function fire(){
        // код, отвечающий за стрельбу
    }
}

class Autobot(Transformer){ // дочерний класс, наследование от Transformer
    function transform(){
        // код, отвечающий за трансформацию в автомобиль
    }
}

class Decepticon(Transformer){ // дочерний класс, наследование от Transformer
    function transform(){
        // код, отвечающий за трансформацию в самолет
    }
}

optimus = new Autobot()
megatron = new Decepticon()

Анимация №3

Сей пример наглядно иллюстрирует, как наследование становится одним из способов дедуплицировать код (DRY-принцип) с помощью родительского класса, и одновременно предоставляет возможности для мутации в классах-потомках.

Перегрузка

Если же в классе-потомке переопределить уже существующий метод в классе-родителе, то сработает перегрузка. Это позволяет не дополнять поведение родительского класса, а модифицировать. В момент вызова метода или обращения к полю объекта, поиск атрибута происходит от потомка к самому корню — родителю. То есть, если у автобота вызвать метод fire(), сначала поиск метода производится в классе-потомке — Autobot, а поскольку его там нет, поиск поднимается на ступень выше — в класс Transformer, где и будет обнаружен и вызван. Следует отметить, что модификация нарушает LSP из набора принципов SOLID, но мы рассматриваем только техническую возможность.

Неуместное применение

Любопытно, что чрезмерно глубокая иерархия наследования может привести к обратному эффекту — усложнению при попытке разобраться, кто от кого наследуется, и какой метод в каком случае вызывается. К тому же, не все архитектурные требования можно реализовать с помощью наследования. Поэтому применять наследование следует без фанатизма. Существуют рекомендации, призывающие предпочитать композицию наследованию там, где это уместно. Любая критика наследования, которую я встречал, подкрепляется неудачными примерами, когда наследование используется в качестве золотого молотка. Но это совершенно не означает, что наследование в принципе всегда вредит. Мой нарколог говорил, что первый шаг — это признать, что у тебя зависимость от наследования.

Как при описании отношений двух сущностей определить, когда уместно наследование, а когда — композиция? Можно воспользоваться популярной шпаргалкой: спросите себя, сущность А является сущностью Б? Если да, то скорее всего, тут подойдет наследование. Если же сущность А является частью сущности Б, то наш выбор — композиция.

Применительно к нашей ситуации это будет звучать так:

  1. Автобот является Трансформером? Да, значит выбираем наследование.
  2. Пушка является частью Трансформера? Да, значит — композиция.

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

Наследование статично

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

Множественное наследование

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

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

Абстрактные классы

Кроме обычных классов в некоторых языках существуют абстрактные классы. От обычных классов они отличаются тем, что нельзя создать объект такого класса. Зачем же нужен такой класс, спросит читатель? Он нужен для того, чтобы от него могли наследоваться потомки — обычные классы, объекты которых уже можно создавать.

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

Полиморфизм

Полиморфизм — свойство системы, позволяющее иметь множество реализаций одного интерфейса. Ничего непонятно. Обратимся к трансформерам.

Положим, у нас есть три трансформера: Оптимус, Мегатрон и Олег. Трансформеры боевые, стало быть обладают методом attack(). Игрок, нажимая у себя на джойстике кнопку «воевать», сообщает игре, чтобы та вызвала метод attack() у трансформера, за которого играет игрок. Но поскольку трансформеры разные, а игра интересная, каждый из них будет атаковать каким-то своим способом. Скажем, Оптимус — объект класса Автобот, а Автоботы снабжаются пушками с плутониевыми боеголовками (да не прогневаются фанаты трансформеров). Мегатрон — Десептикон, и стреляет из плазменной пушки. Олег — басист, и он обзывается. А в чем польза?

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

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

Инкапсуляция — это контроль доступа к полям и методам объекта. Под контролем доступа подразумевается не только можно/неможно, но и различные валидации, подгрузки, вычисления и прочее динамическое поведение.

Во многих языках частью инкапсуляции является сокрытие данных. Для этого существуют модификаторы доступа (опишем те, которые есть почти во всех ООП языках):

  • publiс — к атрибуту может получить доступ любой желающий
  • private — к атрибуту могут обращаться только методы данного класса
  • protected — то же, что и private, только доступ получают и наследники класса в том числе
class Transformer(){
    public function constructor(){ }

    protected function setup(){ }

    private function dance(){ }
}

Как правильно выбрать модификатор доступа? В простейшем случае так: если метод должен быть доступен внешнему коду, выбираем public. В противном случае — private. Если есть наследование, то может потребоваться protected в случае, когда метод не должен вызываться снаружи, но должен вызываться потомками.

Аксессоры (геттеры и сеттеры)

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

В некоторых языках есть синтаксический сахар, позволяющий такие аксессоры маскировать под свойства, что делает доступ прозрачным для внешнего кода, который и не подозревает, что работает не с полем, а с методом, у которого под капотом выполняется SQL-запрос или чтение из файла. Так достигается абстракция и прозрачность.

Интерфейсы

Задача интерфейса — снизить уровень зависимости сущностей друг от друга, добавив больше абстракции.

Не во всех языках присутствует этот механизм, но в ООП языках со статической типизацией без них было бы совсем худо. Выше мы рассматривали абстрактные классы, затрагивая тему контрактов, обязующих имплементировать какие-то абстрактные методы. Так вот интерфейс очень смахивает на абстрактный класс, но является не классом, а просто пустышкой с перечислением абстрактных методов (без имплементации). Другими словами, интерфейс имеет декларативную природу, то есть, чистый контракт без капельки кода.

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

Классы с интерфейсами состоят в отношении «многие ко многим»: один класс может имплементировать множество интерфейсов, и каждый интерфейс, в свою очередь, может имплементироваться многими классами.

У интерфейса двустороннее применение:

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

Например, если какой-то объект помимо основного поведения, может быть сериализован, то пускай он имплементирует интерфейс «Сериализуемый». А если объект можно склонировать, то пусть он имплементирует еще один интерфейс — «Клонируемый». И если у нас есть какой-то транспортный модуль, который передает объекты по сети, он будет принимать любые объекты, имплементирующие интерфейс «Сериализуемый».

Представим, что каркас трансформера оборудован тремя слотами: слот для оружия, для генератора энергии и для какого-нибудь сканера. Эти слоты обладают определенными интерфейсами: в каждый слот можно установить только подходящее оборудование. В слот для оружия можно установить ракетную установку или лазерную пушку, в слот для генератора энергии — ядерный реактор или РИТЭГ (радиоизотопный термоэлектрический генератор), а в слот для сканера — радар или лидар. Суть в том, что каждый слот имеет универсальный интерфейс подключения, а уже конкретные устройства должны соответствовать этому интерфейсу. К примеру, на материнских платах используется несколько типов слотов: слот для процессора позволяет подключать различные процессоры, подходящие под данный сокет, а слот SATA — любой SSD или HDD накопитель или даже CD/DVD.

Обращаю внимание, что получившаяся система слотов у трансформеров — это пример использования композиции. Если же оборудование в слотах будет сменным в ходе жизни трансформера, то тогда это уже агрегация. Для наглядности, мы будем называть интерфейсы, как принято в некоторых языках, добавляя заглавную «И» перед именем: IWeapon, IEnergyGenerator, IScanner.

// описания интерфейсов:

interface IWeapon{
    function fire() {} // декларация метода без имплементации. Ниже аналогично
}

interface IEnergyGenerator{
    // тут уже два метода, которые должны будут реализовать классы:
    function generate_energy() {} // первый
    function load_fuel() {}       // второй
}

interface IScanner{
    function scan() {}
}


// классы, реализующие интерфейсы:

class RocketLauncher() : IWeapon
{
    function fire(){
        // имплементация запуска ракеты
    }
}

class LaserGun() : IWeapon
{
    function fire(){
        // имплементация выстрела лазером
    }
}

class NuclearReactor() : IEnergyGenerator
{
    function generate_energy(){
        // имплементация генерации энергии ядерным реактором
    }
	
    function load_fuel(){
        // имплементация загрузки урановых стержней
    }
}

class RITEG() : IEnergyGenerator
{
    function generate_energy(){
        // имплементация генерации энергии РИТЭГ
    }
	
    function load_fuel(){
        // имплементация загрузки РИТЭГ-пеллет
    }
}

class Radar() : IScanner
{
    function scan(){
        // имплементация использования радиолокации
    }	
}

class Lidar() : IScanner
{
    function scan(){
        // имплементация использования оптической локации
    }
}

// класс - потребитель:

class Transformer() {
    // привет, композиция:
    IWeapon slot_weapon   // Интерфейсы указаны в качестве типов данных.
    IEnergyGenerator slot_energy_generator // Они могут принимать любые объекты,
    IScanner slot_scanner // которые имплементируют указанный интерфейс
	
    /*
    в параметрах методов интерфейс тоже указан как тип данных,
    метод может принимать объект любого класса,
    имплементирующий данный интерфейс:
    */
    function install_weapon(IWeapon weapon){ 
        this.slot_weapon = weapon
    }
	
    function install_energy_generator(IEnergyGenerator energy_generator){
        this.slot_energy_generator = energy_generator
    }
	
    function install_scanner(IScanner scanner){
        this.slot_scanner = scanner
    }
}

// фабрика трансформеров

class TransformerFactory(){
    function build_some_transformer() {
       	transformer = new Transformer()
       	laser_gun = new LaserGun()
       	nuclear_reactor = new NuclearReactor()
       	radar = new Radar()
       	
       	transformer.install_weapon(laser_gun)
       	transformer.install_energy_generator(nuclear_reactor)
       	transformer.install_scanner(radar)
        	
        return transformer
    }
}

// использование

transformer_factory = new TransformerFactory()
oleg = transformer_factory.build_some_transformer()

Анимация №4

К сожалению, в картинку не влезла фабрика, но она все равно необязательна, трансформера можно собрать и во дворе.

Обозначенный на картинке слой абстракции в виде интерфейсов между слоем имплементации и слоем-потребителем дает возможность абстрагировать одних от других. Вы можете это наблюдать, посмотрев на каждый слой в отдельности: в слое имплементации (слева) нет ни слова про класс Transformer, а в слое-потребителе (справа) нет ни слова про конкретные имплементации (там нет слов Radar, RocketLauncher, NuclearReactor и т. д.)

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

Утиная типизация

Явление, которое мы наблюдаем в получившейся архитектуре, называется утиной типизацией: если что-то крякает как утка, плавает как утка, и выглядит как утка, то, скорее всего — это утка.

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

В отличие от иерархической типизации наследования, при утиной типизации трансформеру пофиг, какого класса пушку ему дали, и пушка ли это вообще. Главное, что эта штуковина умеет стрелять! Это не достоинство утиной типизации, а скорее компромисс. Может быть и обратная ситуация, как на этой картинке ниже:

ISP

(Interface Segregation Principle / Принцип разделения интерфейса / Четвертый принцип SOLID) призывает не создавать жирные универсальные интерфейсы. Вместо этого интерфейсы нужно разделять на более мелкие и специализированные, это поможет гибче их комбинировать в имплементирующих классах, не заставляя имплементировать лишние методы.

Абстракция

В ООП все крутится вокруг абстракции. Существуют фанатики, утверждающие, что абстракция должна быть частью ООП-троицы (инкапсуляция, полиморфизм, наследование). А мой инспектор по УДО говорил обратное: абстракция присуща для любого программирования, а не только для ООП, поэтому она должна стоять отдельно. С другой стороны, то же самое можно сказать и про остальные принципы, но из песни слов не выкинешь. Так или иначе, абстракция нужна, и особенно в ООП.

Уровень абстракции

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

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

Неверный выбор уровня абстракции ведет к одной из двух проблем:

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

Еще важно понимать, что уровень абстракции определяется не для всего проекта в целом, а отдельно для разных компонентов. В каких-то местах системы абстракции может быть недостаточно, а где-то наоборот — перебор. Однако, неверный выбор уровня абстракции можно исправить своевременным рефакторингом. Ключевое слово — своевременным. Запоздалый рефакторинг провести проблематично, когда на данном уровне абстракции реализовано уже множество механизмов. Проводить обряд рефакторинга в запущенных системах может сопрягаться с острой болью в труднодоступных местах программиста. Это примерно как поменять фундамент в доме — дешевле построить рядом дом с нуля.

Давайте рассмотрим определение уровня абстракции из возможных вариантов на примере гипотетической игры «трансформеры-онлайн». Уровни абстракции в данном случае будут выступать как слои, каждый последующий рассматриваемый слой будет ложиться поверх предыдущего, забирая из него часть функционала в себя.

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

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

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

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

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

Паттерны проектирования

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

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

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

Еще одна ценность от паттернов — формализации терминологии. Гораздо проще коллеге сказать, что в этом месте используется «цепочка обязанностей», чем полчаса рисовать поведение и отношения объектов на бумажке.

Заключение

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

Хочется закончить свою песнь важными словами. Любые описанные механизмы, принципы и паттерны, как и ООП в целом не стоит применять там, где это бессмысленно или может навредить. Это ведет к появлению статей со странными заголовками типа «Наследование — причина преждевременного старения» или «Синглтон может приводить к онкологическим заболеваниям».

Я серьезно. Если рассмотреть случай с синглтоном, то его повсеместное применение без знания дела, стало причиной серьезных архитектурных проблем во многих проектах. И любители забивать гвозди микроскопом любезно его нарекли антипаттерном. Будьте благоразумны.

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

Инкапсуляция (программирование) — это… Что такое Инкапсуляция (программирование)?

В языках программирования инкапсуля́ция имеет одно из следующих значений, либо их комбинацию:

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

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

В то же время, в языках поддерживающих замыкания, инкапсуляция рассматривается как понятие не присущее исключительно объектно-ориентированному программированию. Также, реализации абстрактных типов данных (например, модули) предлагают схожую модель инкапсуляции.

Сокрытие реализации целесообразно применять в следующих целях:

  • предельная локализация изменений при необходимости таких изменений,
  • прогнозируемость изменений (какие изменения в коде нужно сделать для заданного изменения функциональности) и прогнозируемость последствий изменений.

Примеры

C++

class A 
{
 public:
   int a, b; //данные открытого интерфейса
   int ReturnSomething(); //метод открытого интерфейса
 private:
   int Aa, Ab; //скрытые данные
   void DoSomething(); //скрытый метод
};

Класс А инкапсулирует свойства Aa, Ab и метод DoSomething, представляя внешний интерфейс ReturnSomething, a, b.

C#

Целью инкапсуляции является обеспечение согласованности внутреннего состояния объекта. В C# для инкапсуляции используются публичные свойства и методы объекта. Переменные, за редким исключением, не должны быть публично доступными. Проиллюстрировать инкапсуляцию можно на простом примере. Допустим, нам необходимо хранить вещественное значение и его строковое представление (например, для того, чтобы не производить каждый раз конвертацию в случае частого использования). Пример реализации без инкапсуляции таков:

    class NoEncapsulation
    {
        public double Value;
        public string ValueString;
    }

При этом мы можем отдельно изменять как само значение Value, так и его строковое представление, и в некоторый момент может возникнуть их несоответствие (например, в результате исключения). Пример реализации с использованием инкапсуляции:

    class EncapsulationExample
    {
        private double valueDouble;
        private string valueString;
 
        public double Value
        {
            get { return valueDouble; }
            set 
            {
                valueDouble = value;
                valueString = value.ToString();
            }
        }
 
        public string ValueString
        {
            get { return valueString; }
            set 
            {
                double tmp_value = Convert.ToDouble(ValueString); //здесь может возникнуть исключение
                valueDouble = tmp_value;
                valueString = ValueString;
            }
        }
    }

Здесь доступ к переменным valueDouble и valueString возможен только через свойства Value и ValueString. Если мы попытаемся присвоить свойству ValueString некорректную строку и возникнет исключение в момент конвертации, то внутренние переменные останутся в прежнем, согласованном состоянии, поскольку исключение вызывает выход из процедуры.

Delphi

В Delphi для создания скрытых полей или методов их достаточно объявить в секции private.

  TMyClass = class
  private
    FMyField: Integer;
    procedure SetMyField(const Value: Integer);
    function GetMyField: Integer;
  protected
  public
    property MyField: Integer read GetMyField write SetMyField;
  end;

Для создания интерфейса доступа к скрытым полям в Delphi введены свойства.

PHP5

class A 
{
 private $a; // скрытое свойство
 private $b; // скрытое свойство
 private function DoSomething() //скрытый метод
 {  
  //actions
 }
 
 public function ReturnSomething() //открытый интерфейс
 { 
  //actions
 }
};

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

Java

class A {
 private int a;
 private int b;
 
 private void doSomething() { //скрытый метод
  //actions
 }
 
 public int returnSomething() { //открытый метод
  return a;
 } 
}

JavaScript

A = function() {
 // private
 var _property;
 var _privateMethod = function() { /* actions */ } // скрытый метод
 
 // public
 this.getProperty = function() { // открытый интерфейс
  return _property;
 }
 
 this.setProperty = function(value) { // открытый интерфейс
  _property = value;
  _privateMethod();
 }
}

См. также

ООП с примерами (часть 1) / Хабр

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

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

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

Основные понятия ООП

Класс

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

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

Класс – это способ описания сущности, определяющий состояние и поведение, зависящее от этого состояния, а также правила для взаимодействия с данной сущностью (контракт).

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

С точки зрения структуры программы, класс является сложным типом данных.

В нашем случае, класс будет отображать сущность – автомобиль. Атрибутами класса будут являться двигатель, подвеска, кузов, четыре колеса и т.д. Методами класса будет «открыть дверь», «нажать на педаль газа», а также «закачать порцию бензина из бензобака в двигатель». Первые два метода доступны для выполнения другим классам (в частности, классу «Водитель»). Последний описывает взаимодействия внутри класса и не доступен пользователю.

В дальнейшем, несмотря на то, что слово «пользователь» ассоциируется с пасьянсом «Косынка» и «Microsoft Word», мы будем называть пользователями тех программистов, которые используют ваш класс, включая вас самих. Человека, который является автором класса, мы будем называть разработчиком.

Объект

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

Объект (экземпляр) – это отдельный представитель класса, имеющий конкретное состояние и поведение, полностью определяемое классом.

Говоря простым языком, объект имеет конкретные значения атрибутов и методы, работающие с этими значениями на основе правил, заданных в классе. В данном примере, если класс – это некоторый абстрактный автомобиль из «мира идей», то объект – это конкретный автомобиль, стоящий у вас под окнами.

Интерфейс

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

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

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

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

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

Примером простого интерфейса может служить машина с коробкой-автоматом. Освоить её управление очень быстро сможет любая блондинка, окончившая двухнедельные курсы вождения. С другой стороны, чтобы освоить управление современным пассажирским самолётом, необходимо несколько месяцев, а то и лет упорных тренировок. Не хотел бы я находиться на борту Боинга, которым управляет человек, имеющий двухнедельный лётный стаж. С другой стороны, вы никогда не заставите автомобиль подняться в воздух и перелететь из Москвы в Вашингтон.

Инкапсуляция — это… Что такое Инкапсуляция?

  • Инкапсуляция — (лат. in в, capsula коробочка ; итал. incapsulare закупоривать) 1. Изоляция, закрытие чего либо мешающего, ненужного, вредного с целью исключения отрицательного влияния на окружающее. (Поместить радиоактивные отходы в капсулу, закрыть… …   Википедия

  • ИНКАПСУЛЯЦИЯ — (лат.). Изменение одних слов в другие, в американских языках. Словарь иностранных слов, вошедших в состав русского языка. Чудинов А.Н., 1910. инкапсуляция (лат. in в) образование капсулы 1 вокруг чуждых для организма веществ (инородных тел,… …   Словарь иностранных слов русского языка

  • инкапсуляция — инкапсулирование Словарь русских синонимов. инкапсуляция сущ., кол во синонимов: 1 • инкапсулирование (1) Словарь синонимов ASIS …   Словарь синонимов

  • инкапсуляция «IP в IP» — Для инкапсуляции в дейтаграмму IP используется инкапсуляция IP в IP (IP in IP), для этого перед существующим заголовком IP дейтаграммы вставляется внешний заголовок IP (МСЭ Т Y.1281). [http://www.iks media.ru/glossary/index.html?glossid=2400324]… …   Справочник технического переводчика

  • ИНКАПСУЛЯЦИЯ — (от лат capsula ящик, капсула, сумка), осумкование, термин, употребляющийся для обозначения разрастания соединительнотканной капсулы, сумки вокруг тех или иных пат. образований, паразитов и пр. тел, не подвергающихся почему либо рассасыванию в… …   Большая медицинская энциклопедия

  • инкапсуляция — скрытие включение вбирание Термин объектно ориентированного программирования, означающий структурирование программы на модули особого вида, называемые классами и объединяющие данные и процедуры их обработки, причем внутренние данные класса не… …   Справочник технического переводчика

  • инкапсуляция — (incapsulatio; ин + лат. capsula ящичек, оболочка) процесс отграничения очага воспаления или инородного тела путем образования вокруг него фиброзной оболочки (капсулы) …   Большой медицинский словарь

  • инкапсуляция — инкапсуляция, инкапсуляции, инкапсуляции, инкапсуляций, инкапсуляции, инкапсуляциям, инкапсуляцию, инкапсуляции, инкапсуляцией, инкапсуляциею, инкапсуляциями, инкапсуляции, инкапсуляциях (Источник: «Полная акцентуированная парадигма по А. А.… …   Формы слов

  • инкапсуляция — инкапсул яция, и …   Русский орфографический словарь

  • ИНКАПСУЛЯЦИЯ — (от лат. in — в, внутри и capsula —коробочка, ящичек), разрастание соединительной ткани вокруг инородных тел или мёртвых масс, образующихся в органах и тканях при различных патологических процессах …   Ветеринарный энциклопедический словарь

  • Объектно ориентированное програмирование в графических языках / Хабр

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

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

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

    Вот, например, реальное описание в графической нотации алгоритма управления задвижками АЭС.:

    Рисунок 1. Пример программы управления АЭС в графической нотации

    Слева входные сигналы, справа команды.

    Мне кажется, что такой алгоритм прочитать может даже ребенок:

    • Если насос включен в течении 60 секунд и расход меньше 10, то задвижку на рециркуляции открыть.
    • Если насос включен, то подавать в течении 5 секунд на задвижки 001 и 002 команду открыть.
    • Если расход больше 20 и насос включен, то в течении 5 секунд на задвижку 003 подавать команду закрыть.

    В бытность мою студентом я подрабатывал, создавая библиотеку компонентов для Delphi и был знаком с ООП не понаслышке. Потом, когда столкнулся с реальными программами управления АЭС, очень удивился что нет никакого абстрагирования, инкапсуляции и, прости господи полиморфизма, только чистый Си, и еще желательно урезанный правилами и рекомендация MISRA C, чтобы все было надёжно, переносимо, безопасно.

    Вершиной обрезания Си в моей практике был язык FIL, для систем управления реакторами РБМК. В нем функции заранее писались на Си, компилировались, а потом вызывались на основе текстового файла, где они были описаны на языке FIL. В итоге, можно было вызвать только ограниченный, но тщательно проверенный и отлаженный набор функций. И все это – во имя безопасности и надежности.

    Но при этом система управления реактором и в целом система управления АЭС – это как раз тот случай, где принципы ООП должны применятся в полный рост. В само деле, есть множество однотипного оборудования – задвижки, насосы, датчики, всё легко классифицируется, есть готовые объекты, соответствующие реальному оборудованию. Казалось бы, вот оно – применяй ООП, классы, наследование, абстрагирование и полиморфизм. Но нет, нужен чистый Си и это требования безопасности.

    А дальше – еще интереснее. На самом деле программу для управления АЭС пишет не программист, а технолог – только он знает, что и когда закрывать, открывать, включать, а главное – он знает, когда всю это байду выключать, чтобы не долбануло. А программист должен аккуратно всё это реализовать на коде Си. А еще лучше, что бы программиста вообще бы не было, а технолог сам рисовал технологические алгоритмы управляющих программ в графическом виде, автоматически генерировал код Си и загружал его в аппаратуру управления. Это рекомендуют международные стандарты по безопасности, в этом случае программист – как скрипач – не нужен. Он вносит только дополнительные ошибки и искажения в реализацию мыслей технолога.

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

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

    Например таблица входа в алгоритм:

    /* Index=0
       UID=0
       GeneratorClassName=TSignalReader
       Name=KBA__AA.KBA31EY001.alg_inp
       Type=Вход алгоритма */
    
    state_vars->kbaalgsv0_out_1_ = kba31ap001_xb01;
    state_vars->kbaalgsv0_out_4_ = kba31cf001_xq01;

    Просто присвоение переменных.

    Любой блок описывается как вычисление выхода по входу, с учетом параметров, заданных в списке констант. Например блок «Больше» выглядит в коде так:

    /* Index=5
       UID=5
       GeneratorClassName=TLogBlock
       Name=KBA__AA.KBA31EY001.smu.GT2
       Type=Операция БОЛЬШЕ */
    
    locals->v5_out_0_ = state_vars->kbaalgsv0_out_4_ > consts->kbaalgsv3_a_;

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

    Таким образом, и в других блоках происходит последовательное вычисление локальных переменных из входных, и в конце цикла программы осуществляется запись в выходные переменные.

    /* Index=14
       UID=14
       GeneratorClassName=TSignalWriter
       Name=KBA__AA.KBA31EY001.alg_out
       Type=Выход алгоритма */
    
    if((action==f_InitState)||(action==f_GoodStep)||(action==f_RestoreOuts)){
     kba31ey001_yb01 = locals->v8_out_0_;
     kba31ey001_yb11 = state_vars->kbaalgsv9_out_0_;
     kba31ey001_yb12 = state_vars->kbaalgsv12_out_0_;
     kba31ey001_yb02 = locals->v13_out_0_;
    };

    А где здесь классы, спросите вы?

    Вся методология, связанная с ООП, находится в именах переменных. Казалось бы, что такого может быть в имени переменной? А там может быть цела бездна. Например имя переменной kba31ap001_xb01, просто переменная в коде Си отвечающая требованием по наименованию переменных. Однако для технолога проектанта она выглядит примерно так: «Реакторное отделение, система промышленного водоснабжения, первый насос, пуск». Все это волшебство преобразования происходит благодаря замечательной немецкой системе кодирования (Kraftwerk-Kennzeichensystem) KKS, цитата:

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

    Наряду с маркировкой технологического оборудования, исполнительных органов (запорно-регулирующей, предохранительной, отсечной и т.п. арматуры, механизмов собственных нужд), точек измерения, монтажных единиц, устройств автоматизации, зданий и сооружений, система KKS позволяет маркировать алгоритмы и программы различного вида и назначения (алгоритмы обработки измеряемых технологических параметров, сигнализации, автоматического регулирования, технологических защит, логического управления: блокировок, АВР, пошаговых программ, — расчета технико-экономических показателей и диагностики состояния технологического оборудования), входные, выходные и промежуточные сигналы этих алгоритмов и программ, видеограммы всех уровней, отображаемые на видеотерминалах, кабели и пр.».

    Но самое интересное в последней части имени — _xb01, то что задается через знак подчеркивания. Если посмотреть на базу сигналов для проекта управления, то мы увидим там классы, понятные и знакомые всем, кто когда-то, как-то и где-то интересовался ООП (см. Рис. 2).

    Рисунок 2. Пример структуры базы сигналов для системы управления АЭС.

    У нас есть классы, или таблицы, на рисунке это столбец «Категории». Например, «KD1» у которых есть таблица шаблонных сигналов, полей класса Верхний предел измерения, нижний предел измерения, показание датчика и т.д. — это абстракция.

    А так же есть реализация данного класса — конкретный датчик, например ТК21F02B1, расположенный в контуре, как вы уже догадались по его названию, в «Реакторном отделении, системе промышленного водоснабжения, у первого насоса», да и то, что это датчик расхода, тоже есть в этом названии, но это не точно.

    И у этого экземпляра данного класса есть конкретные сигналы и их значения, в процессе работы программы, и к ним можно получить доступ по именам полей класса. Например, показание датчика рабочее обозначается переменной ТК21F02B1_XQ04.

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

    Посмотрим, как выглядит в графическом виде подпрограмма контроля достоверности датчика. На рисунке 3 часть схемы обработки сигналов:

    Рисунок 3. Пример программы обработки сигнала.

    Видно, что в подпрограмме обработки используются имена переменных ТК21F02B1_XQ04, сформированные по правилам ККS и на основании таблицы полей класса. В приведенном примере происходит вычисление показания датчика в процентах ТК21F02B1_XQ03 по заданным значениям полей экземпляра класса таким, как ТК21F02B1_Xmin и ТК21F02B1_Xmax.

    Если обратится к коду, сгенерированному из этой схемы, то мы увидим простое присвоение значение переменной, чистый Си и никаких плюсов и ООП.

     /* Index=12
       UID=12
       GeneratorClassName=TSignalReader
       Name=KD1.kd3_45.SR6
       Type=Чтение из списка сигналов */
    
    state_vars->su100v12_out_0_ = tk21f02b1_ai;

    И присвоение результата расчета, тоже как простое присвоение переменной (с проверкой на действительность числа, что бы не уронить систему если в результате обработки сигналов мы получили ошибку)

    /* Index=100
       UID=100
       GeneratorClassName=TSignalWriter
       Name=KD1.kd3_45.SW3
       Type=Запись в список сигналов */
    
    if(isfinite(locals->v63_out_0_)){
     tk21f02b1_xq04 = locals->v63_out_0_;
    };

    А в какой же момент появляется объединение данных полей класса методов обработки? На самом деле я знаком с двумя вариантами этого фокуса. Сейчас разберем один из них. (Второй вариант разобран здесь..)

    Посмотрим, как на схеме настраивается блок в котором расположена схема программы обработки (см. рис. 4).

    У нас есть схема, на которую мы размещаем блоки субмодели графического языка программирования, внутри этих блоков находится графическая схема, часть которой приведена на рисунке 3, — программа обработки сигналов с датчиков.

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

    Теперь при генерации кода Си все переменные получат значения нужного датчика. И программист не нужен, технолог все сделал сам когда разрабатывал схему в графическом языке програмирования для алгоритма управления АЭС.

    Рисунок 4. Пример настройки схемы обработки датчика.

    Для присвоения имен служит специальный скрипт автоматики в среде проектирования систем управления, примерно такой, как на рисунке 5. Всем блокам чтения на схеме присваиваются имена, состоящие из имени объекта и имени поля в классе (см. рис. 5).

    Рисунок 5. Настройка имени переменных в блоках чтения.

    Ясно, что аналогичным образом может быть создано неограниченное количество вариантов обработки сигналов, по сути методов для класса в методологии ООП. Точно так же могут быть сформированы для датчика, его подведение при отображении на видеокадрах SCADA системы, или например обработка процедур изменения уставок. Создается схема в графическом виде, сохраняется виде блока и используется при необходимости.

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

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

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

    в Java OOP с примером

    • Home
    • Testing

        • Back
        • Agile Testing
        • BugZilla
        • Cucumber
        • Database Testing
        • 000
        • ETL Testing Database Testing
        • JUnit
        • LoadRunner
        • Ручное тестирование
        • Мобильное тестирование
        • Mantis
        • Почтальон
        • QTP
        • Назад
        • Центр контроля качества (ALM)
        • 000
        • RPA Управление тестированием
        • TestLink
    • SAP

        • Назад
        • ABAP 9 0004
        • APO
        • Начинающий
        • Basis
        • BODS
        • BI
        • BPC
        • CO
        • Назад
        • CRM
        • Crystal Reports
        • QM4
        • 000 HRM
        • Заработная плата
        • Назад
        • PI / PO
        • PP
        • SD
        • SAPUI5
        • Безопасность
        • Менеджер решений
        • Successfactors
        • Учебники SAP

          • Apache
          • AngularJS
          • ASP.Net
          • C
          • C #
          • C ++
          • CodeIgniter
          • СУБД
          • JavaScript
          • Назад
          • Java
          • JSP
          • Kotlin
          • Linux
          • Linux
          • Kotlin
          • Linux
          • js

          • Perl
          • Назад
          • PHP
          • PL / SQL
          • PostgreSQL
          • Python
          • ReactJS
          • Ruby & Rails
          • Scala
          • SQL
          • 000

          • SQL
          • 000

            0003 SQL

            000

            0003 SQL

            000

          • UML
          • VB.Net
          • VBScript
          • Веб-службы
          • WPF
      • Обязательно учите!

          • Назад
          • Бухгалтерский учет
          • Алгоритмы
          • Android
          • Блокчейн
          • Business Analyst
          • Создание веб-сайта
          • CCNA
          • Облачные вычисления
          • 00030003 COBOL 9000 Compiler
              9000 Встроенные системы

            • 00030002 9000 Compiler 9000
            • Ethical Hacking
            • Учебники по Excel
            • Программирование на Go
            • IoT
            • ITIL
            • Jenkins
            • MIS
            • Сеть
            • Операционная система
            • Назад
            • Управление проектами Обзоры
            • Salesforce
            • SEO
            • Разработка программного обеспечения
            • VB A
        • Big Data

            • Назад
            • AWS
            • BigData
            • Cassandra
            • Cognos
            • Хранилище данных
            • 0003

            • HBOps
            • 0003

            • HBOps
            • 0003

            • MicroStrategy

        .

        8.4 — Функции доступа и инкапсуляция

        Автор Alex, 4 сентября 2007 г. | последнее изменение: nascardriver 22 августа 2020 г.

        Зачем делать переменные-члены закрытыми?

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

        В современной жизни у нас есть доступ ко многим электронным устройствам.В вашем телевизоре есть пульт дистанционного управления, который можно использовать для включения / выключения телевизора. Вы едете на работу на машине (или скутере). Вы делаете снимок на свой смартфон. Все эти три вещи используют общий шаблон: они предоставляют простой интерфейс, который вы можете использовать (кнопка, рулевое колесо и т. Д.) Для выполнения действия. Однако то, как на самом деле работают эти устройства, от вас скрыто. Когда вы нажимаете кнопку на пульте дистанционного управления, вам не нужно знать, что он делает для связи с телевизором. Когда вы нажимаете педаль газа на машине, вам не нужно знать, как двигатель внутреннего сгорания заставляет колеса вращаться.Когда вы делаете снимок, вам не нужно знать, как датчики собирают свет в пиксельное изображение. Такое разделение интерфейса и реализации чрезвычайно полезно, поскольку позволяет нам использовать объекты, не понимая, как они работают. Это значительно снижает сложность использования этих объектов и увеличивает количество объектов, с которыми мы можем взаимодействовать.

        По тем же причинам разделение реализации и интерфейса полезно в программировании.

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

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

        В C ++ мы реализуем инкапсуляцию через спецификаторы доступа. Как правило, все переменные-члены класса делаются закрытыми (скрывая детали реализации), а большинство функций-членов становятся общедоступными (открывая интерфейс для пользователя). Хотя требование, чтобы пользователи класса использовали общедоступный интерфейс, может показаться более обременительным, чем предоставление общего доступа к переменным-членам напрямую, это на самом деле дает большое количество полезных преимуществ, которые помогают стимулировать повторное использование класса и удобство обслуживания.

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

        Преимущество: инкапсулированные классы проще в использовании и уменьшают сложность ваших программ

        При полностью инкапсулированном классе вам нужно только знать, какие функции-члены общедоступны для использования этого класса, какие аргументы они принимают и какие значения возвращают.Неважно, как класс был реализован внутри. Например, класс, содержащий список имен, мог быть реализован с использованием динамического массива строк в стиле C, std :: array, std :: vector, std :: map, std :: list или одного из многих других данных. конструкции. Чтобы использовать этот класс, вам не нужно знать (или заботиться), какой именно. Это значительно снижает сложность ваших программ, а также уменьшает количество ошибок. Это ключевое преимущество инкапсуляции больше, чем какая-либо другая причина.

        Все классы стандартной библиотеки C ++ инкапсулированы.Представьте, насколько сложнее был бы C ++, если бы вам нужно было понять, как были реализованы std :: string, std :: vector или std :: cout, чтобы использовать их!

        Преимущество: инкапсулированные классы помогают защитить ваши данные и предотвратить неправильное использование

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

        Например, допустим, мы пишем строковый класс.Мы могли бы начать так:

        class MyString

        {

        char * m_string; // мы динамически разместим здесь нашу строку

        int m_length; // нам нужно следить за длиной строки

        };

        Эти две переменные имеют внутреннее соединение: m_length всегда должна равняться длине строки, содержащейся в m_string (это соединение называется инвариантом).Если бы m_length было общедоступным, любой мог бы изменить длину строки, не изменяя m_string (или наоборот). Это привело бы класс в противоречивое состояние, что могло бы вызвать всевозможные причудливые проблемы. Делая m_length и m_string закрытыми, пользователи вынуждены использовать любые доступные общедоступные функции-члены для работы с классом (и эти функции-члены могут гарантировать, что m_length и m_string всегда устанавливаются соответствующим образом).

        Мы также можем помочь защитить пользователя от ошибок при использовании нашего класса.Рассмотрим класс с переменной-членом открытого массива:

        class IntArray

        {

        public:

        int m_array [10];

        };

        Если пользователи могут получить доступ к массиву напрямую, они могут подписать массив с недопустимым индексом, что приведет к неожиданным результатам:

        int main ()

        {

        Массив IntArray;

        массив.m_array [16] = 2; // неверный индекс массива, теперь мы перезаписали память, которой мы не владеем

        }

        Однако, если мы сделаем массив закрытым, мы можем заставить пользователя использовать функцию, которая сначала проверяет, что индекс действителен:

        класс IntArray

        {

        частный:

        int m_array [10]; // пользователь больше не может получить к нему доступ напрямую

        public:

        void setValue (int index, int value)

        {

        // Если индекс недействителен, ничего не делать

        if (index <0 | | index> = 10)

        возврат;

        m_array [индекс] = значение;

        }

        };

        Таким образом мы защитили целостность нашей программы.В качестве примечания, функции at () std :: array и std :: vector делают нечто очень похожее!

        Преимущество: инкапсулированные классы легче изменить

        Рассмотрим этот простой пример:

        #include

        class Something

        {

        public:

        int m_value1;

        int m_value2;

        int m_value3;

        };

        int main ()

        {

        Что-то что-то;

        что-то.m_value1 = 5;

        std :: cout << something.m_value1 << '\ n';

        }

        Хотя эта программа работает нормально, что произойдет, если мы решим переименовать m_value1 или изменить его тип? Мы сломаем не только эту программу, но, вероятно, и большинство программ, которые также используют class Something!

        Инкапсуляция дает нам возможность изменять способ реализации классов, не нарушая работу всех программ, которые их используют.

        Вот инкапсулированная версия этого класса, который использует функции для доступа к m_value1:

        1

        2

        3

        4

        5

        6

        7

        8

        9

        10

        11

        12

        13

        140002

        14

        18

        19

        20

        #include

        class Something

        {

        private:

        int m_value1;

        int m_value2;

        int m_value3;

        общедоступный:

        void setValue1 (int value) {m_value1 = value; }

        int getValue1 () {return m_value1; }

        };

        int main ()

        {

        Что-то что-то;

        что-то.setValue1 (5);

        std :: cout << something.getValue1 () << '\ n';

        }

        Теперь изменим реализацию класса:

        1

        2

        3

        4

        5

        6

        7

        8

        9

        10

        11

        12

        13

        140002

        14

        18

        19

        20

        #include

        class Something

        {

        private:

        int m_value [3]; // примечание: мы изменили реализацию этого класса!

        public:

        // Мы должны обновить все функции-члены, чтобы отразить новую реализацию

        void setValue1 (int value) {m_value [0] = value; }

        int getValue1 () {return m_value [0]; }

        };

        int main ()

        {

        // Но наша программа по-прежнему работает отлично!

        Что-то что-то;

        что-то.setValue1 (5);

        std :: cout << something.getValue1 () << '\ n';

        }

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

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

        Преимущество: инкапсулированные классы легче отлаживать

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

        Функции доступа

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

        Функция доступа — это короткая общедоступная функция, задача которой — извлекать или изменять значение частной переменной-члена. Например, в классе String вы можете увидеть что-то вроде этого:

        класс MyString

        {

        частный:

        char * m_string; // мы динамически разместим здесь нашу строку

        int m_length; // нам нужно отслеживать длину строки

        public:

        int getLength () {return m_length; } // функция доступа для получения значения m_length

        };

        getLength () — это функция доступа, которая просто возвращает значение m_length.

        Функции доступа обычно бывают двух видов: геттеры и сеттеры. Геттеры (также иногда называемые методами доступа ) — это функции, возвращающие значение частной переменной-члена. Сеттеры (также иногда называемые мутаторами ) — это функции, которые устанавливают значение частной переменной-члена.

        Вот пример класса, у которого есть геттеры и сеттеры для всех его членов:

        1

        2

        3

        4

        5

        6

        7

        8

        9

        10

        11

        12

        13

        140002

        14

        класс Дата

        {

        частный:

        int m_month;

        int m_day;

        int m_year;

        общедоступный:

        int getMonth () {return m_month; } // получатель для месяца

        void setMonth (int month) {m_month = month; } // установщик месяца

        int getDay () {return m_day; } // геттер для дня

        void setDay (int day) {m_day = day; } // установщик дня

        int getYear () {return m_year; } // получатель для года

        void setYear (int year) {m_year = year; } // установщик для года

        };

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

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

        Геттеры

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

        Геттеры должны возвращаться по значению или константной ссылке.

        Функции доступа

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

        А пока мы рекомендуем прагматичный подход. При создании классов учитывайте следующее:

        • Если никому за пределами вашего класса не требуется доступ к члену, не предоставляйте функции доступа для этого члена.
        • Если кому-то за пределами вашего класса нужен доступ к члену, подумайте, можете ли вы вместо этого раскрыть поведение или действие (например,г. вместо setAlive (bool) setter реализуйте функцию kill ()).
        • Если нет, подумайте, можете ли вы предоставить только геттер.

        Сводка

        Как видите, инкапсуляция дает множество преимуществ при небольших дополнительных усилиях. Основное преимущество заключается в том, что инкапсуляция позволяет нам использовать класс, не зная, как он был реализован. Это значительно упрощает использование классов, с которыми мы не знакомы.

        .

        Абстракция против инкапсуляции в OOPS с примерами

        В этой статье объясняется часто обсуждаемая и обсуждаемая тема «Абстракция против инкапсуляции» в контексте принципов OOPS (объектно-ориентированного программирования).

        Слушайте на ходу… Аудиоверсия сообщения в блоге

        Что такое абстракция
        Абстракция относится к концепции сокрытия сложностей системы от пользователей этой системы.

        Пример абстракции. Допустим, у нас есть мобильное приложение для получения последней котировки акций.Для пользователя это так же просто, как ввести название компании или код акций компании в приложении и нажать кнопку «получить». Внутренне приложение будет выполнять последовательность шагов, таких как подключение к подключению к данным / Wi-Fi, а затем вызов RESTful API на внутреннем сервере. Этот внутренний сервер обратится к базе данных или сделает еще один вызов внешнему поставщику услуг котировок акций, чтобы получить котировку акций. Фактическая котировка акций затем перемещается в обратном направлении по цепочке и отображается в приложении.

        Для пользователя приложения всего 2 простых шага: ввести код акций и нажать кнопку выборки, но на самом деле вся работа, как объяснено выше, абстрагируется от пользователя.

        Что такое инкапсуляция
        Инкапсуляция — это языковая конструкция, объединяющая данные и поведение. Кроме того, он ограничивает доступ к этим данным и поведению вместе.

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

        Где возникает путаница
        Идея, при которой большинство разработчиков путается между абстракцией и инкапсуляцией, заключается в том, что они пытаются связать их со скрытием данных.

        Абстракция также скрывает, но, как объяснялось выше, абстракция скрывает сложность . С другой стороны, инкапсуляция сохраняет данные / поведение, которые она инкапсулирует, вместе и контролирует доступ к ним. Другими словами, инкапсуляция скрывает конструкции, которые она инкапсулирует .

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

        Авторские права © 2014-2020 JavaBrahman.com, все права защищены. .

    Добавить комментарий

    Ваш адрес email не будет опубликован. Обязательные поля помечены *