Проблема с ООП заключается в том, что этим термином сейчас называют все что угодно. Привязал методы к хэшу в Perl — ООП. Наплодил в Erlang процессов, которые обмениваются сообщениями — ООП. Объявил пару-тройку функций для работы с какой-то структурой, и снова ООП. Никто уже толком не понимает, что именно сей термин означает, но все с умным видом его произносят. Стыдно же не знать!
Дополнение: Кстати, если кого-то интересует вопрос «объявления пары-тройки функций для работы с какой-то структурой», есть такая книжка Object-oriented programming With ANSI C [PDF] .
Лично я уже перестал понимать, что такое ООП и вообще стараюсь выкинуть этот термин из своего лексикона. Во-первых, потому что я не понимаю, о чем идет речь. Во-вторых, потому что мой собеседник тоже не понимает, и мы оба не понимаем о чем-то своем. Наконец, в-третьих, никому не нужно ООП, что бы оно ни значило. Все, что вам нужно — это алгебраические типы данных (далее АТД) и тайпклассы.
Примечание: АТД также может означать абстрактный тип данных. Это тоже очень важный и полезный АТД, но другой. Постарайтесь не запутаться!
С обоими понятиями мы хорошо знакомы из опыта программирования на Haskell .
Пример АТД на этом замечательном языке:
То же самое на Scala выражается несколько более многословно:
case class MyJust [ A ] ( a : A ) extends MyMaybe [ A ]
case object MyNothing extends MyMaybe [ Nothing ]
Тайпклассы, они же классы типов, они же трейты — это в сущности интерфейсы с возможностью задавать дэфолтную реализацию некоторых методов:
def word : String
def talk ( ) { println ( this . word ) }
}
Тот же код на Haskell:
word :: a -> String
talk :: a -> IO ( )
talk = putStrLn . word
Наши АТД могут реализовать эти интерфейсы:
class Dog extends Animal { def word = «Woof!» }
// …
val dog = new Dog ( )
dog. talk
То же самое на Haskell:
data Dog = Dog
instance Animal Cat where word _ = «Meow!»
instance Animal Dog where word _ = «Woof!»
Притом один АТД может реализовать один и тот же интерфейс совершенно разными способами:
class HappyCat extends JustACat with Animal {
def word = meow + » :)»
}
class SadCat extends JustACat with Animal {
def word = meow + » :(»
}
Что на Haskell переводится так:
meow JustACat = «Meow!»
newtype HappyCat = HappyCat JustACat
newtype SadCat = SadCat JustACat
instance Animal HappyCat where word ( HappyCat x ) = meow x ++ » :)»
instance Animal SadCat where word ( SadCat x ) = meow x ++ » :(»
Что интересно, тип JustACat и интерфейс Animal могут быть объявлены в двух совершенно разных библиотеках, написанных разными программистами, а потом объединены каким-то третьим программистом, реализовавшим экземпляр класса типов.
Ну хорошо, скажете вы. А как же, например, без ООП сделать исключения? Их тоже легко получить при помощи трейтов:
case class MyRuntimeException ( s : String ) extends MyException
val err : MyException = MyRuntimeException ( «Error!» )
err match {
case _ : MyRuntimeException => println ( «Catched!» )
}
В Haskell, по сути, так и делается . И не нужна программистам эта развесистая иерархия исключений в стиле Java. Однако на практике в языке Scala все-таки приходится вписываться в уже существующую иерархию:
// …
try {
throw MyExt ( «ololo!» )
} catch {
case err : MyExt => println ( err. s )
}
Вот по большому счету и все, что нужно знать об АТД и классах типов. Если вы загляните на Hackage , то обнаружите, что с их помощью на практике можно реализовать что угодно. А зачем нам дополнительная сложность? Фактически, мы можем избавиться от иерархии классов (интерфейсы не считаются). В этом случае нам не нужно никакой там ковариации и контрвариации, восходящего и нисходящего преобразования, абстрактных классов, protected методов и так далее (за редкими исключениями, в основном связанными с наследием Java, см определение MyMaybe выше). В общем, все плоско, как в Haskell.
Недаром даже в книжках по Java в последние годы рекомендуется использовать делегирование вместо наследования, а если вы и вынуждены использовать наследование, то не использовать более двух уровней иерархии. Недаром в современных языках программирования, взять хотя бы Rust и Go , нет никакого наследования. Потому что наследование нужно только для кривой эмуляции АТД и трейтов. Во всех остальных случаях оно создает только лишнюю сложность и приводит к созданию запутанного, очень сложного в поддержке кода. А теперь попробуйте угадать, что мы получим, если оставить в ООП только инкапсуляцию и полиморфизм.
Короче говоря, если в своем коде вы пишите protected
или abstract class
, скорее всего , вы делаете что-то ну очень не так.
Забудьте об ООП. Используйте АТД и тайпклассы.