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

From Wikipedia, the free encyclopedia

In computer programming, field encapsulation involves providing methods that can be used to read from or write to the field rather than accessing the field directly. Sometimes these accessor methods are called getX and setX (where X is the field’s name), which are also known as mutator methods. Usually the accessor methods have public visibility while the field being encapsulated is given private visibility — this allows a programmer to restrict what actions another user of the code can perform. Compare the following Java class in which the name field has not been encapsulated:

public class NormalFieldClass {
    public String name;
 
    public static void main(String[] args)
    {
        NormalFieldClass example1 = new NormalFieldClass();
        example1.name = "myName";
        System.out.println("My name is " + example1.name);
    }
}

with the same example using encapsulation:

public class EncapsulatedFieldClass {
    private String name;
 
    public String getName()
    {
        return name;
    }
 
    public void setName(String newName)
    {
        name = newName;
    }
 
    public static void main(String[] args)
    {
      EncapsulatedFieldClass example1 = new EncapsulatedFieldClass();
      example1.setName("myName");
      System.out.println("My name is " + example1.getName());
    }
}

In the first example a user is free to use the public name variable however they see fit — in the second however the writer of the class retains control over how the private name variable is read and written by only permitting access to the field via its getName and setName methods.

Advantages[edit]

  • The internal storage format of the data is hidden; in the example, an expectation of the use of restricted character sets could allow data compression through recoding (e.g., of eight bit characters to a six bit code). An attempt to encode characters out of the range of the expected data could then be handled by casting an error in the set routine.
  • In general, the get and set methods may be produced in two versions — an efficient method that assumes that the caller is delivering appropriate data and that the data has been stored properly, and a debugging version that while slower, performs validity checks on data received and delivered. Such detection is useful when routines (calling or called) or internal storage formats are newly created or modified.
  • The location of the stored data within larger structures may be hidden and so enabling changes to be made to this storage without the necessity of changing the code that references the data. This also reduces the likelihood of unexpected side effects from such changes. This is especially advantageous when the accessors are part of an operating system (OS), a case where the calling (application) code may not be available to the developers of the OS.

Disadvantages[edit]

Access to a subroutine involves additional overhead not present when data is accessed directly. While this is becoming of less concern with the wide availability of fast general-purpose processors it may remain important in coding some real-time computing systems and systems using relatively slow and simple embedded processors. In some languages, like C++, the getter / setter methods are usually inline functions, so that when inlining is performed, the code looks just like direct field accessing.

From Wikipedia, the free encyclopedia

In computer programming, field encapsulation involves providing methods that can be used to read from or write to the field rather than accessing the field directly. Sometimes these accessor methods are called getX and setX (where X is the field’s name), which are also known as mutator methods. Usually the accessor methods have public visibility while the field being encapsulated is given private visibility — this allows a programmer to restrict what actions another user of the code can perform. Compare the following Java class in which the name field has not been encapsulated:

public class NormalFieldClass {
    public String name;
 
    public static void main(String[] args)
    {
        NormalFieldClass example1 = new NormalFieldClass();
        example1.name = "myName";
        System.out.println("My name is " + example1.name);
    }
}

with the same example using encapsulation:

public class EncapsulatedFieldClass {
    private String name;
 
    public String getName()
    {
        return name;
    }
 
    public void setName(String newName)
    {
        name = newName;
    }
 
    public static void main(String[] args)
    {
      EncapsulatedFieldClass example1 = new EncapsulatedFieldClass();
      example1.setName("myName");
      System.out.println("My name is " + example1.getName());
    }
}

In the first example a user is free to use the public name variable however they see fit — in the second however the writer of the class retains control over how the private name variable is read and written by only permitting access to the field via its getName and setName methods.

Advantages[edit]

  • The internal storage format of the data is hidden; in the example, an expectation of the use of restricted character sets could allow data compression through recoding (e.g., of eight bit characters to a six bit code). An attempt to encode characters out of the range of the expected data could then be handled by casting an error in the set routine.
  • In general, the get and set methods may be produced in two versions — an efficient method that assumes that the caller is delivering appropriate data and that the data has been stored properly, and a debugging version that while slower, performs validity checks on data received and delivered. Such detection is useful when routines (calling or called) or internal storage formats are newly created or modified.
  • The location of the stored data within larger structures may be hidden and so enabling changes to be made to this storage without the necessity of changing the code that references the data. This also reduces the likelihood of unexpected side effects from such changes. This is especially advantageous when the accessors are part of an operating system (OS), a case where the calling (application) code may not be available to the developers of the OS.

Disadvantages[edit]

Access to a subroutine involves additional overhead not present when data is accessed directly. While this is becoming of less concern with the wide availability of fast general-purpose processors it may remain important in coding some real-time computing systems and systems using relatively slow and simple embedded processors. In some languages, like C++, the getter / setter methods are usually inline functions, so that when inlining is performed, the code looks just like direct field accessing.

