Проблема с ООП заключается в том, что этим термином сейчас называют все что угодно. Привязал методы к хэшу в Perl — ООП. Наплодил в Erlang процессов, которые обмениваются сообщениями — ООП. Объявил пару-тройку функций для работы с какой-то структурой, и снова ООП. Никто уже толком не понимает, что именно сей термин означает, но все с умным видом его произносят. Стыдно же не знать!

Дополнение: Кстати, если кого-то интересует вопрос «объявления пары-тройки функций для работы с какой-то структурой», есть такая книжка Object-oriented programming With ANSI C [PDF] .

Лично я уже перестал понимать, что такое ООП и вообще стараюсь выкинуть этот термин из своего лексикона. Во-первых, потому что я не понимаю, о чем идет речь. Во-вторых, потому что мой собеседник тоже не понимает, и мы оба не понимаем о чем-то своем. Наконец, в-третьих, никому не нужно ООП, что бы оно ни значило. Все, что вам нужно — это алгебраические типы данных (далее АТД) и тайпклассы.

Примечание: АТД также может означать абстрактный тип данных. Это тоже очень важный и полезный АТД, но другой. Постарайтесь не запутаться!

С обоими понятиями мы хорошо знакомы из опыта программирования на Haskell .

Пример АТД на этом замечательном языке:

data Maybe a = Just a | Nothing

То же самое на Scala выражается несколько более многословно:

sealed trait MyMaybe [ +A ]
case class MyJust [ A ] ( a : A ) extends MyMaybe [ A ]
case object MyNothing extends MyMaybe [ Nothing ]

Тайпклассы, они же классы типов, они же трейты — это в сущности интерфейсы с возможностью задавать дэфолтную реализацию некоторых методов:

trait Animal {
def word : String
def talk ( ) { println ( this . word ) }
}

Тот же код на Haskell:

class Animal a where
word :: a -> String

talk :: a -> IO ( )
talk = putStrLn . word

Наши АТД могут реализовать эти интерфейсы:

class Cat extends Animal { def word = «Meow!» }

class Dog extends Animal { def word = «Woof!» }

// …

val dog = new Dog ( )
dog. talk

То же самое на Haskell:

data Cat = Cat
data Dog = Dog

instance Animal Cat where word _ = «Meow!»
instance Animal Dog where word _ = «Woof!»

Притом один АТД может реализовать один и тот же интерфейс совершенно разными способами:

class JustACat { def meow = «Meow!» }

class HappyCat extends JustACat with Animal {
def word = meow + » :)»
}

class SadCat extends JustACat with Animal {
def word = meow + » :(»
}

Что на Haskell переводится так:

data JustACat = JustACat

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 могут быть объявлены в двух совершенно разных библиотеках, написанных разными программистами, а потом объединены каким-то третьим программистом, реализовавшим экземпляр класса типов.

Ну хорошо, скажете вы. А как же, например, без ООП сделать исключения? Их тоже легко получить при помощи трейтов:

trait MyException

case class MyRuntimeException ( s : String ) extends MyException

val err : MyException = MyRuntimeException ( «Error!» )

err match {
case _ : MyRuntimeException => println ( «Catched!» )
}

В Haskell, по сути, так и делается . И не нужна программистам эта развесистая иерархия исключений в стиле Java. Однако на практике в языке Scala все-таки приходится вписываться в уже существующую иерархию:

case class MyExt ( s : String ) extends Exception

// …

try {
throw MyExt ( «ololo!» )
} catch {
case err : MyExt => println ( err. s )
}

Вот по большому счету и все, что нужно знать об АТД и классах типов. Если вы загляните на Hackage , то обнаружите, что с их помощью на практике можно реализовать что угодно. А зачем нам дополнительная сложность? Фактически, мы можем избавиться от иерархии классов (интерфейсы не считаются). В этом случае нам не нужно никакой там ковариации и контрвариации, восходящего и нисходящего преобразования, абстрактных классов, protected методов и так далее (за редкими исключениями, в основном связанными с наследием Java, см определение MyMaybe выше). В общем, все плоско, как в Haskell.

Недаром даже в книжках по Java в последние годы рекомендуется использовать делегирование вместо наследования, а если вы и вынуждены использовать наследование, то не использовать более двух уровней иерархии. Недаром в современных языках программирования, взять хотя бы Rust и Go , нет никакого наследования. Потому что наследование нужно только для кривой эмуляции АТД и трейтов. Во всех остальных случаях оно создает только лишнюю сложность и приводит к созданию запутанного, очень сложного в поддержке кода. А теперь попробуйте угадать, что мы получим, если оставить в ООП только инкапсуляцию и полиморфизм.

Короче говоря, если в своем коде вы пишите protected или abstract class , скорее всего , вы делаете что-то ну очень не так.

Забудьте об ООП. Используйте АТД и тайпклассы.

EnglishRussianUkrainian