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

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

.
Четыре столпа объектно-ориентированного программирования, часть 2: наследование
12.12.2022 00:00

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

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

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

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

Что такое наследование?

Наследование – это практика вывода классов из других классов, или, иными словами, создание родительско-дочерних отношений между классами.

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

Наследование: пример

Чтобы лучше понимать, на что способно наследование, снова возьмем наш класс Account, с которым мы работали в прошлой статье. В определенном в этом классе методе withdraw() мы разделили бизнес-правила для текущих и сберегательных счетов.

Теперь представьте, что мы хотим расширить функциональность этого класса Account, но только для сберегательных счетов. К примеру, мы хотим иметь возможность добавить туда процентную ставку, но только если тип счета – AccountType.SAVINGS.

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

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

Реализация наследования

В примере кода ниже мы создаем новый класс SavingsAccount, расширяющий наш существующий класс Account и добавляющий возможность рассчитывать процент:

public class SavingsAccount extends Account {
    private double interestRate;
    public SavingsAccount() {
        super(AccountType.SAVINGS);
this.interestRate = 0.03;
}
    public void addInterest() {
        this.balance *= (1 + this.interestRate);
}
}

Этот класс SavingsAccount создается как дочерний для класса Account, как видно по ключевому слову extends. Это означает, что SavingsAccount наследует все публичные свойства и методы, заданные в Account, и вызов методов, определенных в Account, для объекта с типом SavingsAccount теперь возможен:

public static void main(String[] args) throws DepositException {
    SavingsAccount sa = new SavingsAccount();
sa.deposit(500);
}

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

Именно это делает вызов super(): он вызывает конструктор родительского класса Account, который, в свою очередь, устанавливает тип счета и баланс (см. предыдущую статью по реализации конструктора Account).

В дополнение ко всем свойствам и методам, определенным в родительском классе, объект типа SavingsAccount также имеет в своем распоряжении метод addInterest().

Пожалуйста, отметьте, что наследование свойств – односторонний процесс: SavingsAccount наследует публичные свойства и методы от Account, но Account не наследует свойства и методы SavingsAccount. В результате становится невозможным вызвать метод addInterest() для объекта с типом Account.

Наследование и модификаторы доступа

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

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

К счастью, в Java есть решение с применением модификатора защищенного доступа:

public class Account {
    private AccountType type;
protected double balance;

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

Наследование на других языках

Все, что вы видели выше на Java, может быть сделано и на C#. Определение родительско-дочерних отношений между классами выглядит так:

public class SavingsAccount : Account
{
public double InterestRate { get; private set; }
    public SavingsAccount() : base()
{
InterestRate = 0.03;
}
}

У C# также есть защищенное ключевое слово, дающее дочерним классам, а также самому внедряемому классу, доступ к свойствам и методам.

Python тоже позволяет использовать наследование:

class SavingsAccount(Account):
    def __init__(self):
Account.__init__(self,.Savings)
self.interest_rate = 0.03

В отличие от Java и C#, в Python можно даже делать множественное наследование, то есть создать класс, наследующий свойства и методы от нескольких различных классов.

В Python нет модификатора доступа, схожего с защищенным в Java и C#. Однако это поведение можно симулировать через декораторы свойств.

Наследование в автоматизации

Я видел два варианта применения принципа наследования в автоматизации, используемых довольно часто.

Первый основан на паттерне Page Object, который мы вкратце обсудили в прошлой статье. Это использование базовых страниц, содержащих общие методы, применяемые к множеству Page Objects. Это могут быть обертки Selenium API, задающие явное ожидание перед вызовом sendKeys() или click():

public class BasePage {
    private WebDriver driver;
private WebDriverWait wait;
    public BasePage(WebDriver driver) {
        this.driver = driver;
this.wait = new WebDriverWait(this.driver, Duration.ofSeconds(10));
}
// Определение вспомогательного метода, добавляющего явное ожидание перед попыткой клика по элементу
protected void click(By locator) {
    try {
wait.until(ExpectedConditions.elementToBeClickable(locator));
driver.findElement(locator).click();
}
catch (TimeoutException te) {
Assertions.fail(String.format("Exception in click(): %s", te.getMessage()));
}
}

Они могут наследоваться и использоваться в классе Page Object:

public class LoginPage extends BasePage {
    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) {
        super(driver);
}
    public void loginAs(String username, String password) {
        sendKeys(textfieldUsername, username);
sendKeys(textfieldPassword, password);
        // Использование click(), определенного на базовой странице
        click(buttonDoLogin);
    }
}

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

public class BaseTest {
    @BeforeEach
public void before() {
System.out.println("Test setup goes here...");
}
    @AfterEach
public void after() {
System.out.println("Test teardown goes here...");
}
}
public class MyTest extends BaseTest {
    @Test
public void testA() {
System.out.println("Running the first test...");
}
    @Test
public void testB() {
System.out.println("Running the second test...");
}
}

Результат прогона этих тестов будет выглядеть примерно так:

Test setup goes here...
Running the first test...
Test teardown goes here...
Test setup goes here...
Running the second test...
Test teardown goes here...

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

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

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