Автор: Баз Дейкстра (Bas Dijkstra)
Оригинал статьи
Перевод: Ольга Алифанова

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

  • Инкапсуляция (эта статья)
  • Наследование
  • Полиморфизм
  • Абстракция

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

Примеры, которые я даю, в основном будут на Java, но в ходе этой серии статей я упомяну, как внедрять эти концепции, по возможности, в C# и Python.

Что такое инкапсуляция?

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

Инкапсуляция: пример

Чтобы проиллюстрировать концепцию инкапсуляции и ее важности, предположим, что у нас есть класс Account, представляющий собой банковский счет, с двумя свойствами: тип счета (моделируется с использованием enum Account Type, который может принимать значения CHECKING и SAVINGS), и баланс счета, моделируемый как число двойной точности. Крайне наивная реализация такого класса может выглядеть примерно так:

public class Account {
    public AccountType type;
public double balance;
    public Account(AccountType type) {
        this.type = type;
        this.balance = 0;
    }
}

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

Куда лучшим вариантом будет применить инкапсуляцию. Чтобы это сделать, мы сделаем сами свойства скрытыми, ограничивая прямой доступ к ним самим классом Account. Затем мы выдадим доступ к свойству через публичные методы, попутно навязывая ряд бизнес-правил:

public class Account {
    private AccountType type;
private double balance;
    public Account(AccountType type) {
this.type = type;
this.balance = 0;
}
    public double getBalance() {
return this.balance;
}
    public void deposit(double amount) throws DepositException {
if (amount < 0) {
throw new DepositException("You cannot deposit a negative amount!");
}
this.balance += amount;
}
    public void withdraw(double amount) throws WithdrawException {
if (amount < 0) {
throw new WithdrawException("You cannot withdraw a negative amount!");
}
if (amount > this.balance && this.type.equals(AccountType.SAVINGS)) {
throw new WithdrawException("You cannot overdraw on a savings account!");
}
this.balance -= amount;
}
}

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

Это значительно снизит нежелательное поведение в системе. Однако все равно стоит написать ряд тестов для этого класса!

Инкапсуляция на других языках

В C# вы можете сделать нечто подобное тому, что мы уже рассмотрели в Java. В качестве примера я, возможно, инициализирую свойство баланса вот так:

public double Balance { get; private set; }

Это дает окружающему миру доступ для чтения самого свойства баланса, но обновляться оно сможет только изнутри самого класса Account. Как и в примере с Java, мы можем затем выдать доступ к балансу через публичные методы Deposit (число двойной точности) и Withdraw (число двойной точности), которые принуждают к исполнению требуемой бизнес-логики.

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

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

class Account:
    def __init__(self, type):
        self._type = type
        self._balance = 0

Чтобы действительно, действительно сообщить, что свойство приватно, можно добавить двойной префикс (__). Это делает свойство невидимым для пользователей класса (на него нельзя напрямую сослаться, скажем, через account.balance), но все еще не предотвращает доступ стороннего кода, потому что все еще существует подгонка имен Python. По сути доступ к свойству __balance объекта счета типа Account все еще можно получить через account._Account__balance.

Инкапсуляция в автоматизации

Вероятно, наиболее известное и широко используемое применение принципа инкапсуляции в автоматизации – это Page Object, широко используемые в автоматизации через пользовательский интерфейс при помощи таких инструментов, как Selenium Web Driver, Playwright и Cypress.

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

Вот пример объекта LoginPage (в Selenium WebDriver), прячущего детали реализации (локаторы элемента) в частных свойствах, и дающего доступ к ним через обычные тест-действия, определенные в публичных методах (в этом случае это авторизация в приложении через метод loginAs()):

public class LoginPage {
    private final By textfieldUsername = By.name("username");
private final By textfieldPassword = By.name("password");
private final By buttonDoLogin = By.xpath("//input[@value='Log In']");
    public LoginPage(WebDriver driver) {
        driver.get("https://parabank.parasoft.com/parabank");
}
    public void loginAs(String username, String password) {
        sendKeys(textfieldUsername, username);
sendKeys(textfieldPassword, password);
click(buttonDoLogin);
}
}

«Пользователь» такого класса Page Object (обычно тест-метод) может затем взаимодействовать с элементами, определенными в классе, только через публичный метод, не имея прямого доступа к (и без нужды заморачиваться с) деталям внедрения страницы – таким, как структура HTML и любая синхронизация состояния элементов. Это приводит к чистому API для класса page object и лучшему разделению интересов.

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

Обсудить в форуме

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

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

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

Для чего нужна инкапсуляция

