ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 30.10.2023
Просмотров: 426
Скачиваний: 1
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
СОДЕРЖАНИЕ
142 Глава 5
Инкапсуляция — это механизм, позволяющий достичь большинства обозначенных выше целей, но при этом он имеет собственное досто- инство, состоящее в организации кода. В листингах длинных программ чистого процедурного кода (в языке C++ это означает код с функциями, но без классов) бывает очень сложно определить верный порядок для функций и директив компилятору, позволяющий легко запомнить их расположение. Вместо этого мы вынуждены полагаться на среду разра- ботки, которая будет искать наши функции для нас. Инкапсуляция объ- единяет те элементы, которые используются вместе. Если вы работаете с методом класса и понимаете, что вам надо посмотреть или модифи- цировать другой код, скорее всего, этот другой код встретится в другом методе этого же класса, и, следовательно, он где-то рядом.
Повторное использование кода
С точки зрения решения задач, инкапсуляция позволяет нам легче использовать код предыдущей задачи для решения текущей. Часто, даже если мы уже решили задачу, сходную с нашим текущим про- ектом, повторное использование кода, как мы уже узнали, все-таки требует много усилий. Полностью инкапсулированный класс может рабо тать подобно внешнему USB-носителю. Вы просто вставляете его, и он работает. Однако для того, чтобы это произошло, мы долж- ны корректно спроектировать класс и убедиться, что код и данные на самом деле инкапсулированы и настолько независимы от чего-ли- бо за пределами класса, насколько это возможно. Например, класс, который использует глобальную переменную, нельзя скопировать в другой проект без копирования этой глобальной переменной. Поми- мо повторного использования классов в разных программах, классы часто предоставляют более непосредственную форму повторного использования: наследование. Вспомните, как в главе 4 мы говорили об использовании вспомогательных функций для «выделения» кода, доступного двум или более функциям. Наследование использует эту идею в большем масштабе. Используя наследование, мы создаем ро- дительские классы с методами, доступными для двух или большего количества дочерних классов, таким образом, «выделяя» не просто несколько строк кода, а целые методы. Наследование — большая тема сама по себе, и мы рассмотрим эту форму повторного использо- вания кода позднее в этой главе.
Разделение задачи
Один способ, к которому мы будем возвращаться снова и снова, состоит в делении сложной задачи на маленькие фрагменты, с которыми про- ще работать. Классы отлично подходят для деления программ на функ- циональные элементы. Инкапсуляция не только хранит вместе данные и код в пакете, который можно использовать неоднократно; также она отделяет эти данные и код от остальной программы, позволяя нам ра- ботать в этом классе отдельно от чего-то еще. Чем больше классов мы создадим в программе, тем сильнее эффект разделения задачи.
Решение задач с классами
1 ... 13 14 15 16 17 18 19 20 ... 34
143
Таким образом, там, где возможно, мы разрешим классу быть нашим методом разделения сложной задачи. Если классы хорошо спроектированы, это усилит функциональное разделение, и задачу можно будет решить проще. Как побочный результат, возможно, мы обнаружим, что классы, созданные нами для решения одной задачи, могут использоваться в других, даже если мы полностью не рассма- тривали такую возможность, при их создании.
Сокрытие
Некоторые программисты используют термины сокрытие и инкап-
суляция как синонимы, однако здесь мы их разделим. Как было по- казано ранее в этой главе, инкапсуляция — это совместная упаковка данных и кода. Сокрытие означает отделение интерфейса структу- ры данных — определение процессов и их параметров — от реали- зации структуры данных или кода внутри функций. Если сокрытие являлось целью при написании класса, тогда возможно изменение реализации методов, без изменения в клиентском коде (код, кото- рый использует этот класс). И снова мы должны четко определить термин интерфейс . Он означает не только название методов и спи- сок их параметров, но и также объяснение (возможно отраженное в документации), что делают разные методы. Говоря об изменении реализации без изменения интерфейса, мы имеем в виду, что меняем
как методы класса работают, но не что они делают. Некоторые авто- ры книг по программированию относятся к этому, как к своего рода неявному соглашению между классом и клиентом: класс соглашает- ся никогда не менять результаты существующих процессов, а клиент соглашается использовать классы строго на основе их интерфейса и игнорировать детали реализации. Предположим, существует уни- версальный пульт, с помощью которого можно управлять любым те- левизором, начиная от старых моделей с ЭЛТ-экраном и заканчивая моделями с жидкокристаллическим или плазменным экраном. Вы нажимаете 2, затем 5, потом «ввод» и любой экран покажет двадцать пятый канал, хотя механизм, который позволит этому произойти, очень сильно отличается в зависимости от технологии, лежащей в его основе.
Невозможно сокрытие без инкапсуляции, однако в контексте опре- деленной нами терминологии возможна инкапсуляция без сокрытия.
Самый очевидный способ сделать это — объявить все поля класса как public
. В этом случае класс по-прежнему инкапсулирован, так как это пакет, в котором соединены вместе код и данные. Однако сейчас кли- ентский код имеет доступ к важным деталям реализации класса: пере- менным и типам, которые класс использует для хранения данных. Даже если клиентский код
непосредственно не изменяет данные класса, а только изучает их, клиентский код требует особую реализацию класса.
Любые изменения в классе, которые меняют имя или тип любой пере- менной доступной клиентскому коду, требуют соответствующих изме- нений в клиентском коде.
144 Глава 5
Возможно, первое, что вы подумали, что сокрытие гарантирова- но, пока все данные реализованы как приватные, а мы потратили до- статочно времени на проектирование списка функций класса и спи- ска их параметров так, что нам никогда не потребуется их менять.
Хотя все это требуется для сокрытия, этого не достаточно, потому что проблема сокрытия более тонкая. Помните, что по устоявшейся дого- воренности класс не может менять действия любого метода, незави- симо от ситуации. В предыдущих главах мы решали задачу, в которой функция обрабатывала пустой список, а также задачу с нестандартной ситуацией, как, например, поиск среднего значения в массиве, для которого параметр, содержащий размер массива, равнялся нулю. Из- менение результата метода даже для странных случаев представляет собой изменение интерфейса. Этого следует избегать. Это еще одна причина, почему в программировании важно явно рассматривать специальные случаи. Многие программы перестают работать, когда обновляется лежащая в их основе технология или программный ин- терфейс приложения (API), и некоторые системные вызовы, кото- рые надежно возвращали –1, в случаях, когда один из параметров был ошибочным, теперь возвращают вроде бы случайную отрицательную величину. Чтобы избежать такой проблемы, одним из наилучших спо- собов является определение результатов специальных методов в доку- ментации метода или класса. Если ваша собственная документация го- ворит, что вы возвращаете код ошибки –1 в случае особых ситуаций, следует подумать дважды о возможном возврате чего-то еще.
Итак, как же сокрытие влияет на решение задач? Принципы со- крытия говорят программисту отложить в сторону детали реализации класса при работе над клиентским кодом или, в более широком смыс- ле, заниматься конкретной реализацией класса только внутри класса.
Если вы можете выкинуть из головы детали реализации, вы можете избавиться от отвлекающих мыслей и сконцентрироваться на реше- нии текущих задач.
Однако мы должны осознавать ограничения сокрытия при реше- нии задач. Иногда детали реализации все-таки имеют значение для клиента. В предыдущих главах мы видели преимущества и недостатки некоторых структур, основанных на массивах или указателях. Струк- туры, основанные на массивах, допускают случайный доступ, но не мо- гут легко увеличиваться и уменьшаться, в то время как структуры, ос- нованные на указателях, допускают только последовательный доступ, но позволяют добавление или удаление фрагментов без повторного создания всей структуры. Следовательно, класс, созданный на основе структуры, основанной на массиве, имеет качественные различия с классом, построенном на структуре, основанной на указателях.
В информатике мы часто говорим о концепции абстрактного
типа данных , что является сокрытием в самом чистом виде: тип дан- ных определяется только во время работы. В главе 4 мы обсуждали концепцию стека и описали стек программы как смежные блоки
Решение задач с классами 145
памяти. Однако как абстрактный тип данных, стек, это любой тип данных, где вы можете добавить и удалить отдельный элемент, при этом элементы удаляются в обратном порядке по сравнению с тем, в котором они добавлялись. Этот порядок известен как «последним пришел — первым ушел», или LIFO. Ничто не заставляет стек быть смежными блоками памяти, мы можем реализовать стек, используя связный список. Поскольку смежные блоки памяти и связный спи- сок имеют разные свойства, у стека, имеющего одну или другую реа- лизации, также будут разные свойства, и это может привести к боль- шим различиям для клиента, использующего этот стек.
Смысл всего этого в том, что сокрытие будет для нас полезной целью при решении задач в той мере, в которой оно позволяет раз- делить задачу и работать отдельно с различными частями програм- мы. Однако мы не можем позволить себе полностью игнорировать детали реализации.
Читабельность
Хороший класс повышает читабельность программы. Объекты мо- гут соответствовать тому, как мы их видим в реальном мире, и, следо- вательно, вызов метода часто имеет англоязычную читабельность.
Кроме того, взаимоотношения между объектами часто понятнее, чем взаимоотношения между отдельными переменными. Повыше- ние читабельности увеличивает возможности решения задач, по- скольку мы можем лучше понять свой собственный код во время раз- работки и потому что повторное использование встречается чаще, когда понятно, как работать со старым кодом.
Чтобы максимизировать преимущества хорошей читабельности классов, мы должны подумать о том, как методы нашего класса будут ис- пользоваться на практике. Имена методов надо выбирать, думая об от- ражении наиболее точного смысла результатов метода. Например, рас- смотрим класс, осуществляющий расчеты финансовых инвестиций, в котором содержится метод расчета будущей стоимости. Имя compute сообщает намного меньше информации, чем computeFutureValue. Даже выбор правильной части речи для имени может быть важным. Имя computeFutureValue
— это глагол, а futureValue — существительное. По- смотрите, как используются имена в следующем примере кода.
double FV;
X investment.computeFutureValue(FV, 2050);
Y if (investment.futureValue(2050) > 10000) { ...
Если вы задумаетесь, то обнаружите, что первый вариант имеет смысл для самостоятельного вызова, то есть функция, возвращающая void
, в которой будущая стоимость возвращается к вызвавшему ее кли- енту с помощью ссылочного параметра
X
. Последний вариант имеет больше смысла для вызова, который используется в выражении, то есть будущая стоимость возвращается как значение функции
Y