Возможно ли ООП и полностью избежать наследования реализации? - PullRequest
21 голосов
/ 24 сентября 2008

Я выберу Java в качестве примера, большинство людей это знают, хотя все остальные ОО-языки тоже работали.

Java, как и многие другие языки, имеет наследование интерфейса и наследование реализации. Например. класс Java может наследовать от другого, и каждый метод, который имеет реализацию там (при условии, что родитель не является абстрактным), также наследуется. Это означает, что интерфейс наследуется и реализация для этого метода также. Я могу перезаписать это, но мне не нужно. Если я не перезаписываю его, я унаследовал реализацию.

Однако мой класс также может «наследовать» (не в терминах Java) только интерфейс без реализации. На самом деле интерфейсы действительно называются так в Java, они обеспечивают наследование интерфейса, но без наследования какой-либо реализации, так как все методы интерфейса не имеют реализации.

Теперь появилась статья , в которой говорится, что лучше наследовать интерфейсы, чем реализации , вы можете прочитать ее (по крайней мере, в первой половине первой страницы), это довольно интересно. Это позволяет избежать таких проблем, как проблема хрупкого базового класса . Пока в этом много смысла, и многое другое, сказанное в статье, имеет для меня много смысла.

Что меня беспокоит, так это то, что наследование реализации означает повторное использование кода , одно из важнейших свойств языков OO. Если бы в Java не было классов (как, например, Джеймс Гослинг, крестный отец Java, согласно этой статье), это решает все проблемы наследования реализации, но как тогда сделать повторное использование кода возможным?

например. если у меня есть класс Car, и у Car есть метод move (), который заставляет Car двигаться. Теперь я могу подкласс Car для разных типов автомобилей, которые все являются автомобилями, но являются специализированными версиями Car. Некоторые могут двигаться по-другому, в любом случае они должны перезаписать move (), но большинство просто сохранят унаследованное движение, так как они движутся так же, как абстрактный родительский Car. Теперь на секунду предположим, что в Java есть только интерфейсы, только интерфейсы могут наследовать друг от друга, класс может реализовывать интерфейсы, но все классы всегда являются окончательными, поэтому ни один класс не может наследовать от любого другого класса.

Как можно избежать того, чтобы при наличии классов Interface Car и сотен Car необходимо было реализовывать идентичный метод move () для каждого из них? Какие концепции повторного использования кода, кроме наследования реализации, существуют в мире ОО?

В некоторых языках есть Mixins. Являются ли Mixins ответом на мой вопрос? Я читал о них, но я не могу себе представить, как Mixins будет работать в мире Java и смогут ли они действительно решить эту проблему здесь.

Другая идея заключалась в том, что существует класс, который реализует только интерфейс Car, давайте назовем его AbstractCar и реализует метод move (). Теперь другие автомобили также реализуют интерфейс Car, внутренне они создают экземпляр AbstractCar и реализуют свой собственный метод move (), вызывая move () на своем внутреннем абстрактном Car. Но разве это не будет тратить ресурсы впустую (метод, вызывающий просто другой метод - хорошо, JIT может встроить код, но все же) и использовать дополнительную память для хранения внутренних объектов, вам даже не понадобится наследование реализации? (в конце концов, каждому объекту нужно больше памяти, чем просто сумме инкапсулированных данных). Также программисту не неудобно писать фиктивные методы, такие как

public void move() {
    abstractCarObject.move();
}

Кто-нибудь может представить себе лучшую идею, как избежать наследования реализации и при этом иметь возможность повторно использовать код простым способом?

Ответы [ 8 ]

11 голосов
/ 24 сентября 2008

Краткий ответ: Да, это возможно. Но вы должны делать это нарочно, а не случайно (используя финал, абстракцию и дизайн с учетом наследования и т. Д.)

Длинный ответ:

Ну, на самом деле наследование не для «повторного использования кода», это для «специализации» класса, я думаю, что это неверное толкование.

Например, очень плохая идея - создать стек из вектора, просто потому что они похожи. Или свойства из HashTable только потому, что они хранят значения. См. [Действует].

