Try English version of Quizful



Раздаем бесплатные Q! подробности в группе Quizful.Alpha-test
Топ контрибуторов
loading
loading
Знаете ли Вы, что

Список полученных сертификатов находится на странице Вашего профиля. Сертификаты можно распечатать или разместить на Вашем сайте.

Лента обновлений
ссылка 14:16:51
Добавлен вопрос в тест Python 3 Основы
ссылка 12:51:41
Комментарий от igboev:
Очень интересный материал для новичков, спасибо!
Печально...
ссылка 12:29:45
Комментарий от topor:
к hrybs
ссылка 11:22:47
Комментарий от ipaSoft:
Ловушка для Java мэнов
ссылка 11:18:55
Комментарий от ipaSoft:
ёкарный бабай. Я был уверен что никогда не попадусь на э...
Статистика

Тестов: 153, вопросов: 8597. Пройдено: 422074 / 2061724.

Проблемы множественного динамического приведения типов и их решения

head tail Статья
категория
Java
дата19.08.2009
авторalexis112
голосов12

Часто в повседневном программировании мы сталкиваемся с ситуацией, когда приходится производить множественное динамическое приведение типов. Вот общий характерный пример подобных случаев (код в статье пишется на языке Java, из-за того, что он наиболее распространен, хотя все идеи будут также применимы и к C++, и к C#; намеренно избегается употребления Java-специфичных конструкций, таких как abstract class и пр.):

Подход 1


class Base {
//...
} 

class Sub1 extends Base {
//...
} 

class Sub2 extends Sub1 {
//...
} 

class Sub3 extends Base {
//...
} 

class Processor {
      public void process(Base base) {
            if (base instanceof Sub1) {
                  //Processing of Sub1
            } else if (base instanceof Sub2) {
                  //Processing of Sub2
            } else if (base instanceof Sub3) {
                  //Processing of Sub3
            } else {
                  //Processing of Base of some unknown Base subclass
            }
      }
}

Внимательный читатель мог заметить в приведенном коде ошибку: блок кода, связанный с обработкой Sub2 никогда не вызовется. Дело в том, что любой экземпляр класса Sub2 может быть успешно приведен к Sub1, а, поскольку попытки приведения продолжаются до первого успешного, метод удовлетворится приведением к Sub1. Ошибка исправляется простой сменой последовательности приведений. Она была допущена автором нарочно, чтобы подчеркнуть недостатки данного подхода. Действительно, в большом проекте с разветвленной иерархией легко ошибиться таким образом.

Правильный подход для организации такой обработки с точки зрения классического ООП - это замена централизованного обработчика в методе Processor.process() на полиморфный метод, переопределенный для каждого из классов:

Подход 2


class Base {
      public void process() {
            //Processing of Base of some unknown Base subclass
      }
} 

class Sub1 extends Base {
      public void process() {
            //Processing of Sub1
      }
} 

class Sub2 extends Sub1 {
      public void process() {
            //Processing of Sub2
      }
} 

class Sub3 extends Base {
      public void process() {
            //Processing of Sub3
      }
}

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

Поход, который предлагает программирование с использованием паттернов - замена метода process() на паттерн Visitor. Приведем сначала модифицированный код:

Подход 3


class Base {
      public void accept(Visitor visitor) {
            visitor.process(this);
      }
} 

class Sub1 extends Base {
      public void accept(Visitor visitor) {
            visitor.process(this);
      }
} 

class Sub2 extends Sub1 {
      public void accept(Visitor visitor) {
            visitor.process(this);
      }
} 

class Sub3 extends Base {
      public void accept(Visitor visitor) {
            visitor.process(this);
      }
} 

class Visitor {
      public void process(Base base) {
            //Processing of Base of some unknown Base subclass
      }

      public void process(Sub1 sub1) {
            //Processing of Sub1
      }

      public void process(Sub2 sub2) {
            //Processing of Sub2
      }

      public void process(Sub3 sub3) {
            //Processing of Sub3
      }
} 

Изящество данного подхода (в отличие от первого) заключается в том, что приведение типов, по сути, выполняется засчет все того же полиморфного вызова, что и во втором подходе. Действительно, в перегруженном методе accept(), ключевое слово this каждый раз обозначает объект разного типа. Далее, языковые механизмы вызова перегруженных функций вызывают функцию Visitor.process() от максимально подходящего по типу аргумента (то есть, в данном случае, от самого его типа). Понятно, что для обеспечения большего, чем один количества способов обработки, можно создать целую иерархию подклассов Visitor и упешно передавать их в методы accept(), используя все тот же полиморфизм. Данный подход хорош, если нужно добавить в проект еще один способ обработки классов из иерархии Base. Для этого нужно, всего лишь, создать новых подкласс Visitor. Правда, он (подход) не настолько хорош, если в проект добавляется новый класс. Ведь при этом, если иерархия Visitor достаточно разветвленная, придется добавлять соответствующий метод process() в каждый из них. И тогда, более приемлемым оказывается второй подход.

А теперь краткое подведение итогов:

Подход с множественным динамическим приведением типов:

  • Легко добавлять новые способы обработки (+)
  • Не требует какого-либо изменения интерфейса иерархии Base (+)
    (важно когда интерфейсы жестко заданы, например, официальной спецификацией)
  • Трудно добавлять новые классы в иерархию Base (-)
  • Подвержен ошибкам (-)
  • Сосредотачивает в одном методе большое количество кода (-)

Подход с добавлением полиморфного метода для каждого способа обработки:

  • Более надежен, чем множественное динамическое приведение (+)
  • Легко добавлять новые классы в иерархию Base (+)
  • Трудно добавлять новые методы обработки (-)
  • Усложняет интерфейсы иерархии Base (-)
  • Меняет интерфейс иерархии Base(-)

Подход с использованием паттерна Visitor:

  • Более надежен, чем множественное динамическое приведение (+)
  • Легко добавлять новые методы обработки (+)
  • Трудно добавлять новые классы в иерархию Base (+)
  • Меняет интерфейс иерархии Base (-)

Теперь рассмотрим другой пример кода. Предположим, мы разрабатываем игру. Необходимо обеспечить коллизии между объектами игрового мира. Все они принадлежат к одной иерархии классов. По традиции, рассмотрим подход с множественным (еще более множественным, чем в первой части :) динамическим приведением:


class GameObject {

} 

class Car extends GameObject {

} 

class PoliceCar extends Car {

} 

class Bike extends GameObject {

} 

class Collider/*:))*/ {
      public static void collide(GameObject obj1, GameObject obj2) {
            if (obj1 instanceof PoliceCar) {
                  if (obj2 instanceof PoliceCar) {
                        //Process collision
                  } else if (obj2 instanceof Car) {
                        //Process collision
                  } else if (obj2 instanceof Bike) {
                        //Process collision
                  } else {
                        //Unknown object
                  }
            } else if (obj1 instanceof Car) {
                  if (obj2 instanceof PoliceCar) {
                        //Process collision
                  } else if (obj2 instanceof Car) {
                        //Process collision
                  } else if (obj2 instanceof Bike) {
                        //Process collision
                  } else {
                        //Unknown object
                  }
            } else if (obj1 instanceof Bike) {
                  if (obj2 instanceof PoliceCar) {
                        //Process collision
                  } else if (obj2 instanceof Car) {
                        //Process collision
                  } else if (obj2 instanceof Bike) {
                        //Process collision
                  } else {
                        //Unknown object
                  }
            } else {
                  //Unknown object
            }
      }
} 

Автор сильно утомился уже пока писал с помощью копипаста текст примера:). И, правда, сложно представить себе такой код, если классов объектов в игре десятки или сотни. Этот подход является полной аналогией первого подхода из ч. 1 статьи.

