Разделы портала

Онлайн-тренинги

.
Четыре столпа объектно-ориентированного программирования, часть 1: инкапсуляция
01.12.2022 00:00

Автор: Баз Дейкстра (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 и лучшему разделению интересов.

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

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