Инкапсуляция считается одним из четырех основных принципов ООП — объектно-ориентированного программирования. Этот подход представляет сущности в коде как классы и объекты, позволяет «строить» код из них, как из кирпичиков. Но чтобы объекты грамотно выполняли свои задачи и не ломались, нужна инкапсуляция.

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

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

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

Инкапсуляция, абстракция и сокрытие данных

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

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

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

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

Соответственно, различаются и реализации обоих принципов. Хотя тут многое зависит от языка программирования: в некоторых языках, например, C++, инкапсуляция без сокрытия считается бессмысленной. А в других, таких как Python, есть инкапсуляция, но нет сокрытия. Есть и языки, которые жестко разделяют два понятия — так, что они описываются по-разному.

Как выглядит реализация инкапсуляции

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

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

Функции внутри объекта называются методами, а данные — свойствами. Они разные, но объединены внутри одной сущности, как и предписывает принцип инкапсуляции.

Как разработчики инкапсулируют данные

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

  • Свойства — это переменные, «лежащие» внутри объектов. Они могут быть любого типа, а иногда и сами являются объектами. В переменных хранятся данные. Если какая-то переменная является свойством, ее содержимое по умолчанию скрыто. Как открывать и запрещать доступ к свойствам, мы расскажем ниже.
  • Методы — это функции внутри объектов. Они не просто хранят данные, а совершают какое-то действие. Важное отличие метода от обычной функции — его можно вызвать только для объекта, в котором он находится. Вызов метода отличается и внешне: в большинстве языков сначала пишется имя объекта, а потом, через точку — название метода и аргументы. А еще для вызова метода ничего не нужно импортировать — все уже есть в объекте, и это еще один пример целостности инкапсулированной сущности.

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

  • Геттер — это метод объекта, который возвращает значение свойства этого объекта. Например, у объекта в свойстве value написано 15. Тогда геттер getValue() будет возвращать 15.
  • Сеттер — это метод, который изменяет значение свойства, задает его (set). Например, гипотетический метод setValue(x) будет менять значение свойства value. Аргумент x — это новое значение свойства, при вызове сеттера туда подставляются данные.

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

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

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

  • public — объект, функция или метод доступны для всех. Другие сущности могут читать оттуда данные и изменять их. Свойство публичного объекта можно получить, просто написав его название через точку: <имя объекта>.<имя свойства>. С такими данными легко работать, для них не нужны геттеры и сеттеры, но информация оказывается уязвима.
  • private — содержимое объекта доступно только для других его составных частей. Например, метод объекта может вызывать данные из этого же объекта напрямую — а какой-нибудь код снаружи уже не сможет.
  • protected — содержимое объекта доступно ему самому и его производным, например, потомкам.

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

В некоторых языках, например, C#, есть еще вариант internal — доступ открыт только из одного файла. В других файлах нельзя будет создать такой объект. Такой модификатор задается классу.

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

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

А слово sealed, наоборот, «запечатывает» класс и запрещает создавать его потомков. Это нужно для защиты от ситуаций, когда наследование может сломать работу кода.

Как начать работать с инкапсуляцией

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

Если вы интересуетесь IT — приглашаем на курсы! Выберите и получите новую профессию, востребованную на рынке.

Добавлено 3 июля 2021 в 14:11

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

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

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

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

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

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

В 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 array;
    array.m_array[16] = 2; // недопустимый индекс массива, теперь мы
                           // перезаписали память, которой мы не владеем
}

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

#include <iterator> // для std::size()
 
class IntArray
{
private:
    int m_array[10]; // пользователь больше не может получить к нему доступ напрямую
 
public:
    void setValue(int index, int value)
    {
        // Если значение индекса недопустимо, ничего не делать
        if (index < 0 || index >= std::size(m_array))
            return;
 
        m_array[index] = value;
    }
};

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

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

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

#include <iostream>
 
class Something
{
public:
    int m_value1;
    int m_value2;
    int m_value3;
};
 
int main()
{
    Something something;
    something.m_value1 = 5;
    std::cout << something.m_value1 << 'n';
}

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

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

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

#include <iostream>
 
class Something
{
private:
    int m_value1;
    int m_value2;
    int m_value3;
 
public:
    void setValue1(int value) { m_value1 = value; }
    int getValue1() { return m_value1; }
};
 
int main()
{
    Something something;
    something.setValue1(5);
    std::cout << something.getValue1() << 'n';
}

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

#include <iostream>
 
class Something
{
private:
    int m_value[3]; // обратите внимание: мы изменили реализацию этого класса!
 
public:
    // Мы должны обновить все функции-члены, чтобы отразить новую реализацию
    void setValue1(int value) { m_value[0] = value; }
    int getValue1() { return m_value[0]; }
};
 
int main()
{
    // Но наша программа по-прежнему работает нормально!
    Something something;
    something.setValue1(5);
    std::cout << something.getValue1() << 'n';
}

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

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

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

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

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

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

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

