Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Ruby / Ruby-новые грани.doc
Скачиваний:
139
Добавлен:
06.06.2015
Размер:
821.25 Кб
Скачать

4.9 Наследование

Ruby поддерживает только одиночное наследование, и все классы образуют единую иерархию с одним общим корнем — классом Object.

Базовый класс указывается при помощи специального синтаксиса во время описания класса:

  classDerived < Base

    ...

  end

В этом примере класс Derived произведен от класса Base.

Определить суперкласс любого класса можно с помощью метода superclass:

  class Base

  end

  

  class Derived < Base

  end

  

  Base.superclass                  # => Object

  Derived.superclass               # => Base

Этот пример показывает, что если во время описания явно не задать имя суперкласса (как в случае класса Base), то Ruby автоматически использует в качестве суперкласса Object.

Как и в других объектно-ориентированных языках, наследование в Ruby используется для того, чтобы производные классы расширяли или изменяли поведение своих суперклассов. Это достигается за счет перекрытия методов базового класса и внесения в производный класс собственных методов. В этом смысле Ruby не преподносит разработчику каких-либо сюрпризов. Но некоторые особенности, связанные с наследованием, все-таки есть.

Например, если производный класс определяет собственный конструктор, то конструктор базового типа автоматически не вызывается:

  class Base

    def initialize

      puts "Base#initialize"

    end

  end

  

  class Derived < Base

    def initialize

      puts "Derived#initialize"

    end

  end

  

  Derived.new

Этот код приводит к печати сообщения:

  Derived#initialize

Т.е. конструктор базового класса управление не получил, поскольку это нужно делать явно с помощью ключевого слова super:

  class Derived < Base

    def initialize

      puts "Derived#initialize"

      super

    end

  end

Что приводит к вызову конструктора базового класса:

  Derived#initialize

  Base#initialize

За вызов конструктора базового класса отвечает производный класс, и этот вызов может быть выполнен в любой момент. В сочетании с еще одной особенностью наследования это может приводить к интересным эффектам.

Дело в том, что в Ruby все методы класса являются, в терминологии других языков, виртуальными. Т.е. любой метод можно переопределить в производном классе:

  class Base

    def first

      "Base#first"

    end

  

    def second

      "Base#second"

    end

  end

  

  class Derived < Base

    def first

      "Derived#first"

    end

  end

  

  d = Derived.new

  d.first                          # => "Derived#first"

  d.second                         # => "Base#second"

  

  class AnotherDerived < Derived

    def second

      "AnotherDerived#second"

    end

  end

  

  a = AnotherDerived.new

  a.first                          # => "Derived#first"

  a.second                         # => "AnotherDerived#second"

Здесь видно, как класс Derived переопределил метод first, но унаследовал без изменений метод second. В свою очередь, класс AnotherDerived переопределил метод second, но оставил без изменения метод first.

Ситуация усугубляется еще и тем, что в Ruby нет возможности указать, что какой-то конкретный метод больше не может быть переопределен (т.е. в Ruby нет аналога ключевого слова final из Java).

Когда для объекта какого-то класса вызывается метод или, говоря более точно, объекту отсылается сообщение, Ruby ищет обработчик этого сообщения (т.е. тело метода) по имени сообщения. Поиск начинается с того класса, которому принадлежит объект. Если в этом классе соответствующий обработчик не найден, то поиск продолжается в непосредственном суперклассе, затем в суперклассе суперкласса и т.д. Неважно, в каком контексте производится вызов метода — происходит ли это в каком-то методе самого объекта, в каком-то методе базового класса или где-то еще. Ruby всегда точно знает класс объекта, и поиск метода начинается именно в этом классе.

Такое поведение приводит к тому, что если в конструкторе суперкласса вызывается метод, перекрытый в производном классе, то вызывается именно перекрытая производным классом версия метода:

  class Base

    def initialize

      puts self.class.name

      puts first

      puts second

    end

  

    def first

      "Base#first"

    end

  

    def second

      "Base#second"

    end

  end

  

  class Derived < Base

    def first

      "Derived#first"

    end

  end

  

  d = Derived.new

что приводит к:

  Derived

  Derived#first

  Base#second

т.е. сначала Base#initialize печатает имя класса, к которому реально принадлежит объект (Derived), затем результаты методов Derived#first и Base#second.

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

