В.В. Кулямин - Технологии программирования. Компонентный подход (1133554), страница 7
Текст из файла (страница 7)
Этот принцип предписывает организовывать сложную систему в виденабора более простых систем — модулей, взаимодействующих друг с другом через четкоопределенные интерфейсы. При этом каждая задача, решаемая всей системой, разбиваетсяна более простые, решаемые отдельными модулями подзадачи, решение которых, будучискомбинировано определенным образом, дает в итоге решение исходной задачи. Послеэтого можно отдельно рассматривать каждую подзадачу и модуль, ответственный за еерешение, и отдельно — вопросы интеграции полученного набора модулей в целостнуюсистему, способную решать исходные задачи.Выделение четких интерфейсов для взаимодействия упрощает интеграцию, позволяяпроводить ее на основе явно очерченных возможностей этих интерфейсов, без обращения кмногочисленным внутренним элементам модулей, что привело бы к росту сложности.Пример.Примером разбиения на модули может служить структура пакетов и классов библиотекиJDK.
Классы, связанные с основными сущностями языка Java и виртуальной машины,собраны в пакете java.lang. Вспомогательные широко применяемые в различныхприложениях классы, такие, как коллекции, представления даты и пр., собраны вjava.util. Классы, используемые для реализации потокового ввода-вывода — в пакетеjava.io, и т.д.Интерфейсом класса служат его общедоступные методы, а интерфейсом пакета — егообщедоступные классы.Другой пример.Другой пример модульности — принятый способ организации протоколов передачиданных. Мы уже видели, что удобно выделять несколько уровней протоколов, чтобы накаждом решать свои задачи.
При этом надо определить, как информация передается отмашины к машине при помощи всего этого многоуровневого механизма. Обычное решениетаково: для каждого уровня определяется способ передачи информации с или на верхнийуровень — предоставляемые данным уровнем службы. Точно так же определяется, в какихслужбах нижнего уровня нуждается верхний, т.е. как передать данные на нижний уровень иполучить их оттуда.
После этого каждый протокол на данном уровне может бытьсформулирован в терминах обращений к нижнему уровню и должен реализовать операции15службы, необходимые верхнему. Это позволяет заменять протокол-модуль на одном уровнебез внесения изменений в другие.Хорошее разбиение системы на модули — непростая задача. При ее выполнениипривлекаются следующие дополнительные принципы.o Выделение интерфейсов и сокрытие информации.Модули должны взаимодействовать друг с другом через четко определенныеинтерфейсы и скрывать друг от друга внутреннюю информацию — внутренние данные,детали реализации интерфейсных операций.При этом интерфейс модуля обычно значительно меньше, чем набор всех операций иданных в нем.Например, класс java.util.Queue<type E>, реализующий функциональность очередиэлементов типа E, имеет следующий интерфейс.E element()Возвращает элемент, стоящий в голове очереди, не изменяяее.
Создает исключение NoSuchElementException, еслиочередь пуста.boolean offer(E o)Вставляет, если возможно, данный элемент в конец очереди.Возвращает true, если вставка прошла успешно, false —иначе.E peek()Возвращает элемент, стоящий в голове очереди, не изменяяее. Возвращает null, если очередь пуста.E poll()Возвращает элемент, стоящий в голове очереди, и удаляет егоиз очереди.
Возвращает null, если очередь пуста.E remove()Возвращает элемент, стоящий в голове очереди, и удаляет егоиз очереди. Создает исключение NoSuchElementException,если очередь пуста.Внутренние же данные и операции одного из классов, реализующих данный интерфейс,— PriorityBlockingQueue<E> — достаточно сложны. Этот класс реализует очередь сэффективной синхронизацией операций, позволяющей работать с таким объектомнескольким параллельным потокам без лишних ограничений на их синхронизацию.Например, один поток может добавлять элемент в конец непустой очереди, а другой вто же время извлекать ее первый элемент.package java.util.concurrent;import java.util.concurrent.locks.*;import java.util.*;public class PriorityBlockingQueue<E> extends AbstractQueue<E>implements BlockingQueue<E>, java.io.Serializable {private static final long serialVersionUID = 5595510919245408276L;private final PriorityQueue<E> q;private final ReentrantLock lock = new ReentrantLock(true);private final ReentrantLock.ConditionObject notEmpty =lock.newCondition();public PriorityBlockingQueue() { ...
}public PriorityBlockingQueue(int initialCapacity) { … }public PriorityBlockingQueue(int initialCapacity,Comparator<? super E> comparator) { … }public PriorityBlockingQueue(Collection<? extends E> c) { ... }public boolean add(E o) { ... }public Comparator comparator() { … }public boolean offer(E o) { … }public void put(E o) { … }public boolean offer(E o, long timeout, TimeUnit unit) { … }public E take() throws InterruptedException { … }16public E poll() { … }public E poll(long timeout, TimeUnit unit) throws InterruptedException {… }public E peek() { … }public int size() { … }public int remainingCapacity() { … }public boolean remove(Object o) { … }public boolean contains(Object o) { … }public Object[] toArray() { … }public String toString() { … }public int drainTo(Collection<? super E> c) { … }public int drainTo(Collection<? super E> c, int maxElements) { … }public void clear() { … }public <T> T[] toArray(T[] a) { … }public Iterator<E> iterator() { … }private class Itr<E> implements Iterator<E> {private final Iterator<E> iter;Itr(Iterator<E> i) { … }public boolean hasNext() { … }public E next() { … }public void remove() { … }}private void writeObject(java.io.ObjectOutputStream s)throws java.io.IOException { … }}o Адекватность, полнота, минимальность и простота интерфейсов.Этот принцип объединяет ряд свойств, которыми должны обладать хорошоспроектированные интерфейсы. Адекватность интерфейса означает, что интерфейс модуля дает возможностьрешать именно те задачи, которые нужны пользователям этого модуля.Например, добавление в интерфейс очереди метода, позволяющего получить любойее элемент по его номеру в очереди, сделало бы этот интерфейс не вполнеадекватным — он превратился бы почти в интерфейс списка, который используетсядля решения других задач.
Очереди же используются там, где полнаяфункциональность списка не нужна, а реализация очереди может быть сделанаболее эффективной. Полнота интерфейса означает, что интерфейс позволяет решать все значимыезадачи в рамках функциональности модуля.Например, отсутствие в интерфейсе очереди метода offer() сделало бы егобесполезным — никому не нужна очередь, из которой можно брать элементы, акласть в нее ничего нельзя.Более тонкий пример — методы element() и peek(). Нужда в них возникает, еслипрограмма не должна изменять очередь, и в то же время ей нужно узнать, какойэлемент лежит в ее начале. Отсутствие такой возможности потребовало бысоздавать собственное дополнительное хранилище элементов в каждой такойпрограмме. Минимальность интерфейса означает, что предоставляемые интерфейсомоперации решают различные по смыслу задачи и ни одну из них нельзя реализоватьс помощью всех остальных (или же такая реализация довольно сложна инеэффективна).Представленный в примере интерфейс очереди не минимален — методы element()и peek(), а также poll() и remove() можно выразить друг через друга.17Минимальный интерфейс очереди получился бы, например, если выбросить паруметодов element() и remove().Большое значение минимальности интерфейса уделяют, если размер модулейоказывает сильное влияние на производительность программы.
Например, припроектировании модулей операционной системы — чем меньше она занимает местав памяти, тем больше его останется для приложений, непосредственно необходимыхпользователям.При проектировании библиотек более высокого уровня имеет смысл не делатьинтерфейс минимальным, давая пользователям этих библиотек возможности дляповышения производительности и понятности их программ. Например, часто бываетполезно реализовать действия «проверить, что элемент не принадлежит множеству,и, если нет, добавить его» в одном методе, не заставляя пользователей каждый разсначала проверять принадлежность элемента множеству, а затем уже добавлять его. Простота интерфейса означает, что интерфейсные операции достаточноэлементарны и не представимы в виде композиций некоторых более простыхопераций на том же уровне абстракции, при том же понимании функциональностимодуля.Скажем, весь интерфейс очереди можно было бы свести к одной операции Objectqueue(Object o, boolean remove), которая добавляет в очередь объект, указанныйв качестве первого параметра, если это не null, а также возвращает объект в головеочереди (или null, если очередь пуста) и удаляет его, если в качестве второгопараметра указать true.
Однако, такой интерфейс явно сложнее для понимания, чемпредставленный выше.o Разделение ответственности.Основной принцип выделения модулей — создание отдельных модулей под каждуюзадачу, решаемую системой или необходимую в качестве составляющей для решения ееосновных задач.Пример.Класс java.util.Date представляет временную метку, состоящую из даты и времени.Это представление должно быть независимо от используемого календаря, формызаписи дат и времени в данной стране и часового пояса.Для построения конкретных экземпляров этого класса на основе строковогопредставления даты и времени (например, «22:32:00, June 15, 2005») в том виде, как ихиспользуют в Европе, используется класс java.util.GregorianCalendar, посколькуинтерпретация записи даты и времени зависит от используемой календарной системы.Разные календари представляются различными объектами интерфейсаjava.util.Calendar, которые отвечают за преобразование всех дат в некотороенезависимое представление.Для создания строкового представления времени и даты используется классjava.text.SimpleDateFormat, поскольку нужное представление, помимо календарнойсистемы, может иметь различный порядок перечисления года, месяца и дня месяца иразличное количество символов, выделяемое под представление разных элементов даты(например, «22:32:00, June 15, 2005» и «05.06.15, 22:32»).Принцип разделения ответственности имеет несколько важных частных случаев. Разделение политик и алгоритмов.Этот принцип используется для отделения постоянных, неизменяемых алгоритмовобработки данных от изменяющихся их частей и для выделения этих частей,называемых политиками, в параметры общего алгоритма.Так, политика, определяющая формат строкового представления даты и времени,задается в виде форматной строки при создании объекта классаjava.text.SimpleDateFormat.