Неформальное введение в объектно-ориентированное программирование на платформе .NET Framework 2

Неформальное введение в объектно-ориентированное программирование на платформе .NET Framework

Продолжение. Начало в КГ №13.

В качестве эпиграфа...
German>
Также, на мой взгляд, у всех авторов есть еще одна неочевидная ошибка. Все они пишут на объектно-ориентированном языке так, как будто это обычный необъектный JavaScript с некими встроенными функциями, вызываемыми через точку. А где собственные классы? Ведь они сильно упрощают последующую модификацию кода, отчуждения его другим разработчикам и повторное его использование... Задача позволяет разбить себя на отдельные классы. Ведь у нас есть получение данных от пользователя, их обработка и вывод на экран...

Xxxx>
В корне не согласен с Германом Ивановым! Задание было простое и очевидное — написать небольшую (ключевое слово) программу вывода названия месяца по его номеру. В планы не входила массированная обработка исключений, проектирование классов и прочая "тяжелая" артиллерия (типа всяких Parse() — Шмарсе())...

Yyyy>
Хотелось бы отметить код Германа Иванова... написано красиво. Но, как мне кажется, усложнять простую задачу ни к чему. Исключения, понятное дело, ловить надо. Такой подход к планированию проекта — это дело отличное, при том, что проект большой. Но когда просят написать 10 строчек, а получается гораздо больше, по-моему, это чересчур.
(с) Переписка в форуме мастер-класса "Введение в C#"

Предисловие
Прежде, чем мы перейдем к рассмотрению конкретных языков программирования, имеющихся на платформе .Net Framework, мне хотелось бы поговорить с вами о вопросах общего подхода к созданию программ. Очень многие программисты уделяют мало внимания проектированию алгоритма своего будущего приложения. Делают они это совершенно напрасно. Им, по всей видимости, кажется, что достаточно накидать компоненты на форму, написать обработчики событий нажатий на кнопки — и работа над приложением закончена. Мне доводилось видеть довольно много подобных, на первый взгляд, C++ (Delphi) программ, которые скорее можно было бы назвать "C-программы с использованием чужих объектов".
Прежде чем садиться писать код, необходимо четко представлять, что именно вы программируете. И как только вы зададитесь целью алгоритмически описать решаемую задачу, как сразу же наткнетесь на тот факт, что привычный многим "линейный" подход к программированию полностью неэффективен для создания даже элементарных по своей сути программ.
Сравнению классического "линейного" способа решения задачи и решения ее с применением объектно-ориентированного программирования и посвящаются следующие две мои статьи.
Раз уж вы собрались изучать новую для себя платформу программирования и новые языки, то попробуйте сделать над собой еще одно небольшое усилие и изучите также и новый подход к самой идее создания программ. Поверьте: время, потраченное сейчас на голую теорию, с лихвой вам окупится тогда, когда вы начнете писать реальный код. Причем заняться этим вам стоит именно сейчас, перед тем, как мы с вами приступим к изучению того же С#.

Как не следует писать программы, или недостатки линейного подхода к программированию
Любые языки программирования — как современные, так и "мертвые" — всегда оперируют двумя сущностями. Первой из них являются данные, которые вам требуется обработать. Вторая сущность — это функции, обрабатывающие эти данные. К примеру, когда вы на своей кухне приготавливаете чашку кофе, ваши действия легко можно выразить в двух этих терминах программирования.
1. Вы берете (функция) зерна кофе (данные).
2. Засыпаете (функция) зерна (данные) в кофемолку (данные?).
3. Включаете (функция) кофемолку (данные?).
4. Ждете (функция), пока зерна (данные) измельчатся (функция).
5. Кипятите (функция) воду (данные) в турке (данные?).
6. Засыпаете (функция) измельченный(?) кофе (данные) в турку (данные?).
7. Ставите (функция) турку (данные?) на плиту (данные?).
8. Ждете (функция), пока кофе (данные) закипит (функция?).
9. Выключаете (функция) плиту (данные?).
Накидав подобный алгоритм действий, большинство программистов успокаивается и приступает к кодированию. На мой взгляд, подобный подход к программированию в корне неверен, а разделение текста программы на данные и методы по их обработке имеет массу неочевидных отрицательных моментов. Давайте рассмотрим наиболее важные из них.