class MyString
{
private:
    char *m_string; // здесь мы динамически размещаем нашу строку
    int m_length;   // нам нужно отслеживать длину строки
 
public:
    int getLength() { return m_length; } // функция доступа для получения значения m_length
};

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

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

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

class Date
{
private:
    int m_month;
    int m_day;
    int m_year;
 
public:
    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), реализуйте функцию kill()).
  • Если не можете, подумайте, можете ли вы предоставить только геттер.

Резюме

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

Теги

C++ / Cppgetter / геттер / аксессорLearnCppsetter / сеттер / мутаторДля начинающихИнкапсуляцияОбучениеПрограммированиеФукнция доступа

Что такое инкапсуляция?

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

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

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

Не теряя времени, разберем пример, наглядно демонстрирующий рассматриваемую концепцию и то, как она работает в Java.

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

Рассмотрим ее реализацию на примере, описывающем работу кофемашины.

Создадим класс Machinе с атрибутами config, beans, grinder и brewingUnit , хранящими текущее состояние объекта. Методы brewCoffee , brewEspresso , brewFilterCoffee и addBeans реализуют набор операций над этими атрибутами.

Пример 1

        import java.util.HashMap;
import java.util.Map;

public class Machinе{
    private Map config;
    private Map beans;
    private Grinder grinder;
    private BrewingUnit brewingUnit;

    public Machinе(Map beans) {
        this.beans = beans;
        this.grinder = new Grinder();
        this.brewingUnit = new BrewingUnit();
        this.config = new HashMap();
        this.config.put(CoffeeSelection.ESPRESSO, new Configuration(8, 28));
        this.config.put(CoffeeSelection.FILTER_COFFEE, new Configuration(30, 480));
    }

    public Coffee brewCoffee(CoffeeSelection selection) throws CoffeeException {
        switch (selection) {
            case FILTER_COFFEE:
                return brewFilter();
            case ESPRESSO:
                return brewEspresso();
            default:
                throw new CoffeeException("CoffeeSelection [" + selection + "] not supported!");
        }
    }

    private Coffee brewEspresso() {
        Configuration config = configMap.get(CoffeeSelection.ESPRESSO);

        // смолоть кофейные зерна
        GroundCoffee groundCoffee = this.grinder.grind(
            this.beans.get(CoffeeSelection.ESPRESSO), config.getQuantityCoffee());

        // сварить эспрессо
        return this.brewingUnit.brew(CoffeeSelection.ESPRESSO, 
            groundCoffee, config.getQuantityWater());
    }

    private Coffee brewFilter() {
        Configuration config = configMap.get(CoffeeSelection.FILTER_COFFEE);

        // смолоть кофейные зерна снова
        GroundCoffee groundCoffee = this.grinder.grind(
            this.beans.get(CoffeeSelection.FILTER_COFFEE), config.getQuantityCoffee());

        // режим однократного пропуска горячей воды через слой кофе
        return this.brewingUnit.brew(CoffeeSelection.FILTER_COFFEE, 
            groundCoffee, config.getQuantityWater());
    }

    public void addBeans(CoffeeSelection sel, CoffeeBean newBeans) throws CoffeeException {
        CoffeeBean existingBeans = this.beans.get(sel);

        if (existingBeans != null) {
            if (existingBeans.getName().equals(newBeans.getName())) {
                existingBeans.setQuantity(existingBeans.getQuantity() + newBeans.getQuantity());
            } else {
                throw new CoffeeException("Для каждого режима поддерживается только один вид зерен.");
            }
        } else {
            this.beans.put(sel, newBeans);
        }
    }
}

    

Скрытие информации в Java

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

Модификаторы доступа

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

  1. private.
  2. no modifier.
  3. protected.
  4. public.

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

Модификатор private

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

В нашем примере мы используем его, чтобы ограничить доступ ко всем атрибутам и методам brewEspresso и brewFilter. Эти атрибуты и методы должны использоваться только в классе Machine и не являются частью общедоступного API. Вначале это может показаться немного запутанным. Однако очень полезно, когда классы в вашем пакете реализуют четко определенный набор логики. Это также удобно, если вы хотите управлять API, доступным для классов за пределами этого пакета. Впоследствии вы можете использовать видимость пакета для реализации метода, используемого только классами в этом пакете.

Модификатор no modifier

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

Модификатор protected

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

Модификатор public

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

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

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

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

Пример 2

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

        public class Coffee {
    private CoffeeSelection selection;
    private double quantity;

    public Coffee (CoffeeSelection selection, double quantity) {
        this.selection = selection;
        this.quantity = quantity;
    }

    public CoffeeSelection getSelection() {
        return selection;
    }

    public double getQuantity() {
        return quantity;
    }

    public void setQuantity(double quantity) throws CoffeeException {
        if (quantity >= 0.0) {  
            this.quantity = quantity;
        } else {
            throw new CoffeeException("Количество должно быть >= 0.0");
        }
    }
}

    

