Последнее изменение: 22 сентября 2007г.

О философии Java и многом другом

Сразу хочу предупредить вот о чем. Здесь не будет криков "как все круто" или "как все плохо". Я отношусь к Java именно с точки зрения философской. Это – данность. Потому – рассуждать о том, хорош язык или плох, бессмысленно. Язык может удовлетворять предъявляемым требованиям, либо не удовлетворять им. Ежели удовлетворяет – прекрасно, пользуемся. Нет – выбираем другой. А то получается как в том анекдоте: "Мыши плакали, кололись, давились, но продолжали грызть кактус" . Если Java для вас – кактус, возможно, стоит задуматься о выборе другого языка. А если все-таки грызете, несмотря на то, что это кактус, – значит, плюсов в этом занятии для вас больше чем минусов. Тогда не жалуйтесь.

Итак, что можно упомянуть в качестве основных черт Java:

  • Java – полностью объектно-ориентированный язык. Что это значит? Это значит, что ВСЁ в языке является объектом. Исключение – 8 примитивных типов, хотя некоторые свойства объектов у них тоже присутствуют, в частности, у них есть класс.
  • В Java НЕТ множественного наследования в том виде, в котором оно есть в С++. Не было и, надеюсь, не будет. Грамотный дизайн приложения позволяет обойтись без множественного наследования реализации. Лично я за более чем 10 лет работы с Java ни разу не встретился с ситуацией, когда мне это понадобилось бы . Еще раз подчеркиваю – я говорю именно о множественном наследовании от классов, содержащих реализацию.
  • Java – кроссплатформенный язык. Это накладывает некоторые ограничения на дизайн, функциональность и применяемые средства, но переносимость, на мой взгляд, того стоит.

Есть еще много моментов, о которых можно было бы поговорить, но эти – ключевые. Пройдемся по ним по порядку.

Объектная ориентация.

Это не так просто, как может показаться на первый взгляд. Многие, переходя на Java, начинают писать на нем в стиле "C with objects" – весь код в одном-двух методах и в одном классе. Это ни в коем случае не является объектно-ориентированным подходом. Потому как по определению,

Объект – совокупность данных и методов работы с ними.

Почему я так выделил, казалось бы, первое и основное определение ООД? Потому как в Java оно должно применяться как никогда часто, именно ввиду того, что объектом является всё. Практика показывает, что приучиться думать в терминах объектов весьма непросто. Мне приходилось напоминать это определение весьма и весьма опытным разработчикам, когда они пытались скрестить ежа с ужом. А именно – всунуть логику взаимодействия объектов одного типа в класс этого же типа. При том, что для этой логики объекты являются данными, а, значит, по определению они должны быть инкапсулированы в объекте более высокого уровня.

Возникает резонный вопрос: а что, собственно, должен содержать объект? Какие данные? Собственно, в умении это определять и состоит искусство проектирования. Это приходит только с опытом. Я лишь могу сказать, чего объект НЕ должен содержать: ничего лишнего. Объект должен описывать ОДНУ И ТОЛЬКО ОДНУ моделируемую сущность. Причем, как очень хорошо сказал один из участников форума на javalobby.org, Clifton Craig, – "если в описании назначения (responsibility) объекта вы используете 'И' (что-то И что-то), то вы допускаете ошибку." Объект должен быть максимально простым. А из этих простых объектов уже, как из кубиков, собираются более сложные.

Еще одна вещь, которая приходит с опытом – умение мыслить в категориях абстрактных объектов. И если оперировать просто объектами научиться сравнительно легко, то определять уровень абстракции – задача гораздо более сложная. Зато и результат может быть гораздо более впечатляющим.

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

Приведу пример. Допустим, у нас есть объект – дом. Из чего он состоит? Из стен, крыши, окон, дверей. Фундамент у него есть. Вроде все. Можно определить эти пять объектов и на этом успокоиться.