Использование данных и методов не по назначению
Момент первый и основной. При такой системе программирования неявно допускается возможность использования методов, исходно рассчитанных на обработку одного вида данных, для обработки любого другого, возможно, совершенно неподходящего для этой обработки, вида данных.
Говоря более простым языком, любой человек знает о том, что не следует пытаться измельчать турку в кофемолке и довольно опасно ставить турку на плиту, не налив в нее предварительно воду. Для человека это достаточно самоочевидно, а вот с компьютерной программой дело обстоит намного сложнее. Кто-то из отцов информатики в свое время заметил, что компьютер ему все больше и больше напоминает полного идиота, обладающего феноменальной способностью к счету. Если вы поручите компьютерной программе "ставить кофемолку на включенную плиту", она будет с завидной педантичностью это делать невзирая на бессмысленность, а порой и опасность, подобного занятия. В своей "послушности" воле программиста компьютерная программа даст сто очков вперед любой, даже очень хорошо дрессированной собаке. Для того чтобы заставить программу "лезть на стену", вам нужно лишь выучить соответствующий набор команд, являющихся эквивалентом собачьих "Фас" и "Апорт".
Вы наверняка скажете: ладно, программа "дура", но так я же "молодец"! Я всегда смогу объяснить своей программе, что не следует пытаться заливать кипящую воду в кофемолку! Ой, не зарекайтесь! Обратите внимание: я взял для примера совершенно банальный простой алгоритм, и мы с вами уже нащупали кучу способов неправильного использования входящих в него сущностей. Для того чтобы предусмотреть их все, вам необходимо проверять все возможные варианты неправильного использования данных и методов в вашей программе. Это очень большая работа, объем которой растет в геометрической прогрессии по мере ввода в вашу программу новых и новых сущностей.

Помимо вышесказанного, совершенно наивным будет предполагать то, что конечный пользователь станет использовать вашу программу только так, как вы описали в прилагаемой к ней инструкции. К примеру, авторы старенькой ДОСовской игрушки Alone in Dark предполагали, что игрок будет стрелять в зомби из ружья, а при окончании в нем патронов — плакать и искать новые патроны. Мы же с одним моим приятелем вскоре выяснили, что любой предмет, будучи брошен в противника, наносит ему весьма ощутимые повреждения. Наше поле битвы после этого открытия приняло откровенно сюрреалистический вид. На появляющегося из-за угла зомби обрушивался форменный шквал предметов, которые герой игрушки носил в своих карманах. В их число входили носовые платки, спички, свечки, книжки, бутылки с водой, ключи, скрепки, дохлые крысы, ружье без патронов, патроны к пистолету и прочий хлам, который обречен носить с собой любой персонаж игры в стиле RPG.
Еле живой противник на подгибающихся ногах подходил к нам и после финального удара граммофоном по голове испускал дух. Граммофон был моим самым любимым оружием, с его помощью идиотизм происходящего поднимался до недосягаемой ранее высоты. Хотел бы я увидеть лица разработчиков игры в тот момент, когда бы они узнали о таком использовании их игрового алгоритма.
Вы, улыбнувшись, скажете: ну и что в этом страшного? И вам весело, и разработчикам хорошо! Приведу другой, значительно менее забавный пример. Всем хорошо известное "переполнение буфера" происходит лишь потому, что программист неявно предполагает, что пользователь на вход его кода подает определенный вид данных, обладающий при этом определенным размером. О последствиях такой самонадеянности программистов вы, думаю, неоднократно читали в рассылках, посвященных проблемам компьютерной безопасности. Так что не все тут так весело и забавно.

Плохая наглядность исходного алгоритма в тексте программы
Вторым крупным недостатком линейной идеологии программирования является то, что описанный в этих терминах алгоритм недостаточно прозрачен как для самого программиста, так и для его коллег, читающих созданный им текст программы.
Я не зря расставил знаки вопроса после определения "турки", "плиты" и "кофемолки" как "данных". Действительно, они, строго говоря, вовсе не являются данными. Мы ведь не обрабатываем их с помощью методов. Но и методом они также не являются, так как в нашем алгоритме они не совершают никаких действий. Получается, что они — это некая совершенно непонятная сущность: ни рыба тебе и ни мясо. Что такое "измельченный", в терминах линейного программирования вообще не объяснить. Разве что сплавить понятие "измельченный" вместе с понятием "кофе" в единое целое. Но при такой системе понятий как нам отличить уже размельченный кофе от того, которому операция помола еще предстоит впереди? А нам это необходимо знать — хотя бы для того, чтобы не сварить ненароком кофейные зерна или не молоть кофе, который исходно уже и так помолот.

