Гибкие программы ориентированы на полиморфизм, а не наследование . Некоторые языки фокусируются на статической проверке типов ( C ++ , Java , C # ), которая связывает концепции и уменьшает возможности полиморфизма . Языки, которые разделяют понятия, могут позволить вам сосредоточиться на полиморфизме и создать более надежный код. JavaScript, Python, Ruby и VB.NET не имеют типизированных переменных и откладывают проверку типов до времени выполнения. Стоит ли ценить статическую проверку типов, отказываясь от силы чистого полиморфизма во время выполнения?
Наследование и полиморфизм являются независимыми, но взаимосвязанными объектами — возможно иметь одно без другого. Если мы используем язык, который требует, чтобы переменные имели определенный тип ( C ++ , C # , Java ), то мы можем полагать, что эти понятия связаны между собой.
Если вы используете только те языки, которые не требуют, чтобы переменные были объявлены с определенным типом, например, var в JavaScript , def в Python , def в Ruby , dim в VB.NET, то вы, вероятно, понятия не имеете, о чем я кричу! ?
Я считаю, что преимущества чистого полиморфизма перевешивают ценность статической проверки типов. Теперь, когда у нас есть быстрые процессоры, сложные отладчики и конструкции исключений во время выполнения, значение проверки типов во время компиляции минимально. Некоторая борьба с полиморфизмом , поэтому давайте определим это:
Полиморфизм
— это способность отправлять сообщение объекту, не зная, каков его тип.
Полиморфизм — это причина, по которой мы можем водить машины друг друга и почему у нас нет проблем с использованием разных выключателей
,
Автомобиль является полиморфным, потому что вы можете отправлять общеизвестные сообщения ЛЮБОМУ автомобилю ( start (), accelerate (), turnLeft (), turnRight () и т. Д.), Не зная, ВОЗ построила автомобиль. Переключатель освещения является полиморфным, поскольку вы можете отправлять сообщения turnOn () и turnOff () на любой переключатель освещения, не зная, кто его изготовил.
Полиморфизм — это буквально то, что заставляет нашу экономику работать. Это позволяет нам создавать функционально эквивалентные продукты, которые могут иметь совершенно разные реализации. Это основа для ценовых и качественных различий в продуктах, тостерах, блендерах и т. Д.
Полиморфизм через наследование
UML диаграмма выше показывает , как полиморфизм говорится в таких языках , как C ++ , Java и C # . Метод (он же операция) start () объявляется абстрактным (в UML), который откладывает реализацию метода для подклассов на вашем целевом языке. Метод start () объявлен в классе Car и определяет только сигнатуру метода, а не реализацию (технически полиморфизм требует отсутствия кода для метода start () в классе Car ).
Код для метода start () затем реализуется отдельно в подклассах VolkswagenBeetle и SportsCar . Полиморфизм подразумевает, что start () реализован с использованием различных атрибутов в подклассах, в противном случае метод start () может быть просто реализован в суперклассе Car .
Несмотря на то, что большинство из нас больше не пишут код на C ++ , полезно понять, почему сильная связь между наследованием и полиморфизмом убивает гибкость.
// C++ polymorphism through inheritance class Car { // declare signature as pure virtual function public virtual boolean start() = 0; } class VolkswagenBeetle : Car { public boolean start() { // implementation code } } class SportsCar : Car { public boolean start() { // implementation code } } // Invocation of polymorphism Car cars[] = { new VolkswagenBeetle(), new SportsCar() }; for( I = 0; I < 2; i++) Cars[i].start();
Массив cars относится к типу Car и может содержать только объекты, производные от Car ( VolkswagenBeetle и SportsCar ), и полиморфизм работает, как и ожидалось. Однако предположим, что у меня был следующий дополнительный класс в моей программе на C ++ :
// C++ lack of polymorphism with no inheritance class Jalopy { public boolean start() { … } } // Jalopy does not inherit from Car, the following is illegal Car cars[] = { new VolkswagenBeetle(),new Jalopy() }; for( I = 0; I < 2; i++) Cars[i].start();
Во время компиляции это приведет к ошибке, потому что тип Jalopy не является производным от Car . Хотя оба они реализуют метод start () с одинаковой сигнатурой, компилятор остановит меня из-за статической ошибки типа.
Строгая проверка типов, введенная во время компиляции, означает, что весь полиморфизм должен проходить через наследование. Это приводит к проблемам с глубокой иерархией наследования и множественным наследованием, когда возникают всевозможные проблемы с неожиданными побочными эффектами. Даже умеренно сложные программы становятся очень сложными для понимания и поддержки на C ++ .
Историческая справка: C ++ доминировал до середины 1990-х годов просто потому, что это было объектно-ориентированное решение, которое НЕ было интерпретировано. Это означало, что на медленных процессорах того времени он имел приличную производительность. Мы использовали C ++, потому что мы не могли получить сопоставимую производительность ни с одним из интерпретируемых объектно-ориентированных языков того времени, т.е. Smalltalk.
Ослабление связи
Негативные последствия тесной связи между наследованием и полиморфизмом приводят к тому, что Java и C # вводят концепцию интерфейса, которая разделяет идеи наследования и полиморфизма, но при этом строго контролирует тип во время компиляции.
Во-первых, можно реализовать приведенный выше пример C ++ точно с использованием наследования, как показано в C # ниже:
// C# polymorphism using inheritance class Car { public virtual boolean start(); // declare signature } class VolkswagenBeetle : Car { public override boolean start() { // implementation code } } class SportsCar : Car { public override boolean start() { // implementation code } } // Invocation of polymorphism Car cars[] = { new VolkswagenBeetle(), new SportsCar() }; for( I = 0; I < 2; i++) Cars[i].start();
Кроме того, используя концепцию интерфейса, мы можем написать классы на Java следующим образом:
// Java polymorphism using interface interface Car { public boolean start(); } class VolkswagenBeetle implements Car { public boolean start() { // implementation code } } class SportsCar implements Car { public boolean start() { // implementation code } }
Используя
интерфейс , реализации VolkswagenBeetle и SportsCar могут быть полностью независимыми, если они продолжают удовлетворять интерфейсу Car. Таким образом, теперь мы можем сделать наш класс Jalopy полиморфным с двумя другими классами просто:
class Jalopy implements Car { … }
Полиморфизм без наследования
Есть языки, где у вас есть полиморфизм без использования наследования . Некоторыми примерами являются JavaScript, Python, Ruby, VB.NET и Small Talk.
На каждом из этих языков можно написать car.start (), ничего не зная об объекте car и его методе.
# Python polymorphism class VolkswagenBeetle(Car): def start(): # Code to start Volkswagen class SportsCar(Car): def start(): # Code to start SportsCar # Invocation of polymorphism cars = [ VolkswagenBeetle(), SportsCar() ] for car in cars: car.start()
Возможность получить чистый полиморфизм из этих языков имеет только один тип переменной до времени выполнения: var в JavaScript, def в Python, def в Ruby, dim в VB.NET. Только с одним типом переменной не может быть ошибки типа до времени выполнения.
Историческая справка: только во время введения Java и C # мощность процессора была достаточной для интерпретируемых языков, чтобы обеспечить достаточную производительность во время выполнения. Переход от полиморфизма и наследования, тесно связанных с более слабой связью, зависел от способности интерпретаторов времени выполнения выполнять практические приложения с достойной производительностью.
Там нет такого понятия, как бесплатный обед
Вы можете столкнуться со странным поведением, когда выполняете вызовы методов для объектов, которые не реализуют метод, когда проверка типов откладывается до времени выполнения, то есть отправка start () объекту без метода start ().
Когда проверка типов откладывается до времени выполнения, вы хотите, чтобы объект отвечал: « Я понятия не имею, как запустить () », если вы случайно отправили ему метод start ().
Некоторые чисто полиморфные языки обычно имеют способ обнаружения отсутствующих методов:
- В Visual Basic вы можете получить исключение NotImplementedException
- В Ruby вы либо реализуете метод _ missing (), либо перехватываете исключение NoMethodError
- В Smalltalk вы получаете исключение # didNotUnderstand
Некоторые языки не имеют исключений, но есть неуклюжие обходные пути:
- В Python вы должны использовать вызов getattr (), чтобы увидеть, существует ли атрибут для имени, а затем использовать callable (), чтобы выяснить, можно ли его вызвать. Для приведенного выше примера автомобиля это будет выглядеть так:
startCar = getattr(obj, "start", None) if callable(startCar): startCar ()
- JavaScript ( ECMAScript ) вызовет исключение только для отсутствующего метода в Firefox / Spidermonkey.
Даже если у вас есть четко определенный механизм исключений (т. Е. Try catch), когда вы откладываете проверку типов на время выполнения, становится все труднее доказать, что ваши программы работают правильно.
Нетипизированные переменные во время разработки позволяют разработчику создавать коллекции разнородных объектов (то есть наборов, сумок, векторов, карт, массивов).
Когда вы выполняете итерацию по этим разнородным коллекциям, всегда существует возможность вызова метода для объекта, который не реализован. Даже если вы получите исключение, когда это произойдет, может потребоваться много времени, чтобы найти тонкие проблемы.
Вывод
Понятия полиморфизма и наследования связаны только в том случае, если ваш язык требует статической проверки типов ( C ++ , Java , C # и т. Д.). Любой язык, имеющий только общий тип для объявления переменных, имеет полное разделение полиморфизма и наследования ( JavaScript , Python , Ruby , VB.NET ), независимо от того, скомпилированы ли они в байтовый код или интерпретируются напрямую.
Исходные скомпилированные языки ( C ++ и т. Д.) Выполняли статическую проверку типов из-за проблем с производительностью. Выполнение проверки типов во время компиляции создало прочную связь между наследованием и полиморфизмом. Необходимые глубокие структуры наследования классов и множественное наследование приводят к побочным эффектам времени выполнения и коду, который трудно понять.
Такие языки, как C # и Java, использовали понятие интерфейса для сохранения проверки типов во время компиляции, чтобы ослабить связь между наследованием и полиморфизмом. Кроме того, эти языки компилируются в байтовый код, который интерпретируется во время выполнения, чтобы обеспечить баланс между статической проверкой типов и производительностью во время выполнения.
Такие языки, как Ruby , Python , JavaScript , Visual Basic и Smalltalk, используют мощные процессоры для использования интерпретаторов для отсрочки проверки типов во время выполнения (независимо от того, скомпилирован ли исходный код в байт-код или чисто интерпретирован). Откладывая проверку типов, мы разрываем связь между наследованием и полиморфизмом, однако, эта сила приходит с трудностью доказать, что тонкая проблема времени выполнения не возникнет.
Единственная оговорка к чистому полиморфизму заключается в том, что у нас могут появиться тонкие ошибки, которые сложно отследить и исправить. Чистый полиморфизм стоит искать только в том случае, если используемый вами язык может надежно генерировать исключение, когда метод не реализован.
Эффективные программисты ищут полиморфизм, а не сильное наследование. Преимущества чистого полиморфизма перевешивают любые преимущества, которые предоставляет проверка типов времени компиляции, особенно когда у нас есть доступ к очень сложным отладчикам и поддержка обработки исключений во время выполнения. В целом, я считаю, что преимущества чистого полиморфизма перевешивают ценность статической проверки типов.