Однако. Если строить этот дом на песке – фундамент должен быть один. Если на камне – другой. На льду – третий. У дома может быть различно количество комнат. Разная крыша. Разные окна, двери. Если начать реализовывать объект дом для каждой комбинации – можно поседеть. И это при том, что формальную логику построения дома может описать даже ребенок. Сначала нужно сделать фундамент, потом построить стены, потом крышу, потом вставить окна и двери. Всё. И этот ребенок абсолютно прав, поскольку он оперирует абстрактными понятиями – теми самыми пятью объектами. Между тем, Java позволяет легко формализовать процесс построения дома. Нужно всего лишь ввести несколько дополнительных объектов.

Итак, начнем. Мы строим дом. Конкретный. У которого есть проект. Этот проект включает в себя все подробности – из чего сделаны стены, каково расположение комнат, сколько окон и дверей и т.п. Далее, мы определяем абстрактные понятия (объекты) – фундамент, стена, крыша, окно, дверь. У каждого из этих абстрактных понятий есть какие-то свойства – толщина, материал, размеры, местоположение и т.п. Логика, находящаяся в объекте дом, оперирует именно с этими понятиями – больше ей и не надо. Таким образом, мы получаем возможность построить дом из абстрактных кубиков.

Ясно, что из абстракций реального дома не построишь. Нужно как-то эти абстракции реализовать. Для этого мы вводим еще один объект – строитель, – который будет отвечать за создание реальных объектов на основе проекта. Из проекта строитель получает всю необходимую информацию и создает стены нужной толщины, нужной конфигурации, из нужного материала. И в реальности при строительстве дома будут использованы именно эти объекты, хотя логике строительства до конкретных объектов нет дела – она может использовать любые.

Посмотрим, что у нас получилось. У нас есть абстрактные понятия, части дома: фундамент, стена, крыша, окно, дверь. . От конкретики они не зависят. У нас есть дом, который содержит логику построения себя (это не совсем корректно, но для простоты оставим так), причем эта логика оперирует абстрактными объектами, т.е. тоже не зависит от конкретики. У нас есть строитель, который умеет создавать те самые конкретные объекты. Его логика точно так же не зависит от конкретики, он берет все данные из проекта. Что у нас осталось? Один (ОДИН!!!) объект, который мы должны реализовать для построения любого дома – проект. Вся остальная логика у нас уже абстрагирована от конкретной реализации. Фактически мы этого добились введением всего двух дополнительных объектов по сравнению с первоначальным дизайном – строителя и проекта. Плюс еще выделили формальную логику построения дома, без которой обошлись сначала.

Безусловно, этот пример сильно упрощен. Я всего лишь стремился показать, что грамотное проектирование – определение абстрактных объектов, выделение формальной логики и т.п. – позволяет добиться гораздо большей гибкости, нежели при подходе "в лоб". А именно таким подходом чаще всего пользуются те, кто только начинает писать на Java.

В качестве одной из самых значительных черт Java с самого начала называлась возможность написать код один раз, а потом использовать его всюду. Я неоднократно слышал от разных программистов, что это ерунда и не работает. Причем сопровождалось все это вопросом с этакой издевкой – "Ну скажи, ты сам много использовал без изменений из своего кода, написанного ранее?"

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

Хорошими примерами грамотно спроектированных систем является, как правило, библиотеки. Взять, к примеру, Log4J, библиотеку для вывода логов. Можно использовать предопределенные уровни лога, можно определять свои. Сообщения могут выводиться сразу, могут накапливаться и выводиться пакетами. Вывод может происходить десятком различных способов, вплоть до отправки писем или записи в базу данных. Не подходят существующие – можно реализовать свои. И все это при НЕИЗМЕННОМ ядре.

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

Итак, перейдем к следующему ключевому моменту Java, а именно –

Множественное наследование, вернее, его отсутствие.

Строго говоря, это не совсем так. В Java множественное наследование существует, правда, в сильно ограниченном виде. Я говорю об интерфейсах, которые, фактически, являются полностью абстрактными классами с некоторыми ограничениями (например, методы в интерфейсе всегда public, и т.п.)

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