Если подобных "непонятностей" в коде много, то по мере усложнения вашего проекта и вы сами как программист, и читающие код ваши коллеги вскоре полностью потеряете смысл происходящего в программе. Для того же, чтобы понять закодированный таким образом чужой алгоритм, нужно обладать совершенно недюжинным интеллектом и знанием привычек конкретного программиста.
Помимо "непонятных" сущностей на манер "турки", у нас в алгоритме присутствует еще один мистический объект. Это тот самый "бог из машины", который засыпает "кофе" в "кофемолку" и ставит "турку" на "газовую плиту". Называется этот объект — основной поток вычислений. В случае использования линейного подхода к программированию основной поток имеет дело с подавляющим большинством методов программы. Для того чтобы их вызывать, он должен быть хорошо осведомлен о том, что делают эти методы, какие параметры им требуются и что они возвращают обратно. Поэтому модификация свойств любого метода автоматически заставляет программиста модифицировать и сам основной поток. Модификация же основного потока легко может привести к необходимости модификации уже других методов, которые, в свою очередь, опять-таки требуют модификации основного потока.

Процесс подобных изменений нарастает лавинообразно и быстро становится неуправляемым, запутывая нам весь алгоритм. Так как без основного потока нам никак не обойтись в любой программе, нам следует минимизировать по возможности его воздействие на то, что происходит в программе. Это очень трудно сделать при обсуждаемой идеологии программирования.
Программы, написанные в линейном стиле, обычно содержат очень большое количество разных функций и структур данных. Из общей психологии мы знаем, что средний человек в состоянии одновременно удерживать в своей голове лишь несколько разных объектов одновременно, отслеживая связи между ними (порядка 7-10 штук). Программа по мере своего роста очень быстро начинает становиться непонятной своему автору из-за описанного обилия своих составляющих. Программисты борются с этой проблемой по-разному.
Чаще всего большие программы режутся ими на куски, называемые модулями. Поддержка модульности в той или иной степени присутствует во всех широко распространенных языках программирования. На первый взгляд, она, до поры до времени, избавляет от обилия объектов в программе.
Спросите, почему до поры до времени? А потому, что, во-первых, программист теряет восприятие программы как "единого целого", а во-вторых — через некоторое время количество модулей опять-таки перерастает магическое число 7-10, и программист начинает теряться уже в самих модулях, попав в ту же самую ловушку.

Спросите, а почему на первый взгляд? А потому, что модульность кода, как ни странно, сама по себе является генератором дополнительного количества сущностей. Связано это в основном с тем, что программист обычно вводит дополнительные интерфейсные элементы кода, служащие исключительно для связи данного модуля с остальными фрагментами программы. Их использование довольно удобно, и многие программисты попадаются на эту удочку. В результате вместо того, чтобы упрощать программу, модули, напротив, ее только усложняют.
Другим популярным способом борьбы с большим количеством сущностей в программе является "укрупнение" уже имеющихся методов. Происходит это обычно совершенно незаметно для самого программиста. Вместо того чтобы описывать отдельные методы, скажем, для "варки кофе" или его "помола", сочиняется метод под названием "помололи кофе, поставили на газ и сварили".
Недостаток такого подхода самоочевиден и будет подробно рассмотрен в следующем разделе. Вкратце: когда нам в дальнейшем потребуется выделить функциональность, скажем, "варки кофе", мы упаримся вырезать нужный код из этого всеобъемлющего метода. Причем, если у нас это получится, в программе появятся две процедуры, делающие примерно одно и то же. Это чревато в будущем опять-таки большими неприятностями.
Предположим, мы решили добавить в метод "варки кофе" функциональность, проверяющую, не выкипел ли уже наш кофе. Мы находим метод, отвечающий за варку, правим его и совершенно забываем о дублирующем куске кода в методе "помололи кофе, поставили на газ и сварили"!
Это приведет к тому, что кофе будет у нас периодически убегать, а мы, просматривая код метода варки и видя в нем наличие проверки, будем привычно вешать дохлых собак на "кривой" Windows и многострадального Билла Гейтса.
Подобное "укрупнение методов" — очень серьезная идеологическая ошибка, которой следует всячески избегать. Причем уход от проблемы путем вызова вновь созданного нами метода варки кофе из внутренностей исходного "помололи кофе, поставили на газ и сварили" также не есть пример хорошего стиля программирования.
Действительно, теперь мы избежали дублирования кода, но при такой методе мы опять-таки теряем прозрачность самого алгоритма. Становится совершенно непонятно, почему код варки кофе находится в отдельном методе, а все остальное объединено в единое целое.