В Ruby такого контроля нет. Поэтому разработчик может столкнуться с тем, что его код работает не так, как предполагалось:

  class ConnectionParams

    attr_reader :host, :user, :password

  

    def initialize(host, user, password)

      @host, @user, @password = host, user, password

  

      # Метод setup_default назначит значения по умолчанию тем атрибутам,

      # которые получили значение nil от пользователя.

      setup_defaults

    end

  

    def setup_defaults

      # Если не задан @host, то используется localhost.

      @host = ’localhost’ unless @host

    end

  end

  

  class SSLConnectionParams < ConnectionParams

    attr_reader :cert, :ca_cert

  

    def initialize(

        host, user, password,

        cert, ca_cert,

        default_cert = ’~/user.pem’,

        default_ca_cert = ’~/ca.pem’)

      # Внимание: в конструкторе базового класса произойдет

      # вызов setup_defaults.

      super(host, user, password)

  

      @cert, @ca_cert = cert, ca_cert

      @default_cert, @default_ca_cert = default_cert, default_ca_cert

    end

  

    defsetup_defaults

      # Выполнение действий базового класса...

      super

  

      # ...а затем собственных.

      @cert = @default_cert unless@cert

      @ca_cert = @default_ca_cert unless@default_ca_cert

    end

  end

  

  ssl = SSLConnectionParams.new(nil, ’user’, ’pwd’, nil, nil)

  ssl.host                          # => "localhost"

  ssl.cert                          # => nil

  ssl.ca_cert                       # => nil

В данном случае делалась попытка позволить пользователю не задавать явно параметр host в конструкторе ConnectionParams, и параметры cert, ca_cert в конструкторе SSLConnectionParams. Предполагалось, что конструктор посредством метода setup_defaults при необходимости подставит значения по умолчанию.

Для атрибута ConnectionParams#host это сработало, но в случае с SSLConnectionParams#cert/ca_cert произошла ошибка, от которой разработчик в C++ защищен — переопределенный метод SSLConnectionParams#setup_defaults был вызван из конструктора базового класса еще до того, как конструктор SSLConnectionParams проинициализировал атрибуты класса SSLConnectionParams.

В методе SSLConnectionParams#setup_default не произошло никакой ошибки при обращении к неизвестным на тот момент атрибутам cert/default_cert и ca_cert/default_ca_cert из-за того, что Ruby определяет атрибуты в объекте при первом обращении к ним. При этом, если атрибут стоит в правой части присваивания (как в случае с default_cert/default_ca_cert), то в качестве значения атрибута берется nil.

Тем не менее, несмотря на потенциальные опасности, ручной вызов конструктора базового класса в произвольный момент и вызов в базовом классе самых последних версий переопределенных методов имеют и положительные стороны. Например, пусть есть иерархия классов для отображения элементов текстового документа на экране. У каждого элемента есть свой набор атрибутов, таких как гарнитура шрифта, размер, вид (полужирный, курсив и т.д.). Все эти параметры будут присутствовать у всех элементов, поэтому можно определить базовый класс, например, ElementPainter, который в своем конструкторе выполнит создание необходимых графических ресурсов (или породит исключение, если это не удалось). А производные классы будут сообщать ему свои специфические настройки посредством переопределенных методов. Например:

  classElementPainter

    definitialize

      # Конструирование графических объектов с помощью методов

      # font_family, font_size, bold?, italic?, underlined?

      ...

    end

  

    # Производные классы будут переопределять эти методы.

    def font_family; ’Times New Roman’; end

    def font_size; 10; end

    def bold?; false; end

    def italic?; false; end

    def underlined? false; end

    ...

  end

  

  # Заголовки частей, глав, разделов.

  # Все они рисуются полужирным шрифтом, но разных размеров.

  class HeaderElementPainter < ElementPainter

    def bold?; true; end

  end

  

  class PartHeaderElementPainter < HeaderElementPainter

    def font_size; 16; end

  end

  

  class ChapterHeaderElementPainter < HeaderElementPainter

    def font_size; 14; end

  end

  

  class SectionHeaderElementPainter < HeaderElementPainter

    def fond_size; 12; end

  end

  

  # Примеры кода рисуются шрифтом Courier New размера 8 пунктов.

  class SourceCodeElementPainter < ElementPainter

    def font_family; ’Courier New’; end

    def font_size; 8; end

  end

  ...

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

Соседние файлы в папке Ruby