Класс Coffee использует два закрытых атрибута для хранения информации: CoffeeSelection и quantity. Модификатор доступа private делает оба атрибута недоступными для других классов в том же или других пакетах. Если вы хотите получить информацию о текущем состоянии объекта, вы можете вызвать один из общедоступных методов.

Метод getSelection обеспечивает доступ для чтения к атрибуту selection . Он представляет тип кофе, приготовленный кофемашиной. Как видно из фрагмента кода, мы не реализовали метод set для этого атрибута, поскольку мы не можем изменить сорт кофе после того, как он заварен. Доступное количество напитка меняется со временем. После каждого глотка в вашей чашке остается немного меньше. Поэтому мы реализовали метод получения и установки для атрибута quantity .

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

***

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

Материалы по теме

  • ☕ Учебник по Java: cтатический и динамический полиморфизм
  • ☕ Разбираем на простых примерах: наследование в Java

Перевод статьи Eric Elliott — Encapsulation in JavaScript

Инкапсуляция — это объединение данных и методов, которые воздействуют на эти данные, так что доступ к этим данным ограничен извне. Или как описывает это Алан Кей, «инкапсуляция — это локальное хранение и защита и скрытие процесса состояния». В ООП это означает, что объект хранит свое состояние в приватном порядке, и только методы объекта имеют доступ для его изменения.

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

Аналогично, когда вы хотите обновить состояние компонента React с помощью useState или setState, эти изменения не влияют непосредственно на состояние компонента. Вместо этого они ставятся в очередь потенциальные изменения состояния, которые применяются после завершения цикла рендеринга. Вы напрямую не устанавливаете состояние компонента React, это делает сам React;

Почему инкапсуляция?

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

В 1970-х годах Алан Кей был вдохновлен составными структурами данных из диссертации Ивана Сазерленда Sketchpad, разработанной в 1961–1963 годах в Массачусетском технологическом институте и Симуле, разработанной в 1960-х годах Оле-Йоханом Далем и Кристен Найгаард из Норвежского вычислительного центра в Осло. Алан Кей принимал участие в исследованиях и разработках ARPAnet, имел опыт работы в науке и математике и был особенно вдохновлен тем, как клетки инкапсулируются мембраной и передаются через передачу сообщений.

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

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

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

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

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

// Эта реализация работает только с массивами
const doubleAllImperative = data => {
  const doubled = [];
  for (let i = 0, length = data.length; i < length; i++) {
    doubled[i] = data[i] * 2;
  }
  return doubled;
};

// То же самое что и выше но работает с чем угодно через оператор map
const doubleAllInterface = data => data.map(x => x * 2);
const box = value => ({
  map: f => box(f(value)),
  toString: () => `box(${ value })`
});

console.log(
  doubleAllInterface([2,3]), // [4, 6]
  doubleAllInterface(box(2)).toString(), // box(4)
);

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

  1. Избегайте общего изменяемого состояния. «Недетерминизм = параллельная обработка + изменяемое состояние» — Мартин Одерски, дизайнер языка программирования Scala
  2. Программируйте интерфейс, а не реализацию. — Банда четырех, «Design Patterns: Elements of Reusable Object Oriented Software”»
  3. Небольшое изменение требований должно приводить к соответственно небольшому изменению приложения. — N. D. Birrell, M. A. Ould, “A Practical Handbook for Software Development”

Когда Брендан Айх создал JavaScript в эти роковые 10 дней, кульминацией которых стал выпуск в 1995 году, он задумал две идеи:

  • Использования языка программирования Scheme в браузере
  • Все должно выглядеть как в языке Java

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

Java — это объектно-ориентированный язык на основе классов. От Java JavaScript получил понятие функций конструктора, (в конце концов) классы и ключевого слова new (среди прочего).

Так же Брендан Айх черпал свое вдохновение от языка программирования Self. От него он взял прототипное наследование, которые делают JavaScript гораздо более мощным и гибким, чем его аналогично названный, но в остальном дальний родственник, Java, но это уже другая история.

Спустя 23 года этот парадигмальный плавильный котел все еще немного неправильно понят. Одно из таких распространенных недоразумений связано с инкапсуляцией.

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

До появления предложения о приватных полях (private fields proposal) в ECMAScript не было никакого способа создать честное приватное свойство для объектов в JavaScript. Вместо того, чтобы использовать замыкания, с помощью которых обычно создают приватные данные для объектов в JavaScript, некоторые разработчики решили обозначить частные свойства и методы, добавив к ним префикс подчеркивания, и это стало общепризнанным (хотя и неуклюжим и спорным) соглашением.

Это спорно по нескольким причинам:

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

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

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

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

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

Настоящая инкапсуляция в JavaScript

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

Используем Замыкание