«Повторное использование кода» было скорее «бизнес-представлением» о характеристиках ОО, означая, что ваши объекты легко распределялись между узлами; и были переносимы и не имели проблем предыдущего поколения языков программирования. Это было доказано наполовину. Теперь у нас есть библиотеки, которые можно легко распространять; например, в java файлы jar можно использовать в любом проекте, экономя тысячи часов разработки. У OO все еще есть некоторые проблемы с переносимостью и тому подобными вещами, поэтому веб-сервисы сейчас так популярны (как прежде это был CORBA), но это уже другой поток.

Это один из аспектов «повторного использования кода». Другой эффективно, тот, который имеет отношение к программированию. Но в данном случае это не просто «сохранение» строк кода и создание хрупких монстров, но и проектирование с учетом наследования. Это пункт 17 в ранее упомянутой книге; Пункт 17: Дизайн и документация для наследования или иного запрета. См. [Действующий]

Конечно, у вас может быть класс автомобиля и множество подклассов. И да, подход, который вы упомянули об интерфейсе Car, AbstractCar и CarImplementation, - правильный путь.

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

Если вы поступите таким образом, с «хрупким» классом не возникнет проблем (или, по крайней мере, разработчик сознателен или угроза), и подклассы завершают только те части, которые ему разрешены.

Наследование - это больше, чтобы «специализировать» классы, таким же образом Truck является специализированной версией Car, а MosterTruck специализированной версией Truck.

Он не имеет смысла создавать подкласс «ComputerMouse» из автомобиля только потому, что у него есть колесо (колесо прокрутки), как у автомобиля, он движется и имеет колесо ниже только для сохранения строк кода. Он принадлежит к другому домену и будет использоваться для других целей.

Способ предотвращения наследования "реализации" в языке программирования с самого начала, вы должны использовать ключевое слово final в объявлении класса, и таким образом вы запрещаете подклассы.

Подклассы не являются злом, если они сделаны специально. Если это сделано неосторожно, это может стать кошмаром. Я бы сказал, что вы должны начать как можно более приватно и «окончательно», а при необходимости сделать вещи более публичными и расширяемыми. Это также широко объясняется в презентации «Как создавать хорошие API и почему это важно». См. [Good API]

Продолжайте читать статьи, и со временем и практикой (и большим терпением) эта вещь прояснится. Хотя иногда вам просто нужно выполнить работу и скопировать / вставить некоторый код: P. Это нормально, пока вы сначала стараетесь делать это хорошо.

Вот ссылки обоих Джошуа Блоха (ранее работавшего в Sun в ядре Java, теперь работающего на Google)


[Эффективное] Эффективная Java. Определенно лучшая Java-книга, которую не начинающий должен изучать, понимать и практиковать. Должно быть.

Эффективная Java


[Хороший API] Презентация, в которой рассказывается о дизайне API, возможности его повторного использования и смежных темах. Это немного долго, но это стоит каждую минуту.

Как разработать хороший API и почему это важно

С уважением.


Обновление: взгляните на минуту 42 видео ссылки, которую я вам отправил. Об этом говорит эта тема:

"Если у вас есть два класса в публичном API и вы думаете, что один из них должен быть подклассом другого, как, например, Foo является подклассом Bar, спросите себя, является ли Every Foo Bar? ..."

И в предыдущей минуте говорится о «повторном использовании кода» во время разговора о TimeTask.

8 голосов
/ 24 сентября 2008

Проблема с большинством примеров против наследования - это примеры, когда человек использует наследование неправильно, а не из-за неправильного наследования.

В статье, на которую вы разместили ссылку, автор показывает «сломанность» наследования с помощью Stack и ArrayList. Пример некорректен, потому что Stack не является ArrayList и, следовательно, наследование не должно использоваться. В качестве примера можно привести такие же недостатки, как символ расширения строки или номер расширения PointXY.

Прежде чем расширять класс, вы всегда должны выполнять тест "is_a". Поскольку вы не можете сказать, что каждый стек является списком ArrayList без какой-либо ошибки, то не стоит наследовать.

Контракт для Stack отличается от контракта для ArrayList (или List), и стек не должен наследовать методы, которые не заботятся (например, get (int i) и add ()). Фактически, стек должен быть интерфейсом с такими методами, как:

interface Stack<T> {
   public void push(T object);
   public T pop();
   public void clear();
   public int size();
}

