Четыре столпа объектно-ориентированного программирования, часть 3: полиморфизм |
31.08.2023 00:00 |
Автор: Баз Дейкстра (Bas Dijkstra) В этой серии статей я углублюсь в четыре столпа (фундаментальных принципа ) объектно-ориентированного программирования:
Почему? Я считаю, что это знание необходимо не только разработчикам, но и несомненно тестировщикам, работающим с кодом, читающим или пишущим его. Понимание этих принципов позволяет вам лучше понимать код приложения, давать рекомендации по улучшению его структуры и, конечно, лучше писать код автотестов. Примеры, которые я даю, в основном будут на Java, но в ходе этой серии статей я упомяну, как внедрять эти концепции, по возможности, в C# и Python. Что такое полиморфизм?Полиморфизм – способность объекта в объектно-ориентированном программировании принимать разные формы. Слово это греческое, и термин «полиморфизм» дословно переводится как «множество форм». Полиморфизм позволяет программистам прикреплять к одному объекту или части объекта несколько разных реализаций, а также получать доступ к сущностям различных типов через единый интерфейс. Полиморфизм: примерЧтобы лучше понимать, как выглядит полиморфизм, стоит знать, что в объектно-ориентированном программировании часто встречаются два типа полиморфизма – замещение и переопределение. Можно сказать, что даже у самого полиморфизма есть несколько форм. Как это метафизично. Замещение в действииСначала посмотрим на замещение. В предыдущей статье мы определили класс SavingsAccount, наследующий свойства и методы от родительского класса Account. К тому же в первой статье мы внедрили метод withdraw в класс Account, содержащий некоторую общую бизнес-логику (запрещающий нам депозит отрицательного числа), а также бизнес-логику, специфичную для сберегательного счета (предотвращающий снятие выше предельного со сберегательного счета). Можно сказать, что второй кусочек логики должен быть частью класса SavingsAccount вместо класса Account, так как относится только к сберегательным счетам. В результате в обоих классах будет своя собственная реализация метода withdraw(). Это именно то, что позволяет нам замещение: замещать определение метода из родительского класса (в данном случае Account) в дочернем классе (в данном случае SavingsAccount). Это значит, что теперь у нас будет реализация withdraw() в классе Account, которая выглядит так: public void withdraw(double amount) throws WithdrawException { и другое определение для withdraw() в классе SavingsAccount, замещающее определение в Account: @Override Пожалуйста, отметьте, что, хоть использование аннотации @Override и не будет строгой необходимостью в Java – то есть его отсутствие не вызовет ошибок компилятора, - его использование рекомендуется, чтобы код явно сообщал, что тут перезаписывается внедрение метода родительского класса. Итак, применяя полиморфизм путем замещения, мы получаем ситуацию, когда мы можем делать как это: Account account = new Account(AccountType.CHECKING); Так и это: SavingsAccount account = new SavingsAccount(); Еще одно примечание: нам есть, что здесь улучшить. Текущий счет создается через тип Account, а у сберегательного счета свой собственный тип данных – это не очень изящно. Мы вернемся к этому в четвертой и финальной статье серии, когда будем говорить про абстракцию. Переопределение в действииДругой тип полиморфизма, о котором я хочу поговорить в этой статье – это переопределение. В объектно-ориентированном программировании переопределение – это способность определить множество методов с одним именем, но разными наборами аргументов. В качестве примера определим второй конструктор (это особый тип метода) для класса SavingsAccount: public class SavingsAccount extends Account { Это позволяет нам или создать сберегательный счет с процентной ставкой 3% по умолчанию, используя первый конструктор, или создать счет с другой процентной ставкой, используя второй конструктор. Для конструктора или любого иного метода можно создавать столько переопределений, сколько вам захочется – единственное, что или типы данных аргументов, или количество аргументов, или и то, и другое должны быть уникальными. Почему? Потому что Java должна знать, какую версию конструктора или метода вызывать при запуске, и она делает это, смотря на количество и тип данных аргументов, переданных в конструктор или метод. Полиморфизм в других языкахВ C# замещение работает практически так же, как в Java. Вам, однако, нужно разрешить замещение для метода в родительском классе явным образом, используя ключевое слово virtual: public class Employee public class SalesEmployee : Employee Python тоже поддерживает замещение метода довольно прямым образом: class Employee: class SalesEmployee(Employee): Переопределение методов в C# и Python даже проще, чем в Java, так как оба языка поддерживают опциональные аргументы метода в дополнение к значениям аргумента по умолчанию. К примеру, два конструктора, которые мы разбирали для класса Account в Java, можно воссоздать единым конструктором в C# и Python, используя значение аргумента по умолчанию: public class SavingsAccount : Account class SavingsAccount(Account): В обоих случаях теперь можно или создать SavingsAccount с процентной ставкой 3% по умолчанию, или же создать счет с выбранной вами процентной ставкой. Полиморфизм в автоматизацииЛично я нечасто применяю полиморфизм в своих автоматизационных решениях, но в некоторых случаях он может быть полезен – особенно если мы смотрим на переопределение методов. К примеру, у меня есть метод-помощник в Selenium, который должен или пользоваться таймаутом по умолчанию, или использовать кастомный (и, как правило, более длительный) таймаут в отдельных случаях. Вот как могут выглядеть такие методы – например, в случае ожидания кликабельности кнопки перед попыткой клика по ней: protected void click(By locator) { protected void click(By locator, int timeoutInSeconds) { Таким образом, можно или вызвать click(By.Id("someElement")) в сценариях, используя значение таймаута по умолчанию (10 секунд), или задать, скажем, 20-секундный таймаут для отдельных случаев, вызывая `click(By.Id(“someSlowLoadingElement”), 20); Другой случай, где можно выиграть от переопределения, даже если вы не внедряли его самостоятельно – это выбор одного из переопределений метода assertEquals() в JUnit (или TestNG): В автоматизации я гораздо больше пользуюсь переопределением, чем замещением, но наверняка есть ситуации, где замещение будет полезным. В этом случае расскажите мне об этом в комментариях. В четвертой и финальной статье этой серии мы подробнее разберем последний фундаментальный принцип объектно-ориентированного программирования: абстракцию. |