Thursday, August 16, 2007

Подтипы и наследование

Наследование — это довольно сложный механизм, который с понятием подтипа очень сильно связан, но в то же время вносит довольно много путаницы и ошибок.

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

Обычно есть несколько видов наследования. Первый — самый понятный и полезный — это наследование интерфейса. Он есть во все ОО-языках.

Нужен он, как известно, для очень полезных вещей — обеспечение слабого связывания через полиморфизм. Или, если проще, для того, чтобы некий код мог пользовать другой код через интерфейс, не зная деталей реализации.

Другими словами, конкретный класс Cow, реализующий интерфейс IAnimal, должен иметь соответствующий подтип Cow <: IAnimal. То есть везде, где используется IAnimal можно впихнуть Cow и все будет все ок. По сути, так оно и работает.

 

Но ситуация заметно усложняется, если наследование происходит не от абстрактного интерфейса, а от другой реализации. В C++ это public наследование, в Java — кейворд extends.

Жизнь усложняется из-за двух основных обстоятельств: Во-первых, поскольку в классах обычно есть какое-то состояние, две эти реализации это состояние по разному дергают. А во-вторых, в большинстве языков есть такая фича: пусть функция sayMoo() в Cow вызывает Cow::open(), и если open() в наследнике переопределено, а sayMoo() — нет, то sayMoo() будет вызывать переопределенную версию open(). Это называется "открытой рекурсией" (open recursion).

Из-за этих сложностей нередко появляются неприятные баги, и, чтобы постороннему человеку понять как работает WhiteCow, нужно смотреть заодно всю иерархию доверху — таким образом, с инкапсуляцией и слабым связыванием получается нехорошо.

Чтобы избежать всяких отвратительных ошибок, был изобретен всем известный Liskov Substitution Principle — по которому подкласс должен себя вести в точности как родитель в отношении ряда заранее определенных инвариантов, чтобы его можно было считать подтипом родителя. К сожалению, соблюдение этого принципа ложится на плечи программиста, а дело это не очень тривиальное.

Чистое же наследование реализации (private inheritance в C++) страдает похожими недостатками. Только подкласс при таком наследовании уже подтипом родителя не является совсем.

Ссылки: