Урок №115. Инкапсуляция, Геттеры и Сеттеры

  Юрий  | 

  |

  Обновл. 13 Сен 2021  | 

 111121

 ǀ   12 

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

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

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

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

Таким образом, мы защитим целостность нашей программы.

Примечание: Функция at() в std::array и std::vector делает что-то похожее!

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

Рассмотрим следующий простой пример:

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

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

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

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

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

Преимущество №4: С инкапсулированными классами легче проводить отладку.

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

Функции доступа (геттеры и сеттеры)

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

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

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

Функции доступа обычно бывают двух типов:

   геттеры — это функции, которые возвращают значения закрытых переменных-членов класса;

   сеттеры — это функции, которые позволяют присваивать значения закрытым переменным-членам класса.

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

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

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

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

Правило: Геттеры должны использовать тип возврата по значению или по константной ссылке. Не используйте для геттеров тип возврата по неконстантной ссылке.

Заключение


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

   проще в использовании;

   уменьшают сложность программы;

   защищают данные и предотвращают их неправильное использование;

   легче изменять;

   легче отлаживать.

Это значительно облегчает использование классов.

Оценить статью:

Звёзд: 1Звёзд: 2Звёзд: 3Звёзд: 4Звёзд: 5 (423 оценок, среднее: 4,91 из 5)
Загрузка...

Комментариев: 12

  1. Дмитрий:

    Правильно ли я понял, что чтобы динамически выделить память для члена — переменной класса достаточно объявить указатель на переменную или массив, а компилятор будет понимать char *m_string как char *m_string = new char?

  2. Дмитрий:

    И почему при выделении строки мы пишем: char *m_string; , а не std:string *m_string;?

  3. Дмитрий:

    У вас написано:

    А в уроке 86 сказано, что для динамического выделения памяти используется оператор new, а чтобы воспользоваться этой памятью нужно создать указатель. Если это так, то почему в данном примере присутствует только указатель?

  4. Денис Фролов:

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

    1. Aleksey:

      Спасибо за пояснение!

  5. Fandre:

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

  6. Никита:

    И всё-таки :
    «Инкапсуляция имеет много преимуществ, основное из которых — это использовать класс без необходимости понимания его реализации.»

    Я не понимаю, почему если я оставлю класс открытым , это значит что я сразу должен его весь понимать? Почему сейчас например класс “string” закрытый , и мне не нужно понимать как он там работает, я могу им просто пользоваться. А если в этом Классе изменить private на public, то мне резко надо будет изучать этот класс вдоль и поперёк ?

    Я понимаю что будь он public, это значит что его можно «крашнуть», он становится уязвимым . Но причём сдесь «не надо в нем разбираться чтобы пользоваться ,потому что часть его содержимого под грифом «private» — не пойму.

    1. Александр:

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

  7. Ануар:

    Основное преимущество инкапсуляции – использовать класс без необходимости знать его реализацию.

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

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

    1. kenjyminamori:

      Я думаю автор несколько неоднозначен выразил свою мысль. В статье раскрывается главный смысл инкапсуляции как механизм для создания ИНТЕРФЕЙСОВ(см Википедию). По-сути, это и является "не знанием реализации".

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

  8. Торос:

    "Если бы пользователи могли бы напрямую обращаться к массиву, то они могли бы использовать недопустимый индекс:
    Однако, если мы сделаем массив закрытым, то мы сможем заставить пользователя использовать функцию, которая, первым делом, проверяет корректность индекса:"
    Кто имеется ввиду под пользователем? Это программист который использует класс? Или пользователь программы? не понимаю

    1. Фото аватара Юрий:

      Пользователь — тот, кто использует программу (не программист, который её пишет).

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

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