const createCounter = () => {
  // Переменная, определенная в области действия конструктора
  // является приватной для этой функции. 

  let count = 0;
  return ({
    // Любые другие функции, определенные в той же области, являются привилегированными:
     // Они оба имеют доступ к закрытой переменной `count`
     // определяется в любом месте их цепочки областей видимости 
    click: () => count += 1,
    getCount: () => count.toLocaleString()
  });
};
const counter = createCounter();
counter.click();
counter.click();
counter.click();
console.log(
  counter.getCount()
);

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

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

Использование приватных полей

На момент написания этой статьи закрытые поля стали доступны в babel с включенной функцией stage-3. Они так же поддерживаются в Chrome, Opera, браузере Android и Chrome для Android, поэтому есть хороший шанс, что вы можете сделать что-то вроде этого:

class Counter {
  #count = 0
  
  click () {
    this.#count += 1;
  }
  getCount () {
    return this.#count.toLocaleString()
  }
}

const myCounter = new Counter();
myCounter.click();
myCounter.click();
myCounter.click();
console.log(
  myCounter.getCount()
);

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


Эрик Эллиотт — автор книг «Composing Software» и «Programming JavaScript Applications». Как соучредитель EricElliottJS.com и DevAnywhere.io, он обучает разработчиков основным навыкам разработки программного обеспечения. Создает и консультирует команды разработчиков для криптографических проектов, а также вносит свой вклад в опыт разработки программного обеспечения для Adobe Systems, Zumba Fitness, The Wall Street Journal, ESPN, BBC и ведущих исполнителей звукозаписи, включая Usher, Frank Ocean, Metallica и многих других.

Была ли вам полезна эта статья?

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

Практический пример

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

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

<?php
class Account
{
    private $balance = 0;
    private $account = [];

    protected function save()
    {
        // сохраним в файл
        file_put_contents('account.txt', json_encode($this));
    }

    public function add($amount, $explanation)
    {
        $this->balance += $amount;
        $this->account[] = [$amount, $explanation];
        $this->save();
    }

    public function withdraw($amount, $explanation)
    {
        $this->balance -= $amount;
        $this->account[] = [-$amount, $explanation];
        $this->save();
    }

    /* Пропустим другие методы */
}

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

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

<?php
class AccountConsumer
{
    function addToAccount(Account $account)
    {
        // этой функции нет никакой разницы от того, использует
        // ли счет какую-то базу данных или файлы - вся разница
        // в работе счета инкапсулирована
        $account->add($this->totalCharge, $this->getExplanation());
    }

    /* Пропустим другие методы */
}

Функция addToAccount будет одинаково хорошо работать как с экземпляром класса Account, так и с экземпляром класса DatabaseAccount:

<?php
class DatabaseAccount extends Account
{
    protected function save()
    {
        // сохраним в базу данных
        foreach ($this->account as list ($amount, $explanation)) {
            $this->database->updateRec($amount, $explanation);
        }
    }

    /* Пропустим другие методы */
}

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

Зачем ещё это может быть нужно?

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

#Руководства

  • 9 окт 2019

  • 19

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

vlada_maestro / shutterstock

Евгений Кучерявый

Пишет о программировании, в свободное время создаёт игры. Мечтает открыть свою студию и выпускать ламповые RPG.

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

  • Что такое классы и объекты.
  • Особенности работы с объектами.
  • Модификаторы доступа, инкапсуляция.
  • Полиморфизм и перегрузка методов.
  • Полиморфизм.
  • Наследование и ещё немного полиморфизма.
  • Абстрактные классы и интерфейсы.
  • Практикум.

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

А вот у водителя, скажем, ВАЗ 2105 с механической коробкой передач больше свободы действий, но с ней приходит и большая ответственность. Если игнорировать внутреннее устройство автомобиля, то можно «подпалить сцепление» или вывести из строя выжимной подшипник.

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

Описанный выше принцип и называется инкапсуляцией. Процессы, происходящие под капотом автомобиля, скрыты от водителя, а управление осуществляется через удобный и безопасный «интерфейс» — руль и педали. Такой же принцип лежит в основе ООП.

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

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

Первый уровень, с которым сталкиваются все разработчики, — публичный. Чтобы сказать компилятору, что что-то должно быть доступно для всех, используется ключевое слово public.

Рассмотрим на примере класса Item:

public class Item
{
    public string name;
    public int cost;
 
    public Item(string name, int cost)
    {
        this.name = name;
        this.cost = cost;
    }
}

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

Item sword = new Item("Sword of Destiny", 500000);
 
Console.WriteLine($"You've got {sword.name}! It costs {sword.cost} rubles.");

Так как поля публичные, в консоли они отобразятся без каких-либо проблем:

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

sword.cost = 50;
Console.WriteLine($"You've got {sword.name}! It costs {sword.cost} rubles.");