Высокая сложность модификации имеющегося кода
Третий крупный недостаток линейного программирования — это повышенная сложность модификации уже имеющегося кода.
Предположим, мы с вами написали и ввели в эксплуатацию программу, описывающую алгоритм приготовления кофе. По прошествии некоторого времени заказчик приобрел кофеварку и теперь желает, чтобы мы с вами модифицировали свою программу так, чтобы она работала в этих изменившихся условиях. Тут и начинается у нас как разработчика жаркая пора.
Необходимо исключить из программы все блоки кода, отвечающие за обработку турки и газовой плиты. Вместо них необходимо добавить новые блоки, отвечающие за свежеприобретенную кофеварку. Отыскать в тексте программы все случаи использования этих сущностей довольно сложно. Кроме этого, мы также теряем возможность пользоваться уже наработанными ранее алгоритмами приготовления кофе. Хотя бы потому, что процесс варки кофе в кофеварке существенно отличен от процесса варки кофе на плите. Нам, по сути, приходится заново перелопачивать большинство имеющихся у нас методов.
Раньше мы предварительно кипятили воду, теперь этого делать не надо. Раньше мы засыпали измельченный кофе в турку, а теперь засыпаем кофе в кофеварку. Раньше мы ставили кофе на плиту, теперь нам нужно нажать тумблер и включить кофеварку. Раньше мы ждали, пока кофе закипит, теперь этого делать не нужно, так как кофеварка выключится сама. Раньше мы выключали плиту, теперь выключаем кофеварку.
В той или иной степени оказались измененными все блоки программы. Фактически мы не столько модифицируем код, сколько заново пишем новую программу.

Но и это еще не все! В процесс массовой переделки включаются и те куски кода, которые, на первый взгляд, и не должны были измениться. Возьмем для примера блок кода, отвечающий за помол кофе. На первый взгляд, ничего не изменилось: мы и раньше мололи кофе и сейчас его мелем той же самой кофемолкой. Но если мы с вами были добросовестными программистами и проверяли свой код на возможное неверное использование методом, о котором я рассказывал вам выше, нас ожидает еще одна "засада".
Предположим, мы с вами в коде, отвечающем за кофемолку, заблаговременно проверили наличие у пользователя плиты, необходимой нам для последующей варки смолотого кофе. Зачем? — спросите вы. А затем, что лучше вспомнить о забытом дома бумажнике еще при выходе из квартиры, чем обнаружить его отсутствие при попытке расплатиться в кассе магазина.
Думаю, многие из моих читателей натыкались в Интернет на длинную череду html-страниц, предназначенных для регистрации пользователя, загружающего с сайта ознакомительную версию программы или получающего, скажем, новый e-mail-адрес. Долго и муторно, с кучей проверок и обязательных для ввода полей, они выясняют, где вы живете. Потом им становится интересно, сколько вам лет и правильно ли вы указали свой e-mail-адрес. В результате, когда вспотевший от усилий пользователь уже предвкушает окончание этой экзекуции, на экран перед ним выскакивает сообщение вроде "mysql:error 4321.1". После этого жизнеутверждающего заявления страница вываливается на начальный экран мастера, и все начинается сначала.
В такой момент мне очень хочется взять граммофон из игрушки Alone in Dark и долго бить им по голове разработчика этого софта, а вместе с ним заодно всех тех, кто нанял его на работу. Поэтому, если мы с вами хотим стать профессиональными кодерами, в наших программах такой ситуации возникать никогда не должно. Следует немедленно, еще при запуске программы, убеждаться в том, что ваша программа способна довести возложенные на нее функции до победного конца.
Так вот. Если мы предусмотрели в коде подобные проверки, нам придется лезть во все подряд методы и переправлять код в связи с изменением списка задействованных в программе сущностей.

Таким образом, модификация программы, написанной в "линейной" идеологии, сводится, по сути, к написанию вместо нее другой, новой программы. А так как заново писать весь код программисту обычно лень, он начинает полностью перелопачивать уже имеющийся, внося еще большую неразбериху в и так невнятный изначально алгоритм программы. После двух-трех подобных переделок наш горе-программист сам полностью теряет понимание алгоритма работы программы "в целом". Посторонний же человек, не знакомый с его первоначальными задумками и последующим их развитием, вообще не в состоянии понять, чего именно наш программист добивался, когда писал те или иные фрагменты кода.
Где же выход? — спросит устрашенный читатель. — Неужели за все годы развития информатики никто так и не придумал, как обойти эти подводные камни? Выход действительно есть. Необходимо полностью отказаться от привычного многим линейного подхода к программированию и перейти на альтернативную ему идеологию, называемую "объектно-ориентированное программирование" — сокращенно ООП.
Описанию лежащих в основе ООП принципов я посвящу следующую статью.

Продолжение следует.
(с) Герман Иванов



Компьютерная газета. Статья была опубликована в номере 14 за 2003 год в рубрике программирование :: разное

©1997-2024 Компьютерная газета