Главная страница
Навигация по странице:

  • Рис. 7.8.

  • Почему инкапсуляция является фундаментальной объектно-ориентированной концепцией

  • Как наследование ослабляет инкапсуляцию

  • ПОСТОЯННОЕ ТЕСТИРОВАНИЕ _______________________________________________________

  • Подробный пример полиморфизма

  • Ответственность объектов Снова обратимся к примеру с Shape из главы 1 (рис. 7.11). 155

  • Абстрактные классы, виртуальные методы и протоколы

  • Объектно-ориентированный подход. Объектно_ориентированный_подход. Объектно ориентированный подход Мэтт Вайсфельд 5е международное издание ббк 32. 973. 2018


    Скачать 5.43 Mb.
    НазваниеОбъектно ориентированный подход Мэтт Вайсфельд 5е международное издание ббк 32. 973. 2018
    АнкорОбъектно-ориентированный подход
    Дата31.03.2023
    Размер5.43 Mb.
    Формат файлаpdf
    Имя файлаОбъектно_ориентированный_подход.pdf
    ТипДокументы
    #1028905
    страница17 из 25
    1   ...   13   14   15   16   17   18   19   20   ...   25
    150
    То, что автомобиль состоит из двигателя, стереосистемы и двери, легко понять, поскольку большинство людей именно так и представляют себе автомобили.
    Однако при проектировании объектно-ориентированных программных систем важно помнить, что объекты, как и автомобили, состоят из других объектов.
    Более того, количество узлов и ветвей, которое может включать соответствую- щая древовидная структура классов, фактически неограниченно.
    На рис. 7.8 показана объектная модель для
    Car
    , включая
    Engine
    ,
    Stereo и
    Door
    Рис. 7.8. Иерархия.классов.во.главе.с.Car
    СЛОЖНОСТЬ МОДЕЛИ ________________________________________________________________
    Как.и.в.случае.с.проблемой.наследования.в.примере.с.классами,.которые.касаются.
    лающих.и.нелающих.собак,.злоупотребление.композицией.может.привести.к.повы- шению.сложности..Между.созданием.объектной.модели,.достаточно.детализиро- ванной.для.того,.чтобы.быть.адекватно.выразительной,.и.модели,.которая.настоль- ко.детализирована,.что.ее.сложно.понять.и.сопровождать,.проходит.тонкая.грань.
    Обратите внимание, что все три объекта, которые образуют
    Car
    , сами состоят из других объектов.
    Engine содержит
    Pistons и
    SparkPlugs
    ;
    Stereo включает
    Radio и
    Cassette
    ;
    Door содержит
    Handle
    . Заметьте также, что там имеется еще

    151
    Почему.инкапсуляция.является.фундаментальной.концепцией. .
    один уровень:
    Radio содержит
    Tuner
    . Мы могли бы также добавить, что
    Handle содержит
    Lock
    , а
    Cassette включает
    FastForwardButton
    . Кроме того, мы могли бы пойти на один уровень дальше
    Tuner и создать объект
    Dial
    . Проектировщи- ку решать, какими будут качество и сложность объектной модели.
    Почему инкапсуляция является
    фундаментальной объектно-ориентированной
    концепцией
    Инкапсуляция — фундаментальная объектно-ориентированная концепция.
    Каждый раз при рассмотрении парадигмы «интерфейс/реализация» мы говорим об инкапсуляции. Основной вопрос заключается в том, что в классе должно быть видно, а что — нет. Инкапсуляция в равной мере касается данных и по- ведений. Когда речь идет о классе, то первоочередное проектное решение «вра- щается» вокруг инкапсуляции как данных, так и поведений в хорошо написан- ном классе.
    Гилберт и Маккарти определяют инкапсуляцию как «процесс упаковки вашей программы с разделением каждого из ее классов на две обособленные части — интерфейс и реализацию». Эта идея многократно повторяется и по ходу нашей книги.
    Но при чем здесь инкапсуляция и какое отношение она имеет к этой главе?
    В данном случае мы сталкиваемся с объектно-ориентированным парадоксом.
    Инкапсуляция является настолько фундаментальной объектно-ориентиро- ванной концепцией, что представляет собой одно из главных правил ООП.
    Наследование тоже считается одной из трех важнейших объектно-ориентиро- ванных концепций. Однако оно некоторым образом фактически нарушает инкапсуляцию! Как такое возможно? Неужели две из трех важнейших объ- ектно-ориентированных концепций противоречат друг другу? Рассмотрим это подробнее.
    Как наследование ослабляет инкапсуляцию
    Как уже говорилось, инкапсуляция — это процесс упаковки классов в открытый интерфейс и закрытую реализацию. По сути, в классе скрывается все, о чем другим классам знать необязательно.
    Петер Коуд и Марк Мейфилд отмечают, что при использовании наследования инкапсуляция, в сущности, ослабляется в рамках иерархии классов. Они гово- рят о конкретном риске: наследование означает сильную инкапсуляцию по отношению к остальным классам, но слабую инкапсуляцию между суперклассом и его подклассами.

    Глава.7..Наследование.и.композиция
    152
    Проблема заключается в том, что если от суперкласса будет унаследована реа- лизация, которая затем подвергнется модификации, то такое изменение рас-
    пространится по иерархии классов. Этот волновой эффект потенциально способен затронуть все подклассы. Поначалу это может не показаться большой проблемой, однако, как мы уже видели ранее, подобный волновой эффект может привести к непредвиденным проблемам. Например, тестирование превратится в кошмар. В главе 6 мы говорили о том, как инкапсуляция упрощает системы тестирования. В теории, если вы создадите класс с именем
    Cabbie
    (рис. 7.9) и соответствующими открытыми интерфейсами, то любое изменение реализации
    Cabbie должно быть прозрачным для всех остальных классов. Однако в любой конструкции изменение суперкласса, безусловно, нельзя назвать прозрачным для того или иного подкласса. Понимаете, в чем проблема?
    Рис. 7.9. UML-диаграмма.класса.Cabbie
    Если бы другие классы находились в прямой зависимости от реализации клас- са
    Cabbie
    , то тестирование стало бы более сложным, а то и вовсе невозможным.
    С применением другого подхода при проектировании, с помощью абстрагиро- вания поведений и наследования только атрибутов, проблемы, обозначенные выше, должны исчезнуть.
    ПОСТОЯННОЕ ТЕСТИРОВАНИЕ _______________________________________________________
    Даже.при.инкапсуляции.вам.потребуется.повторно.протестировать.классы,.исполь- зующие.Cabbie,.чтобы.убедиться.в.том,.что.соответствующее.изменение.не.при- вело.к.каким-либо.проблемам.
    Если вы затем создадите подкласс
    Cabbie с именем
    PartTimeCabbie
    , который унаследует реализацию от
    Cabbie
    , то изменение реализации
    Cabbie напрямую повлияет на новый класс.
    Взгляните, к примеру, на UML-диаграмму, показанную на рис. 7.10.
    PartTimeCabbie
    — это подкласс
    Cabbie
    . Поэтому
    PartTimeCabbie наследует от-

    153
    Почему.инкапсуляция.является.фундаментальной.концепцией. .
    крытую реализацию
    Cabbie
    , включая метод giveDirections()
    . Если метод giveDirections()
    изменится в
    Cabbie
    , то это напрямую повлияет на
    PartTimeCabbie и все другие классы, которые позднее могут быть созданы как подклассы
    Cabbie
    . В силу этой специфики изменения реализации
    Cabbie необя- зательно инкапсулируются в классе
    Cabbie
    Рис. 7.10. UML-диаграмма.классов.Cabbie/PartTimeCabbie
    Чтобы снизить риск, который представляет эта дилемма, при использовании наследования важно придерживаться строгого условия «является экземпляром».
    Если подкласс на самом деле является конкретизацией суперкласса, то измене- ния родительского класса, вероятно, подействуют на дочерний класс естествен- ным и ожидаемым образом. Чтобы проиллюстрировать это, обратимся к следу- ющему примеру: если класс
    Circle унаследует реализацию от класса
    Shape
    , а изменение реализации
    Shape нарушит
    Circle
    , то
    Circle в действительности не является конкретизацией
    Shape
    Как наследование может быть неправильно использовано? Рассмотрим ситуа- цию, когда вам требуется создать окно для целей графического интерфейса пользователя. У вас, возможно, возник бы порыв создать окно (
    Window
    ), сделав его подклассом класса
    Rectangle
    :
    public class Rectangle {
    }
    public class Window extends Rectangle {
    }

    Глава.7..Наследование.и.композиция
    154
    На самом деле
    Window для графического интерфейса пользователя представля- ет собой нечто намного большее, чем подкласс
    Rectangle
    . Это неконкретизиро- ванная версия
    Rectangle
    , как, например,
    Square
    . Настоящий класс
    Window может включать
    Rectangle
    (и даже много
    Rectangle
    ); вместе с тем это ненастоящий
    Rectangle
    . При таком подходе класс
    Window не должен наследовать от
    Rectangle
    , но должен содержать классы
    Rectangle
    :
    public class Window {
    Rectangle menubar;
    Rectangle statusbar;
    Rectangle mainview;
    }
    Подробный пример полиморфизма
    Многие люди считают полиморфизм краеугольным камнем объектно-ориенти- рованного проектирования. Разработка класса для создания полностью неза- висимых объектов является сутью объектно-ориентированного подхода. В хо- рошо спроектированной системе объект должен быть способен ответить на все важные вопросы о себе. Как правило, объект должен быть ответственным за себя. Эта независимость является одним из главных механизмов повторного использования кода.
    Как уже отмечалось в главе 1, полиморфизм буквально означает множествен-
    ность форм. При отправке сообщения объекту он должен располагать методом, позволяющим ответить на это сообщение. В иерархии наследования все под- классы наследуют интерфейсы от своих суперклассов. Однако поскольку каж- дый подкласс представляет собой отдельную сущность, каждому из них может потребоваться дать отдельный ответ на одно и то же сообщение.
    Повторно обратимся к примеру из главы 1, взглянув на класс
    Shape
    . Он содержит поведение
    Draw
    . Вместе с тем, когда вы попросите кого-то нарисовать фигуру, первый вопрос, который вам зададут, вероятно, будет звучать так: «Какой фор- мы?» Просто сказать человеку нарисовать фигуру будет слишком абстрактным
    (кстати, метод
    Draw в
    Shape не содержит реализации). Вы должны указать, фи- гуру какой именно формы имеете в виду. Для этого потребуется обеспечить фактическую реализацию в
    Circle и других подклассах. Несмотря на то что
    Shape содержит метод
    Draw
    ,
    Circle переопределит этот метод и обеспечит соб- ственный метод
    Draw
    . Переопределение, в сущности, означает замену реализации родительского класса своей собственной.
    Ответственность объектов
    Снова обратимся к примеру с
    Shape из главы 1 (рис. 7.11).

    155
    Почему.инкапсуляция.является.фундаментальной.концепцией. .
    Рис. 7.11. Иерархия.классов.во.главе.с.Shape
    Полиморфизм — один из наиболее изящных вариантов использования насле- дования. Помните, что создать экземпляр
    Shape нельзя. Это абстрактный класс, поскольку он содержит абстрактный метод getArea()
    . В главе 8 абстрактные классы очень подробно описаны.
    Однако экземпляры
    Rectangle и
    Circle создать можно, так как это конкретные классы. Несмотря на то что
    Rectangle и
    Circle представляют фигуры, у них имеются кое-какие различия. Поскольку речь идет о фигурах, можно вычислить их площадь. Однако формулы для вычисления площадей окажутся разными.
    Таким образом, формулы нельзя будет включить в класс
    Shape
    Именно здесь в дело вступает полиморфизм. Смысл полиморфизма заключается в том, что вы можете отправлять сообщения разным объектам, которые будут отвечать на них в соответствии со своими объектными типами. Например, если вы отправите сообщение getArea()
    классу
    Circle
    , то это приведет к вычислению с использованием формулы, отличной от той, которая будет применена, если от- править аналогичное сообщение getArea()
    классу
    Rectangle
    . Это потому, что
    Circle и
    Rectangle отвечают каждый за себя. Если вы попросите
    Circle возвратить значение площади круга, то он будет знать, как это сделать. Если вы захотите, чтобы
    Circle нарисовал круг, то он сможет сделать и это. Объект
    Shape не смог бы сделать этого, даже если бы можно было создать его экземпляр, поскольку у него нет достаточного количества информации о себе. Обратите внимание, что на UML-диаграмме (см. рис. 7.11) метод getArea()
    в классе
    Shape выделен кур- сивом. Это означает, что данный метод является абстрактным.
    В качестве очень простого примера представьте, что у вас имеется четыре класса: абстрактный класс
    Shape и конкретные классы
    Circle
    ,
    Rectangle и
    Star
    . Вот код:
    public abstract class Shape{
    public abstract void draw();
    }

    Глава.7..Наследование.и.композиция
    156
    public class Circle extends Shape{
    public void draw() {
    System.out.println("Я рисую круг");
    }
    }
    public class Rectangle extends Shape{
    public void draw() {
    System.out.println("Я рисую прямоугольник");
    }
    }
    public class Star extends Shape{
    public void draw() {
    System.out.println("Я рисую звезду");
    }
    }
    Обратите внимание, что для каждого класса есть только один метод — draw()
    Вот что важно для полиморфизма и объектов, которые отвечают за себя: кон- кретные классы сами несут ответственность за функцию рисования. Класс
    Shape не обеспечивает код для осуществления рисования; классы
    Circle
    ,
    Rectangle и
    Star делают это сами. Вот код как доказательство этого:
    public class TestShape {
    public static void main(String args[]) {
    Circle circle = new Circle();
    Rectangle rectangle = new Rectangle();
    Star star = new Star();
    circle.draw();
    rectangle.draw();
    star.draw();
    }
    }
    Тестовое приложение
    TestShape создает три класса:
    Circle
    ,
    Rectangle и
    Star
    Чтобы нарисовать соответствующие им фигуры,
    TestShape просит отдельные классы сделать это:

    157
    Почему.инкапсуляция.является.фундаментальной.концепцией. .
    circle.draw();
    rectangle.draw();
    star.draw();
    Выполнив
    TestShape
    , вы получите следующие результаты:
    C:\>java TestShape
    Я рисую круг
    Я рисую прямоугольник
    Я рисую звезду
    Это и есть полиморфизм в действии. Что бы было, если бы вы захотели создать новый класс, например
    Triangle
    ? Вам потребовалось бы просто написать этот класс, скомпилировать, протестировать и использовать его. Базовому классу
    Shape не пришлось бы претерпевать изменения, равно как и любому другому коду:
    public class Triangle extends Shape{
    public void draw() {
    System.out.println("Я рисую треугольник");
    }
    }
    Теперь можно отправлять сообщение
    Triangle
    . И хотя класс
    Shape не знает, как нарисовать треугольник,
    Triangle известно, как это сделать:
    public class TestShape {
    public static void main(String args[]) {
    Circle circle = new Circle();
    Rectangle rectangle = new Rectangle();
    Star star = new Star();
    Triangle triangle = new Triangle ();
    rectangle.draw();
    star.draw();
    triangle.draw();
    }
    }
    C:\>java TestShape
    Я рисую круг
    Я рисую прямоугольник
    Я рисую звезду
    Я рисую треугольник

    Глава.7..Наследование.и.композиция
    158
    Чтобы увидеть истинную мощь полиморфизма, вы можете передать объект
    Shape методу, который абсолютно не имеет понятия, какую фигуру предстоит нари- совать. Взгляните на приведенный далее код, который включает параметры, обозначающие определенные фигуры:
    public class TestShape {
    public static void main(String args[]) {
    Circle circle = new Circle();
    Rectangle rectangle = new Rectangle();
    Star star = new Star();
    drawMe(circle);
    drawMe(rectangle);
    drawMe(star);
    }
    static void drawMe(Shape s) {
    s.draw();
    }
    }
    В данном случае объект
    Shape может быть передан методу drawMe()
    , который способен обеспечить рисование любой допустимой фигуры, даже такой, которую вы добавите позднее. Вы можете выполнить эту версию
    TestShape точно так же, как и предыдущую.
    Абстрактные классы, виртуальные методы и протоколы
    Абстрактные классы, как они определяются на Java, также могут быть непо- средственно реализованы на .NET и C++. Неудивительно, что код, написанный на C# .NET, похож на код, который написан на Java, как показано далее:
    public abstract class Shape{
    public abstract void draw();
    }
    Код, написанный на Visual Basic .NET, выглядит так:
    Public MustInherit Class Shape
    Public MustOverride Function draw()
    End Class

    159
    Почему.инкапсуляция.является.фундаментальной.концепцией. .
    Аналогичная функциональность может быть обеспечена на C++ с использова- нием виртуальных методов, а код будет выглядеть следующим образом:
    class Shape
    {
    public:
    virtual void draw() = 0;
    }
    Как уже отмечалось в предыдущих главах, Objective-C и Swift не полностью реализуют функциональность абстрактных классов.
    Например, взгляните на приведенный далее код Java-интерфейса для класса
    Shape
    :
    public abstract class Shape{
    public abstract void draw();
    }
    Соответствующий протокол Objective-C (Swift) показан в следующем коде.
    Обратите внимание, что в коде, написанном как на Java, так и на Objective-C, нет реализации для метода draw()
    :
    @protocol Shape
    @required
    - (void) draw;
    @end // Shape
    На данном этапе функциональность абстрактного класса и протокола является почти одинаковой, однако именно здесь интерфейс Java-типа и протокол раз- личаются. Взгляните на приведенный далее Java-код:
    public abstract class Shape{
    public abstract void draw();
    public void print() {
    System.out.println("Я осуществляю вывод");
    };
    }
    В приведенном выше примере, написанном на Java, метод print()
    обеспечива- ет код, который может быть унаследован тем или иным подклассом. Несмотря на то что дело обстоит аналогичным образом и в C# .NET, VB .NET и C++, этого нельзя сказать о протоколе Objective-C, который выглядел бы так:

    Глава.7..Наследование.и.композиция
    160
    @protocol Shape
    @required
    - (void) draw;
    - (void) print;
    @end // Shape
    В этом протоколе предусмотрена подпись метода print()
    , в силу чего она должна быть реализована подклассом; вместе с тем включение кода невоз- можно. Коротко говоря, подклассы не могут напрямую наследовать какой- либо код от протокола, поэтому протокол нельзя использовать тем же образом, что и абстрактный класс, а это имеет значение при проектировании объектной модели.
    Резюме
    Эта глава содержит базовый обзор того, что представляют собой наследование и композиция и чем они отличаются. Многие авторитетные проектировщики, предпочитающие объектно-ориентированные технологии, утверждают, что композицию следует применять при наличии возможности, а наследование — только тогда, когда это необходимо.
    Однако это немного упрощенный подход. Я считаю, что озвученное утверждение скрывает реальную проблему, которая может заключаться в том, что композиция является более подходящей в большем количестве случаев, чем наследование, а не в том, что ее следует использовать при наличии возможности. Тот факт, что композиция может оказаться более подходящей в большинстве случаев, не оз- начает, что наследование — это зло. Используйте как композицию, так и насле- дование, но только в соответствующем контексте.
    В предшествующих главах концепции абстрактных классов и Java-интерфейсов поднимались несколько раз. В главе 8 мы обратим внимание на концепцию контрактов на разработку, а также рассмотрим, как классы и Java-интерфейсы используются для выполнения этих контрактов.
    1   ...   13   14   15   16   17   18   19   20   ...   25


    написать администратору сайта