Третий ключевой момент Java –

Кроссплатформенность.

Я бы сказал, что это одна из наиболее сильных сторон Java. Возможность исполнения одного и того же кода на любой платформе где есть интерпретатор открывает серьезные перспективы. Самым большим камнем преткновения в этом вопросы некоторое время был пользовательский интерфейс. Однако с появлением JFC (Java Foundation Classes, он же Swing) разработчики получили возможность использования (и реализации собственных!) платформонезависимых интерфейсов пользователя.

Однако эта же черта накладывает и ограничения. Прежде всего, Java – не native язык. И для использования системных функций он опирается на native-библиотеки в составе JRE. Эти библиотеки покрывают, на мой взгляд, 90% всех запросов разработчиков. Но существуют еще 10%. Я бы эти 10% поделил на две части – блажь и необходимость. Причем блажи, на мой взгляд, существенно больше. Приведу несколько примеров.

Пример 1. Необходимо, совершенно, кровь из носу – но чтобы приложение сворачивалось в system tray. Это – блажь. В чистом виде. Пользователи Linux почему-то прекрасно обходятся без system tray. Если мне не изменяет память, пользователи Mac – тоже. Соответственно, поведение приложения будет зависеть от платформы, что не есть хорошо, мягко говоря.

Пример 2. Необходимо осуществлять периодическую проверку функционирования канала до некоторого сервера. Причем не просто самого факта существования маршрута – нужен контроль времени прохождения пакета. Т.е. простейший ping. Это – необходимость. Одна беда – ping реализуется на основе ICMP (если я правильно помню). А протокол самого нижнего уровня, к которому имеет доступ Java-приложение – TCP либо UDP. ICMP же, фактически, является частью IP, т.е. лежит ниже1. Следовательно, реализовать его "руками" не получится. А поддержки ICMP у native-библиотек Java – нет. И вот тут – хочешь или нет, а иметь дело с системой напрямую каким-то образом придется.

Пример 3. Доступ к каталогу в Unix-like системах регулируется правами user-group-others, прописываемыми для каждого каталога в отдельности. Предположим, серверное приложение должно создать домашнюю директорию для некоторого пользователя. А приложение работает под пользователем www. Следовательно, нужно выставить созданной директории нового владельца и определенные права. В Java же нет способов это сделать, поскольку такая модель регулирования доступа является специфичной для Unix-like систем. Обращение к системе напрямую неизбежно.

Примеров таких можно привести множество. Я чаще всего сталкивался с попытками реализовать именно блажь. Либо пользователь этого хочет, либо разработчику кажется, что это круто, классно, удобно и т.п. В любом случае, на мой взгляд, это недопустимо. Прежде всего потому, что напрочь убивает кроссплатформенность. Потому как ОЧЕНЬ сложно добиться одинакового поведения на любой платформе, используя системные функции и/или библиотеки. Это я хорошо знаю на личном опыте.

Если же все-таки приходится работать с системой напрямую – это надо делать очень аккуратно и в строго необходимом количестве. В любом случае придется задуматься – на каких платформах приложение потенциально будет выполняться, и есть ли на всех этих платформах средство для достижения результата. В приведенном мною примере с правами доступа, например, в Windows нет таких возможностей. И возникает резонный вопрос – а стоит ли делать это для Linux/Unix, если на других платформах приложение гарантированно будет работать по-другому? Может, стоит изменить дизайн так, чтобы вообще отказаться от такого решения?

В любом случае лично я – против использования native-вызовов и библиотек. Я рассматриваю их как последнее средство, когда испробованы уже все способы, и ни один из них результата не дал. Для меня гораздо дороже переносимость.

* * *

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



1. Я всегда считал, что ICMP лежит выше IP, т.е. на том же уровне, что и TCP/IP. Спасибо участнику конференции с ником Zorkus, указавшему на эту ошибку. Несмотря на то, что ICMP использует фрейм IP, он находится с ним на одном уровне и считается частью IP.