Из-за того, что поле публичное, оно изменится:

Это плохо по нескольким причинам:

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

Разумеется, это не лучшее, что может случиться с приложением.

Чтобы поля были защищены от вмешательства, используется ключевое слово private — оно делает члены класса доступными только внутри самого класса.

public class Item
{
    private string name;
    private int cost;
 
    public Item(string name, int cost)
    {
        this.name = name;
        this.cost = cost;
    }
}

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

Есть два способа сделать поле доступным только для чтения. Первый — использовать ключевое слово readonly, но оно запрещает менять значение вообще.

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

public string GetName()
{
    return this.name;
}
 
public int GetCost()
{
    return this.cost;
}

К такой практике прибегают Java-разработчики, но в C# есть более элегантный способ — свойства.

public string Name 
{ //Объявление свойства. Свойства принято называть так же, как и поля, но начинаются они с заглавной буквы
    get //Конструкция get (также она называется геттером) позволяет определить логику получения значения
    {
        return this.name;
    }
 
    set //Конструкция set (сеттер) позволяет определить логику изменения значения
    {
        this.name = value; //value — это значение, которое передаётся свойству
    }
}

Теперь, чтобы получить данные, нужно обратиться к свойству, а не к полю:

sword.Name = "Sword of Protection"; //Значения для свойства передаются так же, как и для обычной переменной
 
Console.WriteLine($"You've got {sword.Name}!");

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

Обратите внимание, что можно просто написать set; или get; если не требуется дополнительная логика. Это сработает, если у поля и свойства одинаковые имена и если это примитивный тип (int, float, char, double и другие). Со ссылочными типами (объекты и строки) это не работает.

Также можно менять логику работы со значением:

public int Cost
{
    get
    {
        return this.cost;
    }
 
    set
    {
        if(value > 0)
        {
            this.cost = value;
        }
    }
}

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

То есть если запустить вот такой код:

sword.Cost = 50;
sword.Cost = -1;
 
Console.WriteLine($"Sword's cost: {sword.Cost}!");

То выведено будет 50, а не -1:

Также можно создавать свойства без поля:

public bool IsExpensive
{
    get
    {
        if(this.cost > 5000)
        {
            return true;
        }
        else
        {
            return false;
        }
    }
}

Это свойство вернёт true, если цена выше 5000, и false, если ниже.

Ключевое слово private можно также применять и к методам. Это делает их доступными только внутри класса.

private void Wipe()
{
    this.name = "";
    this.cost = 0;
}

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

public class Char
{
 
    private Collider collider = new Collider();
 
    private class Collider
    {
 
        public int x = 5;
        public int y = 10;
 
        public int width = 15;
        public int height = 25;
 
    }
}

Иногда нужно сделать компонент доступным только внутри одного файла — например, в Program.cs, Item.cs или любом другом. Для этого используется ключевое слово internal.

class Program
{
    static void Main(string[] args)
    {
        Backpack b = new Backpack();
        Console.WriteLine($"{b.itemsCount} items in the backpack.");
    }
}
 
internal class Backpack
{
    public int itemsCount = 10;
}

Класс Backpack можно будет использовать только внутри файла Program.cs, и попытка объявить его внутри другого файла приведёт к ошибке.

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

Например:

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine($"5 + 6 = {Calc.Sum(5, 6)}");
    }
}
 
class Calc
{
    public static int Sum(int a, int b) //Создание статичного метода
    {
        return a + b;
    }
}

Метод Sum () используется в классе Program, хотя экземпляр класса Calc не создавался. При этом можно сделать статичным как отдельный метод или свойство, так и весь класс. В этом случае все поля и методы тоже должны быть статичными.

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

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

Подробно о том, что такое классы и объекты, читайте в первой статье цикла об объектно-ориентированном программировании.

Напишите класс GameObject, в котором будут храниться координаты объекта. Координаты должны быть доступны для чтения, а их изменение должно происходить в методе Move ().

Есть и другие ключевые слова:

  • abstract;
  • protected;
  • private protected;
  • protected internal;
  • sealed и другие.

Они будут рассмотрены в статье о наследовании.

From Wikipedia, the free encyclopedia

In computer programming, field encapsulation involves providing methods that can be used to read from or write to the field rather than accessing the field directly. Sometimes these accessor methods are called getX and setX (where X is the field’s name), which are also known as mutator methods. Usually the accessor methods have public visibility while the field being encapsulated is given private visibility — this allows a programmer to restrict what actions another user of the code can perform. Compare the following Java class in which the name field has not been encapsulated:

public class NormalFieldClass {
    public String name;
 
    public static void main(String[] args)
    {
        NormalFieldClass example1 = new NormalFieldClass();
        example1.name = "myName";
        System.out.println("My name is " + example1.name);
    }
}

with the same example using encapsulation:

