Четыре столпа объектно-ориентированного программирования, часть 4: абстракция |
05.09.2023 00:00 |
Автор: Баз Дейкстра (Bas Dijkstra) В этой серии статей я углублюсь в четыре столпа (фундаментальных принципа ) объектно-ориентированного программирования:
Почему? Я считаю, что это знание необходимо не только разработчикам, но и несомненно тестировщикам, работающим с кодом, читающим или пишущим его. Понимание этих принципов позволяет вам лучше понимать код приложения, давать рекомендации по улучшению его структуры и, конечно, лучше писать код автотестов. Примеры, которые я даю, в основном будут на Java, но в ходе этой серии статей я упомяну, как внедрять эти концепции, по возможности, в C# и Python. Что такое абстракция?Абстракция – это создание абстрактных репрезентаций или схем для конкретных концепций, как правило, классов. Абстракция позволяет разработчикам навязать общую структуру группе связанных классов – или через использование интерфейсов, или через абстрактные классы. Абстракция: примерЧтобы лучше понять, как выглядит абстракция и каковы ее преимущества для объектно-ориентированного программирования, рассмотрим пример. В предыдущей статье серии мы закончили на том, что класс SavingsAccount, который наследуется от родительского класса Account, но имеет свое собственное внедрение отдельных методов. Если, однако, пристальнее посмотреть на отношения между Account и SavingsAccount, то родительско-дочерние отношения, возможно, не лучший выбор. Предпочтительным вариантом моделирования разных типов классов в нашем коде будет репрезентация каждого типа счета (текущего, сберегательного, инвестиционного…) своим собственным классом без каких-либо родительско-дочерних отношений между ними. Нам, однако, нужно убедиться, что все классы следуют некоей общей структуре – или что они, скорее, содержат определенные свойства и методы, общие для всех типов счетов. Применение абстракции поможет нам добиться именно этого. Сначала посмотрим, как это делается с применением интерфейсов. Интерфейсы в действииИнтерфейсы в Java можно рассматривать, как форму контракта, которого должны придерживаться все внедряющие этот интерфейс классы. Он, как минимум, содержит список методов, которые должны присутствовать во всех классах, внедряющих этот интерфейс. Вот так может выглядеть интерфейс Account: public interface Account { void withdraw(double amount); void deposit(double amount); Этот интерфейс сообщает нам, что все типы счетов должны, как минимум, внедрять метод withdraw(), а также метод deposit(). Классы также могут содержать другие методы, не определенные в интерфейсе, но обязаны внедрять эти. Класс SavingsAccount теперь можно определить, внедрив интерфейс Account примерно так: public class SavingsAccount implements Account { private final int number; private double balance; public SavingsAccount(int number, double interestRate) { @Override @Override Заметьте, что ключевое слово implements используется тут для объявления, что наш класс SavingsAccount следует структуре, заданной интерфейсом Account. Отсутствие внедрения методов, заданных в интерфейсе класса, вызовет ошибку компилятора. Другие классы вроде CheckingAccount теперь тоже могут внедрять интерфейс Account, и мы можем даже инстанцировать новые объекты, используя тип данных интерфейса: public static void main(String[] args) { // Текущий счет требует только задания номера счета // Сберегательный счет требует номера счета и процентной ставки Итак, интерфейсы – это способ задать общую структуру классов. Повторюсь, это можно рассматривать, как форму контракта. Но что, если внедрение для разных методов одинаково для многих или даже всех классов, внедряющих этот интерфейс? Не приведет ли это к обширной дупликации кода? Ну, да, приведет, но не пугайтесь, есть способы справиться с этим. Один из методов решения этой проблемы – это использование методов по умолчанию в вашем интерфейсе. Метод по умолчанию определяется на уровне интерфейса и автоматически доступен для всех классов, использующих этот интерфейс: public interface Account { void withdraw(double amount); void deposit(double amount); default void printBankInfo() { public static void main(String[] args) { Account account = new CheckingAccount(9876); Решение, однако, имеет свои пределы, так как у интерфейсов в Java нет состояний – то есть вы не можете определять, получать доступ к или модифицировать свойства интерфейса. Если, скажем, мы хотим задать общее внедрение для метода deposit() для всех типов счетов в абстракции, этого нельзя добиться при помощи интерфейсов. Вместо этого нужно использовать абстрактный класс. Абстрактные классы в действииКак и интерфейсы, абстрактные классы дают возможность навязать общую структуру группе связанных классов. В отличие от интерфейсов, однако, абстрактные классы могут иметь состояние, и могут иметь внедрение методов, которые получают доступ к состоянию объекта и модифицируют его – то есть его свойства. Вот как может выглядеть абстрактный класс Account, задающий общее внедрение для метода deposit(): public abstract class Account { protected double balance; abstract void withdraw(double amount); public void deposit(double amount) { public void printBankInfo() { А так наш класс SavingsAccount теперь расширяет Account (вы расширяете абстрактный класс, вы не внедряете его): public class SavingsAccount extends Account { private final int number; public SavingsAccount(int number, double interestRate) { @Override Как можно видеть, SavingsAccount больше не нуждается во внедрении метода deposit(), так как он уже поддерживается абстрактным классом, но метод можно вызвать для объекта с типом SavingsAccount без проблем: public static void main(String[] args) { Account mySavingsAccount = new SavingsAccount(1234, 0.03); Более того, так как свойство balance уже определено в Account, SavingsAccount может получить к нему доступ и использовать его, не нуждаясь в явном повторном его определении. Все это, конечно, работает при условии, что ваши модификаторы доступа позволяют это. Абстракция на других языкахВ C# абстракция работает практически так же, как в Java, с некоторыми мелкими отличиями. Основное из них заключается в том, что в C# в интерфейсе можно определить даже больше, чем в Java. Вы можете определить интерфейсы, а интерфейсы могут содержать внедрения методов, как в Java, но в C# (как минимум в последних версиях языка) интерфейсы могут также определять состояния и получать к ним доступ (и снова – свойства). По этой причине различия между интерфейсами и абстрактными методами в C# даже меньше, чем в Java. Крупнейшие, по моему мнению, оставшиеся различия:
Эти различия применимы как к Java, так и к C#. Python не имеет концепции абстрактного класса, а также не знает концепции интерфейса. Технически вы можете сконструировать что-то, что некоторым образом похоже на интерфейс (показано в этой статье), но я считаю, что это выглядит очень натужно – не так, как нормальный интерфейс в Java и C#. Абстракция в автоматизацииАбстракция – принцип объектно-ориентированного программирования, который я применяю в автоматизации меньше всего – до такой степени, что мне сложно привести полезный пример ее использования. Я даже полагаю, что если вы используете интерфейсы и абстрактные классы в автоматизации, стоит задаться вопросом, не оверинжиниринг ли это. Распространенный пример применения абстракции в коде автоматизации (но не самостоятельное внедрение) – это интерфейс WebDriver в Selenium. Факт, что в коде можно сделать WebDriver driver = new ChromeDriver(); и WebDriver driver = new FirefoxDriver(); - что позволяет создавать тесты, которые можно запускать в разных браузерах, не жонглируя разными объектами драйвера – это демонстрация силы абстракции. Я с радостью посмотрел бы на опровергающие меня примеры, демонстрирующие, что определение и использование интерфейсов и абстрактных классов в автоматизации – хорошая идея. Если у вас есть примеры того, как абстракция сильно помогла с кодом автоматизации, пожалуйста, пришлите мне их – я с удовольствием сменю свою точку зрения. |