Легко понять, что второй подход с реализацией в объектах полиморфного метода здесь неприменим (полиморфизм предлагает механизм для определения типа всего лишь одного аргумента). Поэтому перейдем сразу к решению с использованием Visitor. Для описанного ниже подхода в литературе часто можно встретить название множественная диспетчеризация или мультиметоды. Вначале – листинг кода:


class Collidable {
      public void collide(Collidable c) {
            //Must be overloaded correctly
      }

      public void collideTo(GameObject go) {
            collide(go); 
      }

      public void collideTo(Car c) {
            collide(c); 
      }

      public void collideTo(PoliceCar pc) { 
            collide(pc);
      }

      public void collideTo(Bike b) { 
            collide(b);
      }
} 

class GameObject extends Collidable {
      public void collide(Collidable c) {
            c.collideTo(this);
      }

      public void collideTo(GameObject go) {
            //Process collision 
      }
} 

class Car extends GameObject {
      public void collide(Collidable c) {
            c.collideTo(this);
      }

      public void collideTo(GameObject go) {
            //Process collision 
      }

      public void collideTo(Car c) {
            //Process collision 
      }
} 

class PoliceCar extends Car {
      public void collide(Collidable c) {
            c.collideTo(this);
      }

      public void collideTo(GameObject go) {
            //Process collision 
      }

