OOP в университете и индустрии
Mar. 12th, 2013 10:02 pmУ факультета компьютерных наук в Carnegie-Mellon University очень высокая репутация, он известен как один из самых лучших в Америке - именно этот факультет, а не весь университет. Так вот, в нем, оказывется, решили не преподавать больше объектно-ориентированное программирование в вводных курсах. Те студенты, что захотят с ним познакомиться, смогут это сделать на факультативном предмете для второго курса:
Object-oriented programming is eliminated entirely from the introductory curriculum, because it is both anti-modular and anti-parallel by its very nature, and hence unsuitable for a modern CS curriculum. A proposed new course on object-oriented design methodology will be offered at the sophomore level for those students who wish to study this topic.
Этой новости уже два года - не знаю, как там у них с этим все вышло. В комментариях к этой блог-записи есть умеренно интересный спор о том, действительно ли ООП "anti-modular and anti-parallel by its very nature", в котором присутствует много академического жаргона.
Я не знаю, что думать конкретно об идее "не преподавать ООП на первом курсе CS". Профессор CMU утверждает, что одной из причин их решения было то, что они встретились с выпускниками CMU, работающими сейчас в Facebook, и те им сказали, что больше всего в университете им помог курс функционального программирования, так что они решили давать побольше этого. Проблемы с этим аргументом очевидны: FP необязательно должно целиком вытеснять OOP, а главное, эту информацию они получили от людей, которые будучи студентами как раз начинали с OOP-языка. Но с другой стороны, есть резон и в том, что факультет CS должен давать студентам теоретические начала, а также учить их думать и понимать, а не только обучать писать программы, и на престижном факультете, куда идут абитуриенты высокого уровня, может и лучше это делать с помощью императивных и функциональных языков, а OOP считать чем-то, что можно потом подучить.
Поскольку я вижу обе стороны этого спора, и не могу между ними решить (недостаточно данных), я напишу вместо этого о применении OOP в индустрии, которое хоть и повсеместно - это глупо отрицать, но довольно значительно изменилось за последние 20 лет. Мне кажется, что в этом - в том, как применять OOP - мы коллективно многому научились, хоть этот опыт плохо согласуется с "философией" OOP, скажем так.
Я думаю, что развитие OOP-языков и то, как они применяются на практике, особенно в больших проектах, показало за последние 20 лет следующую тенденцию. Главная польза OOP была и остается в том, как эта практика помогает модуляризации исходного кода. Под модуляризацией я понимаю в широком смысле все, что помогает программисту, с одной стороны, держать связанные друг с другом вещи вместе (как на экране, так и в уме), и с другой стороны, иметь дело с менее тесно связанными вещами раздельно. При этом "помогает" означает не только "делает возможным", но также - и даже более важно! - "делает обычным, очевидным, самым простым выбором".
Это может показаться на первый взгляд тривиальным утверждением, но не думаю, что оно совсем тривиально. Когда говорят о достоинствах OOP, упоминают всякие разные вещи. Например:
- моделирование проблемы или решения в виде иерархии объектов и их методов
- code reuse: использование уже написанного кода, который надо лишь немного изменить (или добавить в него), с помощью наследования
- энкапсуляция, скрытие внутренней имплементации от внешних пользователей
Я считаю, что эти вещи сами по себе важны намного меньше, чем то, насколько они помогают или мешают главной цели: модуляризации исходного кода. Именно это в конце концов решает то, стоит ими пользоваться или нет, и к этому в конечном итоге, путем проб и ошибок, приходит вся индустрия.
В свете этого утверждения давайте посмотрим на основные черты и особенности OOP, и я попробую проиллюстрировать вышесказанное на их примере.
1. Начнем с самого основного: соединения кода и данных в одном классе. Это оказалось невероятно полезной идеей, которая, возможно, отвечает за львиную долю всей пользы от OOP вообще. При этом согласно академическим или пуристическим понятиям это даже необязательно OOP вообще, но это неважно. Представьте себе C++ без виртуальных функций, наследования и полиморфизма, просто с классами, в которых есть данные и методы. Неважно, что специалист по теории языков программирования скажет, что это не OOP вообще, а тривиальный синтаксический сахар (а что-то типа CLOS в Лиспе, где нет четкой привязки методов к классам, наоборот, самый что ни на есть OOP). Даже этот тривиальный синтаксический сахар сам по себе уже дает огромную пользу в смысле модуляризации. Оказывается, огромное количество решений, которые приходится писать на практике, естественным образом выглядят так: небольшое количество связанных друг с другом данных, и функции для работы именно с этими данными. Понятие класса помогает держать эти связанные вещи вместе, а не связанные вещи - отдельно, в другом классе (и другом файле обычно).
Конечно, для этого не обязательно иметь OOP или понятие класса. Мне приходилось иметь дело с большими проектами, написанными на C; в них обычно оказывалось, что во многих исходных файлах определена одна-две небольших структуры, хранящие связанную друг с другом информацию, и рядом - функции для работы с этой структурой. Имена функций и структур подчеркивают их связь; часто складывается конвенция, что функции получают указатель на структуру первым аргументом. Все это работает, но чтобы это поддерживать, нужно стараться. Если не стараться (или если поручить дело слабенькому программисту), со временем функции для работы с этой структурой растекутся в другие файлы, имена будут не очень ясные, итд. Классы дают эту модуляризацию практически бесплатно и поддерживают ее тоже почти бесплатно. В одном этом уже - огромное их преимущество. Это также главная причина, по которой C++ победил C.
2. Наследование по интерфейсу помогает модуляризации. Определяется контракт, и он разделяет в пространстве (исходного кода) и уме (программиста) имплементацию и использование этого контракта. В больших проектах это незаменимо. Должен быть какой-то способ решить: я делаю это, а ты делаешь это, и мы потом вот по этой линии стыкуемся. Эта линия будет контрактом, как ее ни назови; но моделировать ее в виде интерфейса или абстрактного класса очень удобно (в динамических OOP-языках происходит то же самое, просто контракт задается неявно или в документации, а не в виде типа). Кстати, в небольших проектах это тоже необходимо, даже если есть только один программист: "я" и "ты" просто означает "я сегодня" и "я завтра". Должна быть возможность думать только о части системы.
Полиморфизм помогает модуляризации в той мере, в какой он обеспечивает удобную работу с контрактом, т.е. наследованием по интерфейсу.
3. Наследование кода (implementation inheritance) мешает модуляризации, и поэтому почти всегда оказывается плохой идеей. Я не могу окинуть взглядом одну штуку (класс) и понять ее поведение, мне нужно учитывать другие штуки (классы), которые написаны обычно в другом месте, в другом файле. Одного этого достаточно, чтобы навредить модуляризации. Этот вред обычно оказывается значительнее, чем польза от code reuse. Говоря в терминах Джавы, implements намного полезнее extends. Множественное наследование (multiple inheritance) в этом смысле еще хуже, и вреда от него - в реальном мире и с реальными программистами, а не в идеальном мире, населенном гуру - намного больше, чем пользы. Решение Джавы отказаться от множественного наследования кода вообще в этом смысле закономерно. Триумф STL, в которой практически нет наследования кода, над другими потенциальными стандартными библиотеками для C++ - закономерен.
Все это про интерфейсы, наследование итд. никто не понимал в начале 90-х, а сейчас понимают многие, а может, это уже и стало общим местом, не уверен. Так что прогресс налицо.
Object-oriented programming is eliminated entirely from the introductory curriculum, because it is both anti-modular and anti-parallel by its very nature, and hence unsuitable for a modern CS curriculum. A proposed new course on object-oriented design methodology will be offered at the sophomore level for those students who wish to study this topic.
Этой новости уже два года - не знаю, как там у них с этим все вышло. В комментариях к этой блог-записи есть умеренно интересный спор о том, действительно ли ООП "anti-modular and anti-parallel by its very nature", в котором присутствует много академического жаргона.
Я не знаю, что думать конкретно об идее "не преподавать ООП на первом курсе CS". Профессор CMU утверждает, что одной из причин их решения было то, что они встретились с выпускниками CMU, работающими сейчас в Facebook, и те им сказали, что больше всего в университете им помог курс функционального программирования, так что они решили давать побольше этого. Проблемы с этим аргументом очевидны: FP необязательно должно целиком вытеснять OOP, а главное, эту информацию они получили от людей, которые будучи студентами как раз начинали с OOP-языка. Но с другой стороны, есть резон и в том, что факультет CS должен давать студентам теоретические начала, а также учить их думать и понимать, а не только обучать писать программы, и на престижном факультете, куда идут абитуриенты высокого уровня, может и лучше это делать с помощью императивных и функциональных языков, а OOP считать чем-то, что можно потом подучить.
Поскольку я вижу обе стороны этого спора, и не могу между ними решить (недостаточно данных), я напишу вместо этого о применении OOP в индустрии, которое хоть и повсеместно - это глупо отрицать, но довольно значительно изменилось за последние 20 лет. Мне кажется, что в этом - в том, как применять OOP - мы коллективно многому научились, хоть этот опыт плохо согласуется с "философией" OOP, скажем так.
Я думаю, что развитие OOP-языков и то, как они применяются на практике, особенно в больших проектах, показало за последние 20 лет следующую тенденцию. Главная польза OOP была и остается в том, как эта практика помогает модуляризации исходного кода. Под модуляризацией я понимаю в широком смысле все, что помогает программисту, с одной стороны, держать связанные друг с другом вещи вместе (как на экране, так и в уме), и с другой стороны, иметь дело с менее тесно связанными вещами раздельно. При этом "помогает" означает не только "делает возможным", но также - и даже более важно! - "делает обычным, очевидным, самым простым выбором".
Это может показаться на первый взгляд тривиальным утверждением, но не думаю, что оно совсем тривиально. Когда говорят о достоинствах OOP, упоминают всякие разные вещи. Например:
- моделирование проблемы или решения в виде иерархии объектов и их методов
- code reuse: использование уже написанного кода, который надо лишь немного изменить (или добавить в него), с помощью наследования
- энкапсуляция, скрытие внутренней имплементации от внешних пользователей
Я считаю, что эти вещи сами по себе важны намного меньше, чем то, насколько они помогают или мешают главной цели: модуляризации исходного кода. Именно это в конце концов решает то, стоит ими пользоваться или нет, и к этому в конечном итоге, путем проб и ошибок, приходит вся индустрия.
В свете этого утверждения давайте посмотрим на основные черты и особенности OOP, и я попробую проиллюстрировать вышесказанное на их примере.
1. Начнем с самого основного: соединения кода и данных в одном классе. Это оказалось невероятно полезной идеей, которая, возможно, отвечает за львиную долю всей пользы от OOP вообще. При этом согласно академическим или пуристическим понятиям это даже необязательно OOP вообще, но это неважно. Представьте себе C++ без виртуальных функций, наследования и полиморфизма, просто с классами, в которых есть данные и методы. Неважно, что специалист по теории языков программирования скажет, что это не OOP вообще, а тривиальный синтаксический сахар (а что-то типа CLOS в Лиспе, где нет четкой привязки методов к классам, наоборот, самый что ни на есть OOP). Даже этот тривиальный синтаксический сахар сам по себе уже дает огромную пользу в смысле модуляризации. Оказывается, огромное количество решений, которые приходится писать на практике, естественным образом выглядят так: небольшое количество связанных друг с другом данных, и функции для работы именно с этими данными. Понятие класса помогает держать эти связанные вещи вместе, а не связанные вещи - отдельно, в другом классе (и другом файле обычно).
Конечно, для этого не обязательно иметь OOP или понятие класса. Мне приходилось иметь дело с большими проектами, написанными на C; в них обычно оказывалось, что во многих исходных файлах определена одна-две небольших структуры, хранящие связанную друг с другом информацию, и рядом - функции для работы с этой структурой. Имена функций и структур подчеркивают их связь; часто складывается конвенция, что функции получают указатель на структуру первым аргументом. Все это работает, но чтобы это поддерживать, нужно стараться. Если не стараться (или если поручить дело слабенькому программисту), со временем функции для работы с этой структурой растекутся в другие файлы, имена будут не очень ясные, итд. Классы дают эту модуляризацию практически бесплатно и поддерживают ее тоже почти бесплатно. В одном этом уже - огромное их преимущество. Это также главная причина, по которой C++ победил C.
2. Наследование по интерфейсу помогает модуляризации. Определяется контракт, и он разделяет в пространстве (исходного кода) и уме (программиста) имплементацию и использование этого контракта. В больших проектах это незаменимо. Должен быть какой-то способ решить: я делаю это, а ты делаешь это, и мы потом вот по этой линии стыкуемся. Эта линия будет контрактом, как ее ни назови; но моделировать ее в виде интерфейса или абстрактного класса очень удобно (в динамических OOP-языках происходит то же самое, просто контракт задается неявно или в документации, а не в виде типа). Кстати, в небольших проектах это тоже необходимо, даже если есть только один программист: "я" и "ты" просто означает "я сегодня" и "я завтра". Должна быть возможность думать только о части системы.
Полиморфизм помогает модуляризации в той мере, в какой он обеспечивает удобную работу с контрактом, т.е. наследованием по интерфейсу.
3. Наследование кода (implementation inheritance) мешает модуляризации, и поэтому почти всегда оказывается плохой идеей. Я не могу окинуть взглядом одну штуку (класс) и понять ее поведение, мне нужно учитывать другие штуки (классы), которые написаны обычно в другом месте, в другом файле. Одного этого достаточно, чтобы навредить модуляризации. Этот вред обычно оказывается значительнее, чем польза от code reuse. Говоря в терминах Джавы, implements намного полезнее extends. Множественное наследование (multiple inheritance) в этом смысле еще хуже, и вреда от него - в реальном мире и с реальными программистами, а не в идеальном мире, населенном гуру - намного больше, чем пользы. Решение Джавы отказаться от множественного наследования кода вообще в этом смысле закономерно. Триумф STL, в которой практически нет наследования кода, над другими потенциальными стандартными библиотеками для C++ - закономерен.
Все это про интерфейсы, наследование итд. никто не понимал в начале 90-х, а сейчас понимают многие, а может, это уже и стало общим местом, не уверен. Так что прогресс налицо.
no subject
Date: 2013-03-12 08:07 pm (UTC)no subject
Date: 2013-03-12 08:11 pm (UTC)no subject
Date: 2013-03-12 08:17 pm (UTC)> Неважно, что специалист по теории языков программирования скажет, что это не OOP вообще, а тривиальный синтаксический сахар
да, сахар и не требует концепции класса или объекта. ср. перловое
{
my $var;
sub1 {};
sub2 {};
}
вполне изолированный набор из данных и кода, вдобавок данных скрытых от внешнего мира.
no subject
Date: 2013-03-12 08:25 pm (UTC)Например: у всех окошек есть более-менее одинаковое оформление, функции перемещения и изменения размера, и т.д. и т.п., и у каждого - своя "бизнес-логика".
Сделать MyWindow implements IWindow и определить все методы этого IWindow, пускай даже как вызовы соответствующих системных функций-заготовок - поистине издевательство.
Для заимствования не так уж много хороших средств.
Это или миксины, или наследование.
Миксины заморачивают, а наследование развращает. Выбирайте! %)
no subject
Date: 2013-03-12 08:27 pm (UTC)no subject
Date: 2013-03-12 08:28 pm (UTC)no subject
Date: 2013-03-12 08:30 pm (UTC)no subject
Date: 2013-03-12 08:34 pm (UTC)no subject
Date: 2013-03-12 08:34 pm (UTC)А вот функциональное программирование очень хорошо учит именно программировать и думать. Поэтому не случайно, что классический MIT-овский курс по программированию был функциональным (http://mitpress.mit.edu/sicp/)
P.S. А к Carnegie-Mellon у меня сложное отношение. В Москве преподаватели оттуда читают иногда треннинги, и я был на нескольких. Правильные, хорошие. Но по большей части бесполезные в практической деятельности. Теоретики.
no subject
Date: 2013-03-12 08:34 pm (UTC)Полиморфизм - это сразу три контракта: объекта перед клиентским кодом, предка перед наследниками (которые будут реюзать его) и наследников перед предком (ибо тот может вызывать виртуальные функции, определённые в наследниках).
Наследование - это, с одной стороны, субклассирование, то есть конкретизация и зауживание (в полном соответствии с принципом Лисков), а с другой стороны, расширение.
Наследник может сломать контракт предка, если известно, что ни при каких условиях он не будет эксплуатироваться как предок.
Именно это место и иллюстрируется срачем - прямоугольник это квадрат, или квадрат это прямоугольник?
В наивном ООП эти вещи свалены в кучу.
no subject
Date: 2013-03-12 08:45 pm (UTC)no subject
Date: 2013-03-12 08:48 pm (UTC)no subject
Date: 2013-03-12 08:50 pm (UTC)no subject
Date: 2013-03-12 08:54 pm (UTC)И таки я абсолютно согласен -- ООП failed как его подавали вначале, и succeeded (практически) в той части где оно просто добавляет дополнительные scopes.
2 cents
Date: 2013-03-12 08:55 pm (UTC)2. Если не увлекаться хиерархиями - использование platform-independent base classes + platform-dependent derived ones, позволяет software пережить несколько поколений hardware.
3. Гарантированное выполнение деструктора (c++) очень очень большое дело для тех кто понимает как этим пользоваться.
Главная проблема - даже минимально приемлемый уровень реального знакомаства с тем же с++ не под силу примерно половине С программистов, что ставит практический крест на применении в больших организациях.
no subject
Date: 2013-03-12 09:09 pm (UTC)Использование наследования обычно заканчивается на примерах из обучающей литературы.
Редкое исключение - инфраструктура, например Socket->TcpSocket->AsyncTcpSocket.
Множественное наследование - вообще ад.
А интерфейсы - как раз весьма полезная вещь. В особенности для выработки дисциплины чёрного ящика.
no subject
Date: 2013-03-12 09:13 pm (UTC)no subject
Date: 2013-03-12 09:17 pm (UTC)раба на цепи.
no subject
Date: 2013-03-12 09:19 pm (UTC)У меня нет специального образования. Я не знаю четыре главных слова и т.д. Программировать я учился на Паскале, и до ООП мы не дошли. Потом в аспирантуре я писал в основном рассчетные программки на Питоне. Сейчас работаю фрилансером, конкректно сейчас перевожу разработанные математические модели на Питон, так чтобы они быстро и параллельно считались.
Про модуляризацию. В Питоне есть модули, одна из основных "ошибок" джавистов на питоне - создавать модуль а потом в нем одноименный класс.
По поводу интерфейсов, возражение примерно такое же как и при холиваре cтатическая / динамическая типизация. Используя юнит тесты можно проверять соответсвие протоколу в критичных местах, и все. Поддерживать протокол везде - "неэкономично"
Особенность моей работы (у меня фактически нет начальников-программистов и большая часть кода уходит в "архив") позволяет мне творить некую функциональщину.
Которая просто получается короче и понятнее.
Изредка у меня встречаются dict с функциями, этакие легковесные объекты, и под одними и теми же ключами у меня иногда оказываются совершенно разные функции, когда я думаю, о том коде которы бы мне понадобился в ООП чтобы этого добиться, я собственно даже плохо представляю ООП структуру которая бы позволяла иметь все комбинации из 4 функций у каждой из которых есть два варианта и они нигде не близко к
{"f1":f1v1,"f2":f2v1,"f3":f3v2,"f4":f4v1}
а то что они стоят на своих местах проверяется юниттестами тривиально.
Своих племянников я ООП учить не буду, до последней возможности.
no subject
Date: 2013-03-12 09:22 pm (UTC)Забудем про iostream, посмотрим на реализацию контейнеров в STL.
Там наследование для code reuse в полный рост, что бы четыре раза не реализовывать set, map, multiset, multimap. Не знаю почему именно через наследование.
Заглянул в lib/gcc/i686-pc/4.5.3/include/c++/unordered_set
unordered_set унаследован от __unordered_set,
__unordered_set от _Hashtable,
_Hashtable множественно унаследован аж от трёх классов _Rehash_base, _Map_base, _Equality_base
Т.е. унаследовать автомобиль от рулевого колеса и багажника в STL - это самое обычное дело
no subject
Date: 2013-03-12 09:24 pm (UTC)По поводу параллелизма -- я на нынешнем проекте эксперементирую с immutable objects там, где есть concurrency. Очень непривычно и все время кажется, что все очень неэффективно, но на самом деле багов меньше и думать легче. У знакомых похожий опыт.
Пытаюсь отучить себя от использования inheritance как оружие борьбы с copy/paste. С этим пока успехов меньше, но очевидно, что нужно.
no subject
Date: 2013-03-12 09:25 pm (UTC)no subject
Date: 2013-03-12 09:30 pm (UTC)http://en.wikipedia.org/wiki/Arrow_(computer_science)
no subject
Date: 2013-03-12 09:36 pm (UTC)> но чтобы это поддерживать, нужно стараться.
Опыт показывает что если не стараться, то в С++ получается не менее худший код, просто несколько в другом направлении. Например, часта излишняя любовь к громоздким классам, где все поведение описывается в виде методов, вместо свободных функций, что уменьшает связывание и улучшаю инкапсуляцию. Кстати, такие функции нечлены тоже являются интерфейсом класса.
stl мало использует динамическое наследование (вроде только fstream, вдобавок множественное :), но широко использует статическое, для чего шаблоны и были созданы.
наследование кода действительно может быть проблемой, но в таких случаях рекомендуется использовать закрытое наследование, а еще лучше паттерн pImpl...
no subject
Date: 2013-03-12 09:39 pm (UTC)