Класс, подобный ArrayListStack, может реализовывать интерфейс стека, и в этом случае использовать композицию (имеющую внутренний ArrayList), а не наследование.

Наследование не плохо, плохое наследование плохо.

4 голосов
/ 24 сентября 2008

Вы также можете использовать композицию и шаблон стратегии. текст ссылки

public class Car
{
  private ICar _car;

  public void Move() {
     _car.Move();
  }
}

Это гораздо более гибко, чем использование поведения на основе наследования, поскольку позволяет изменять во время выполнения, заменяя при необходимости новые типы автомобилей.

2 голосов
/ 10 октября 2008

Забавно ответить на мой собственный вопрос, но я нашел кое-что довольно интересное: Sather .

Это язык программирования без наследования реализации вообще! Он знает интерфейсы (называемые абстрактными классами без реализации или инкапсулированных данных), и интерфейсы могут наследовать друг друга (на самом деле они даже поддерживают множественное наследование!), Но класс может реализовывать только интерфейсы (абстрактные классы, сколько угодно), оно не может наследоваться от другого класса. Однако он может «включать» другой класс. Это скорее концепция делегата. Включенные классы должны быть созданы в конструкторе вашего класса и уничтожены при уничтожении вашего класса. Если вы не перезаписываете методы, которые они имеют, ваш класс также наследует их интерфейс, но не их код. Вместо этого создаются методы, которые просто перенаправляют вызовы вашего метода в метод с равными именами включаемого объекта. Разница между включенными объектами и просто инкапсулированными объектами заключается в том, что вам не нужно создавать делегирование вперед, и они не существуют как независимые объекты, которые вы можете передавать, они являются частью вашего объекта и живут и умирают вместе с вашим object (или, если говорить технически, более подробно): память для вашего объекта и всех включенных в него создается с помощью одного вызова alloc, того же блока памяти, вам просто нужно инициировать их в вызове конструктора, в то время как при использовании реальных делегатов каждый из этих объектов вызывает собственный вызов alloc, имеет собственный блок памяти и живет полностью независимо от вашего объекта).

Язык не такой красивый, но мне нравится идея, стоящая за ним: -)

2 голосов
/ 24 сентября 2008

Чтобы сделать миксины / композиции проще, взгляните на мой процессор аннотаций и аннотаций:

http://code.google.com/p/javadude/wiki/Annotations

В частности, пример миксинов:

http://code.google.com/p/javadude/wiki/AnnotationsMixinExample

Обратите внимание, что в настоящее время он не работает, если делегированные интерфейсы / типы имеют параметризованные методы (или параметризованные типы в методах). Я работаю над этим ...

2 голосов
/ 24 сентября 2008

Вы можете использовать состав . В вашем примере объект Car может содержать другой объект с именем Drivetrain. Метод move () автомобиля может просто вызвать метод drive () его трансмиссии. Класс Drivetrain может, в свою очередь, содержать такие объекты, как Engine, Transmission, Wheels и т. Д. Если вы структурируете иерархию классов таким образом, вы можете легко создавать автомобили, которые движутся по-разному, составляя их из различных комбинаций более простых частей (т.е. повторно использовать код).

1 голос
/ 24 сентября 2008

Наследование не обязательно для объектно-ориентированного языка.

Рассмотрим Javascript, который даже более объектно-ориентирован, чем Java. Там нет классов, только объекты. Код повторно используется путем добавления существующих методов к объекту. Объект Javascript, по сути, представляет собой карту имен для функций (и данных), где начальное содержимое карты устанавливается прототипом, и новые записи могут быть добавлены в данный экземпляр на лету.

0 голосов
/ 24 сентября 2008

Вы должны прочитать Design Patterns. Вы обнаружите, что интерфейсы имеют решающее значение для многих типов полезных шаблонов проектирования. Например, абстрагирование различных типов сетевых протоколов будет иметь один и тот же интерфейс (к вызывающему его программному обеспечению), но мало повторного использования кода из-за разного поведения каждого типа протокола.

Некоторые алгоритмы открывают глаза на показ того, как собрать множество элементов программирования для выполнения некоторой полезной задачи. Шаблоны проектирования делают то же самое для объектов. Показывает, как комбинировать объекты таким образом, чтобы выполнить полезную задачу.

Шаблоны дизайна от банды четырех

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...