      public void collideTo(Car c) {
            //Process collision 
      }

      public void collideTo(PoliceCar pc) { 
            //Process collision
      }
} 

class Bike extends GameObject {
      public void collide(Collidable c) {
            c.collideTo(this);
      }

      public void collideTo(GameObject go) {
            //Process collision 
      }

      public void> collideTo(Car c) {
            //Process collision 
      }

      public void collideTo(PoliceCar pc) { 
            //Process collision
      }

      public void collideTo(Bike b) { 
            //Process collision
      }
} 

class Collider {
      public static void collide(GameObject obj1, GameObject obj2) {
            obj1.collide(obj2);
      }
}

Здесь применяется двойное приведение типов с помощью паттерна Visitor. Каждый объект является как Visitor, так и Acceptor. При вызове метода collide() происходит приведение типа его аргумента посредствам полиморфизма см. часть 1 (метод collide перегружен в каждом классе). Но методы collideTo() для каждого класса тоже определены свои и поэтому при вызове collideTo() в методе collide() выполняется еще одно приведение типа засчет полиморфности каждого из collideTo().

В простейшей реализации можно было бы переопределить все методы collideTo() для каждого из подклассов иерархии. Но тогда теоретически существовали бы две независимые реализации каждого типа коллизии (то есть не гарантировалось бы, что вызовы obj1.collide(obj2) и obj2.collide(obj1) приведут к одинаковому результату). В таком подходе для добавления нового класса объекта игрового мира потребовалось бы модифицировать код всех других классов (добавить в каждый из них соответствующий метод collideTo()). Это привело бы к дополнительным затратам на интеграцию кода (см. часть 1). Скорее всего, нам это не нужно. Реализация, приведенная в листинге лишена описанных недостатков. При соответствующей реализации методов collideTo() по умолчанию (каждый из них перенаправляет вызов своему аргументу), которую каждый из старых классов наследует, он (этот класс) не нуждается в собственной реализации новых версий collideTo() (которые в данном примере, вероятно, получались бы копипастом и создавали дополнительное дублирование).

Пример:

Предположим, нам нужно добавить в игру класс:


class MountainBike extends Bike {
} 

Что для этого нужно:

1) Добавить в класс Collidable метод collideTo(MountainBike mb):


class Collidable {
      //...
      public void collideTo(MountainBike mb) {
            collide(mb);
      }
}

2) Реализовать класс MountainBike:


class MountainBike extends Bike {
      public void collide(Collidable c) {
            c.collideTo(this);
      }

      public void collideTo(GameObject go) {
            //Process collision 
      }

      public void collideTo(Car c) {
            //Process collision 
      }

      public void collideTo(PoliceCar pc) { 
            //Process collision
      }

      public void collideTo(Bike b) { 
            //Process collision
      }

      public void collideTo(MountainBike mb) {
            //Process collision
      }

}

Все, теперь коллизии между всеми объектами будут обрабатываться корректно. И для этого мы поменяли всего лишь два класса!

Единственная опасность, которую таит данный подход - если мы при определении очередного класса забудем переопределить один из методов collideTo() может возникнуть зацикливание, то есть постоянные перенаправления вызовов через реализацию по умолчанию. Этого можно избежать, добавив в класс Collidable простой флаг перенаправления. По умолчанию он имеет значение false, перед тем как перенаправить обработку коллизии ему будет присвоено true и повторного перенаправления не произойдет. По окончании обработки флагу вновь присваивается значение false. Чтобы данный подход работал в многопоточном приложении нужно синхронизировать все методы collide().

--
Алексей Спиридонов

Если Вам понравилась статья, проголосуйте за нее

Голосов: 12  loading...
geolip   admin   Vier   chich1   yohan   Igor_K   ost   amazurok   SiarheiZoo   TeaWitch   SamTan   vladek