Объектно-ориентированный подход. Объектно_ориентированный_подход. Объектно ориентированный подход Мэтт Вайсфельд 5е международное издание ббк 32. 973. 2018
Скачать 5.43 Mb.
|
170 и Swift, абстрактные классы не предусмотрены. Таким образом, для того чтобы реализовать контракт при работе с Objective-C и Swift, вам потребуется исполь- зовать протоколы. Интерфейсы Перед тем как определять интерфейс, интересно отметить, что в C++ нет кон- струкции с таким названием. Применяя C++, вы, в принципе, можете создать интерфейс, использовав синтаксическое подмножество абстрактного класса. Например, приведенный далее код на C++ является абстрактным классом. Однако поскольку единственный метод в этом классе — виртуальный, реализа- ция отсутствует. В результате этот абстрактный класс обеспечивает ту же функциональность, что и интерфейс. class Shape { public: virtual void draw() = 0; } ТЕРМИНОЛОГИЯ, СВЯЗАННАЯ С ИНТЕРФЕЙСАМИ ___________________________________ Это.еще.один.из.тех.случаев,.когда.программная.терминология.оказывается.запу- танной,.даже.очень.запутанной..Знайте,.что.термин.«интерфейс».можно.использовать. в.нескольких.значениях. Первый:.графический.интерфейс.пользователя.(GUI.—.Graphical.User.Interface).ши- роко.применяется.для.обозначения.визуального.интерфейса,.с.которым.взаимодей- ствует.пользователь,.зачастую.—.на.мониторе. Второй:.интерфейс.для.класса.—.это,.в.сущности,.подписи.его.методов. Третий:.при.использовании.Objective-C.вы.можете.разбивать.код.на.физически.раз- дельные.модули,.называемые.интерфейсом.и.реализацией. Четвертый:.интерфейс.Java-стиля.и.протокол.Objective-C,.по.сути,.представляют. собой.контракт.между.родительским.и.дочерним.классами. Сам собой напрашивается следующий вопрос: если абстрактный класс может обеспечивать ту же функциональность, что и интерфейс, то зачем в Java и .NET вообще предусмотрена конструкция, называемая интерфейсом? И зачем в Objective-C и Swift предусмотрен протокол? Прежде всего, C++ поддерживает множественное наследование, в отличие от Java, Objective-C, Swift и .NET. Несмотря на то что классы Java, Objective-C, Swift и .NET могут наследовать только от одного родительского класса, они могут реализовывать много интерфейсов. Использование нескольких абстракт- ных классов лежит в основе множественного наследования; таким образом, в случае применения Java и .NET нельзя пойти этим путем. Коротко говоря, при 171 Что.такое.контракт?. . использовании интерфейса вам не придется беспокоиться о формальной струк- туре наследования — теоретически вы сможете добавить интерфейс в любой класс, если это будет иметь смысл при проектировании. Однако абстрактный класс требует, чтобы наследование осуществлялось от него и, соответственно, от его потенциальных родительских классов. ИНТЕРФЕЙСЫ И МНОЖЕСТВЕННОЕ НАСЛЕДОВАНИЕ ________________________________ По.этим.соображениям.интерфейсы.часто.считают.«обходными.путями»,.компенси- рующими.отсутствие.множественного.наследования..Технически.это.неверно..Ин- терфейсы.представляют.собой.отдельную.методику.проектирования,.и,.несмотря. на.то.что.их.можно.использовать.для.проектирования.приложений.с.применением. множественного.наследования,.они.не.являются.заменой.множественного.насле- дования.или.«обходными.путями»,.компенсирующими.его.отсутствие. Как и абстрактные классы, интерфейсы — это мощный инструмент приведения контрактов в исполнение в случае с фреймворками. Прежде чем мы перейдем к каким-либо концептуальным определениям, будет полезно взглянуть на UML- диаграмму фактического интерфейса и соответствующий код. Посмотрите на интерфейс Nameable , показанный на рис. 8.4. Рис. 8.4. UML-диаграмма.Java-интерфейса Обратите внимание, что Nameable определен на UML-диаграмме как интерфейс, благодаря чему его можно отличить от обычного класса (абстрактного или нет). Обратите внимание: интерфейс содержит два метода: getName() и setName() Вот соответствующий код: public interface Nameable { String getName(); void setName (String aName); } Вот для сравнения код соответствующего протокола Objective-C: @protocol Nameable @required - (char *) getName; Глава.8..Фреймворки.и.повторное.использование 172 - (void) setName: (char *) n; @end // Nameable Обратите внимание, что в этом коде Nameable объявлен не как класс, а как ин- терфейс. Из-за этого методы getName() и setName() считаются абстрактными, а реализация отсутствует. Интерфейс, в отличие от абстрактного класса, может не обеспечивать вообще никакой реализации. В результате любой класс, реали- зующий интерфейс, должен обеспечивать реализацию для всех методов. Напри- мер, в Java класс наследует от абстрактного класса, в то время как класс реали- зует интерфейс. НАСЛЕДОВАНИЕ РЕАЛИЗАЦИИ И НАСЛЕДОВАНИЕ ОПРЕДЕЛЕНИЯ __________________ Наследование.иногда.называют.наследованием реализации,.а.интерфейсы.—.на- следованием определения. Связываем все воедино Если и абстрактные классы, и интерфейсы содержат абстрактные методы, то в чем заключается реальная разница между ними? Как мы уже видели ранее, абстрактные классы включают абстрактные и конкретные методы, а интерфей- сы содержат только абстрактные методы. Почему же они так отличаются в этом плане? Допустим, нам необходимо спроектировать класс, представляющий собаку, с таким расчетом, что мы будем позднее добавлять и другие классы млекопита- ющих. Логическим ходом в данной ситуации было бы создание абстрактного класса Mammal public abstract class Mammal { public void generateHeat() {System.out.println("Выработка тепла");} public abstract void makeNoise(); } Этот класс содержит конкретный метод generateHeat() и абстрактный метод makeNoise() . Первый является конкретным, поскольку все млекопитающие вы- рабатывают тепло. Второй является абстрактным, потому что все млекопитаю- щие издают разные звуки. Создадим также класс Head , который будем использовать, когда речь зайдет об отношении композиции: public class Head { String size; 173 Что.такое.контракт?. . public String getSize() { return size; } public void setSize(String aSize) { size = aSize;} } Класс Head содержит два метода — getSize() и setSize() . Несмотря на то что композиция объясняет разницу между абстрактными классами и интерфей- сами, ее использование в этом примере иллюстрирует то, как она связана с абстрактными классами и интерфейсами в общей конструкции объектно- ориентированной системы. Я считаю, что это важно, поскольку так пример выглядит полным. Помните, что есть два способа установления объектных отношений: использование отношения «является экземпляром», представля- емого наследованием, и применение отношения «содержит как часть», пред- ставляемого композицией. Вопрос состоит в следующем: куда именно «впи- сывается» интерфейс? Чтобы ответить на этот вопрос и связать все воедино, создадим класс с име- нем Dog , который будет подклассом Mammal , реализующим Nameable и обладаю- щим объектом Head (рис. 8.5). Рис. 8.5. UML-диаграмма.образца.кода Если говорить кратко, то Java и .NET позволяют создавать объекты тремя пу- тями — с помощью наследования, интерфейсов и композиции. Обратите вни- Глава.8..Фреймворки.и.повторное.использование 174 мание, что штриховая линия на рис. 8.5 представляет интерфейс. Этот пример показывает, когда вам следует использовать каждую из этих конструкций. Когда выбирать абстрактный класс? Когда выбирать интерфейс? Когда выбирать композицию? Разберемся подробнее. Вам должны быть уже знакомы следующие концепции: Dog является подклассом Mammal , поэтому здесь имеет место отношение на- следования; Dog реализует Nameable , поэтому здесь имеет место отношение интерфейса; Dog обладает Head , поэтому здесь имеет место отношение композиции. Приведенный далее код демонстрирует, как можно включить абстрактный класс и интерфейс в один и тот же класс: public class Dog extends Mammal implements Nameable { String name; Head head; public void makeNoise(){System.out.println("Лай");} public void setName (String aName) {name = aName;} public String getName () {return (name);} } После того как вы взглянете на UML-диаграмму, у вас может возникнуть вопрос: хотя штриховая линия от Dog к Nameable и представляет интерфейс, разве это все же не наследование? На первый взгляд, ответ не так прост. Несмотря на то что интерфейсы — это особый тип наследования, важно знать, что именно оз- начает слово «особый» в данном случае. Понимание особой разницы является ключом к грамотному объектно-ориентированному проектированию. Хотя наследование представляет собой строгое отношение «является экземпля- ром», это не совсем так в случае с интерфейсом. Рассмотрим такой пример. Собака является млекопитающим. Рептилия не является млекопитающим. Таким образом, класс Reptile не смог бы наследовать от класса Mammal . Однако интерфейс выходит за пределы разных классов. Собаке можно дать имя. Ящерице можно дать имя. Ключ здесь в том, что классы при строгом наследовании должны быть связан- ными. Например, в рассматриваемой нами конструкции класс Dog находится 175 Что.такое.контракт?. . в непосредственной связи с классом Mammal . Собака является млекопитающим. Собаки и ящерицы не связаны на уровне млекопитающих, поскольку ящерица — это не млекопитающее. Однако интерфейсы могут использоваться для классов, которые не являются связанными. Вы можете дать имя собаке так же, как и ящерице. В этом и за- ключается ключевая разница между использованием абстрактного класса и ин- терфейса. Абстрактный класс представляет некоторую реализацию. Фактически мы ви- дели, что в Mammal содержится конкретный метод generateHeat() . Даже если мы не знаем, с каким млекопитающим имеем дело, нам все равно известно, что все млекопитающие вырабатывают тепло. Однако интерфейс моделирует только поведение. Интерфейс никогда не обеспечивает реализации какого-либо рода — только поведение. Он определяет поведение, которое будет одинаковым во всех классах, между которыми, возможно, не окажется никакой связи. Имена можно давать не только собакам, но и машинам, планетам и т. д. Некоторые разработчики считают, что интерфейсы — это неполноценная замена множественного наследования. Несмотря на то что интерфейсы действительно были составляющей того самого Java, который избавился от множественного наследования (что позаимствовали и многие другие языки программирования), интерфейсы и наследование применяются при проектировании в разных случаях, как нам показывает пример с Nameable. Код, выдерживающий проверку компилятором Можно ли доказать или опровергнуть, что в случае с интерфейсами имеет место настоящее отношение «является экземпляром»? В ситуации с Java (это также может быть сделано при использовании C# и VB) мы можем позволить компи- лятору выяснить все за нас. Взгляните на приведенный далее код: Dog D = new Dog(); Head H = D; Если прогнать этот код через компилятор, то будет выведено следующее сообще- ние об ошибке: Test.java:6: Несовместимый тип идентификатора. Не могу преобразовать Dog в Head. Head H = D; Ясно, что Dog — это не Head . Мы знаем это, и компилятор согласен. Однако, как и следовало ожидать, с приведенным далее кодом все будет отлично: Dog D = new Dog(); Mammal M = D; Глава.8..Фреймворки.и.повторное.использование 176 Это настоящее отношение наследования, и неудивительно, что компилятор разбирает этот код с положительным результатом, поскольку Dog является под- классом Mammal Теперь мы можем по-настоящему протестировать интерфейс. Представляет ли он собой настоящее отношение «является экземпляром»? Компилятор считает, что да: Dog D = new Dog(); Nameable N = D; С этим кодом все отлично. Таким образом, мы можем с уверенностью сказать, что экземпляр класса Dog — это сущность, которой можно дать имя. Это простое, но эффективное доказательство того, что как наследование, так и интерфейсы представляют собой отношение «является экземпляром». При правильном при- менении отношение интерфейса больше похоже на «ведет себя как». Существу- ют и интерфейсы данных, которые соответствуют отношению «является экзем- пляром», но чаще встречаются первые. ИНТЕРФЕЙС NAMEABLE _______________________________________________________________ Интерфейс.определяет.конкретное.поведение,.а.не.реализацию..Реализуя.интер- фейс. Nameable ,.вы.подразумеваете,.что.обеспечите.соответствующее.поведение. с.помощью.реализации.методов. getName() .и. setName() ..Вам.решать,.как.именно. вы.это.сделаете..Все,.что.вам.потребуется,.—.обеспечить.данные.методы. Заключение контракта Простое правило при определении контракта заключается в обеспечении не- реализованного метода с помощью либо абстрактного класса, либо интерфейса. Таким образом, когда подкласс проектируется с намерением реализовать кон- тракт, он должен обеспечивать реализацию нереализованных методов в роди- тельском классе или в интерфейсе. Как уже отмечалось, одно из преимуществ контрактов — стандартизация согла- шений по программированию. Подробнее рассмотрим эту концепцию, взглянув на пример того, что бывает, когда не используются стандарты программирования. В данном случае у нас будет три класса: Planet , Car и Dog . Каждый из них реали- зует код для задания имени сущности. Однако поскольку все они реализованы по отдельности, каждый класс располагает отличающимся синтаксисом для из- влечения имени. Взгляните на приведенный далее код для класса Planet : public class Planet { String planetName; public void getplanetName() {return planetName;}; } 177 Что.такое.контракт?. . Аналогичным образом, класс Car мог бы иметь такой код: public class Car { String carName; public String getCarName() { return carName;}; } А класс Dog мог бы иметь следующий код: public class Dog { String dogName; public String getDogName() { return dogName;}; } Очевидно здесь то, что любому, кто воспользуется этими классами, придется заглянуть в документацию (какой ужас!) для того, чтобы выяснить, как извлечь имя в каждом из этих случаев. Хотя необходимость заглянуть в документацию не самое худшее, что может случиться, было бы здорово, если бы для всех клас- сов, используемых в проекте (или компании), применялось бы одно и то же соглашение об именовании — это немного облегчило бы жизнь. Именно здесь в дело вступает интерфейс Nameable Идея состоит в том, чтобы заключить контракт, охватывающий классы любых типов, которым требуются имена. По мере того как пользователи разных клас- сов будут переходить от одного класса к другому, им не придется выяснять те- кущий синтаксис для именования объекта. Все классы Planet , Car и Dog станут задействовать один и тот же синтаксис именования. Чтобы достичь этой высокой цели, мы можем создать интерфейс (у нас есть возможность воспользоваться интерфейсом Nameable , который мы применяли ранее). Суть соответствующего соглашения заключается в том, что все классы должны реализовывать Nameable . Таким образом, пользователям придется за- помнить только один интерфейс для всех классов в том, что касается соглашений об именовании: public interface Nameable { public String getName(); public void setName(String aName); } Новые классы Planet , Car и Dog должны выглядеть так: Глава.8..Фреймворки.и.повторное.использование 178 public class Planet implements Nameable { String planetName; public String getName() {return planetName;} public void setName(String myName) { planetName = myName;} } public class Car implements Nameable { String carName; public String getName() {return carName;} public void setName(String myName) { carName = myName;} } public class Dog implements Nameable { String dogName; public String getName() {return dogName;} public void setName(String myName) { dogName = myName;} } В данном случае у нас имеется стандартный интерфейс, при этом мы использо- вали контракт для гарантии того, что дело будет обстоять именно так. Факти- чески одним из главных преимуществ применения той или иной современной интегрированной среды разработки является то, что при реализации интерфей- са она будет автоматически обеспечивать заглушки требуемых методов. Эта функция позволяет сэкономить много времени и сил при использовании интер- фейсов. Есть одна небольшая проблема, о которой вы, возможно, задумывались. Идея контракта великолепна, если все играют по правилам, но что, если какая-нибудь сомнительная личность не захочет соблюдать правила (например, жуликоватый программист)? Суть в том, что людям ничто не мешает нарушить стандартный контракт, однако в таких случаях они столкнутся с большими проблемами. С одной стороны, менеджер проекта может настоять на том, чтобы все соблю- дали контракт, точно так же, как и на том, что все члены команды должны ис- пользовать одни и те же соглашения об именовании переменных и систему управления конфигурацией. Если тот или иной член команды не станет соблю- дать правила, то ему могут сделать выговор или даже уволить. Обеспечение следования правилам — один из способов гарантировать, что кон- тракты соблюдаются. При этом бывают ситуации, когда результатом нарушения контракта оказывается непригодный к использованию код. Возьмем, к примеру, 179 Пример.из.сферы.электронного.бизнеса. . Java-интерфейс Runnable . Java-апплеты реализуют интерфейс Runnable , по- скольку он требует, чтобы любой реализующий его класс обязательно реализо- вывал метод run() . Это важно, так как браузер, вызывающий апплет, будет вызывать метод run() , содержащийся в Runnable . Если метод run() будет от- сутствовать, то произойдет ошибка. Системные «точки расширения» По сути, контракты являются «точками расширения» в вашем коде. Везде, где нужно сделать части системы абстрактными, вы можете использовать контракт. Вместо того чтобы создавать связи с объектами определенных классов, можно «подключиться» к любому объекту, реализующему контракт. Вам необходимо знать, где именно контракты окажутся полезны; вместе с тем возможно и зло- употребление контрактами. Вам потребуется выявить общие детали вроде ин- терфейса Nameable , о котором мы говорили в этой главе. Но знайте, что в случае применения контрактов возможны компромиссные решения. Они могут сделать возможность повторного использования кода более реальной, однако несколь- ко усложняют работу. |