public class EncapsulatedFieldClass {
    private String name;
 
    public String getName()
    {
        return name;
    }
 
    public void setName(String newName)
    {
        name = newName;
    }
 
    public static void main(String[] args)
    {
        EncapsulatedFieldClass example1 = new EncapsulatedFieldClass();
        example1.setName("myName");
        System.out.println("My name is " + example1.getName());
    }
}

In the first example a user is free to use the public name variable however they see fit — in the second however the writer of the class retains control over how the private name variable is read and written by only permitting access to the field via its getName and setName methods.

Advantages[edit]

  • The internal storage format of the data is hidden; in the example, an expectation of the use of restricted character sets could allow data compression through recoding (e.g., of eight bit characters to a six bit code). An attempt to encode characters out of the range of the expected data could then be handled by casting an error in the set routine.
  • In general, the get and set methods may be produced in two versions — an efficient method that assumes that the caller is delivering appropriate data and that the data has been stored properly, and a debugging version that while slower, performs validity checks on data received and delivered. Such detection is useful when routines (calling or called) or internal storage formats are newly created or modified.
  • The location of the stored data within larger structures may be hidden and so enabling changes to be made to this storage without the necessity of changing the code that references the data. This also reduces the likelihood of unexpected side effects from such changes. This is especially advantageous when the accessors are part of an operating system (OS), a case where the calling (application) code may not be available to the developers of the OS.

Disadvantages[edit]

Access to a subroutine involves additional overhead not present when data is accessed directly. While this is becoming of less concern with the wide availability of fast general-purpose processors it may remain important in coding some real-time computing systems and systems using relatively slow and simple embedded processors. In some languages, like C++, the getter / setter methods are usually inline functions, so that when inlining is performed, the code looks just like direct field accessing.

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

Практический пример

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

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

<?php
class Account
{
    private $balance = 0;
    private $account = [];

    protected function save()
    {
        // сохраним в файл
        file_put_contents('account.txt', json_encode($this));
    }

    public function add($amount, $explanation)
    {
        $this->balance += $amount;
        $this->account[] = [$amount, $explanation];
        $this->save();
    }

    public function withdraw($amount, $explanation)
    {
        $this->balance -= $amount;
        $this->account[] = [-$amount, $explanation];
        $this->save();
    }

    /* Пропустим другие методы */
}

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

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

<?php
class AccountConsumer
{
    function addToAccount(Account $account)
    {
        // этой функции нет никакой разницы от того, использует
        // ли счет какую-то базу данных или файлы - вся разница
        // в работе счета инкапсулирована
        $account->add($this->totalCharge, $this->getExplanation());
    }

    /* Пропустим другие методы */
}

Функция addToAccount будет одинаково хорошо работать как с экземпляром класса Account, так и с экземпляром класса DatabaseAccount:

<?php
class DatabaseAccount extends Account
{
    protected function save()
    {
        // сохраним в базу данных
        foreach ($this->account as list ($amount, $explanation)) {
            $this->database->updateRec($amount, $explanation);
        }
    }

    /* Пропустим другие методы */
}

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

Зачем ещё это может быть нужно?

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

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

Проблема: у вас есть публичное поле.

Решение: сделайте поле приватным и создайте для него методы доступа.

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

Пример: Encapsulate_Field.java, Encapsulate_Field.js

Достоинства

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

Когда нельзя применить

  1. Встречаются случаи, когда инкапсуляция полей нежелательна из соображений, связанных с повышением производительности. Эти случаи очень редки, но иногда этот момент бывает очень важным.
  2. Например, у вас есть графический редактор, в котором есть объекты, имеющие координаты x и y. Эти поля вряд ли будут меняться в будущем. К тому же, в программе участвует очень много различных объектов, в которых присутствуют эти поля. Поэтому обращение напрямую к полям координат экономит значительную часть процессорного времени, которое иначе затрачивалось бы на вызовы методов доступа. Как иллюстрация этого исключения, существует класс Point в Java, все поля которого являются публичными.

Порядок рефакторинга

  1. Создайте геттер и сеттер для поля.
  2. Найдите все обращения к полю. Замените получение значения из поля геттером, а установку новых значений в поле — сеттером.
  3. После того, как все обращения к полям заменены, сделайте поле приватным.

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

Возможно, вам также будет интересно:

  • Инициаторами велосекции стали ошибка
  • Инициатор iscsi ошибка целевого устройства
  • Инициатор iscsi ошибка подключения
  • Инициализован отчет об ошибке iommu hal
  • Инициализирован отчет об ошибке iommu что это

  • Понравилась статья? Поделить с друзьями:
    0 0 голоса
    Рейтинг статьи
    Подписаться
    Уведомить о
    guest

    0 комментариев
    Старые
    Новые Популярные
    Межтекстовые Отзывы
    Посмотреть все комментарии