Опубликован: 18.09.2006 | Уровень: специалист | Доступ: платный | ВУЗ: Московский государственный университет имени М.В.Ломоносова
Лекция 1:

Проблемы разработки сложных программных систем

Лекция 1: 123 || Лекция 2 >

Хорошее разбиение системы на модули — непростая задача. При ее выполнении привлекаются следующие дополнительные принципы.

  • Выделение интерфейсов и сокрытие информации.

    Модули должны взаимодействовать друг с другом через четко определенные интерфейсы и скрывать друг от друга внутреннюю информацию — внутренние данные, детали реализации интерфейсных операций.

    При этом интерфейс модуля обычно значительно меньше, чем набор всех операций и данных в нем.

    Например, класс 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 { … }
        public 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 { … }
    }
    1.2.
  • Адекватность, полнота, минимальность и простота интерфейсов.

    Этот принцип объединяет ряд свойств, которыми должны обладать хорошо спроектированные интерфейсы.

    • Адекватность интерфейса означает, что интерфейс модуля дает возможность решать именно те задачи, которые нужны пользователям этого модуля.

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

    • Полнота интерфейса означает, что интерфейс позволяет решать все значимые задачи в рамках функциональности модуля.

      Например, отсутствие в интерфейсе очереди метода offer() сделало бы его бесполезным — никому не нужна очередь, из которой можно брать элементы, а класть в нее ничего нельзя.

      Более тонкий пример — методы element() и peek(). Нужда в них возникает, если программа не должна изменять очередь, и в то же время ей нужно узнать, какой элемент лежит в ее начале. Отсутствие такой возможности потребовало бы создавать собственное дополнительное хранилище элементов в каждой такой программе.

    • Минимальность интерфейса означает, что предоставляемые интерфейсом операции решают различные по смыслу задачи и ни одну из них нельзя реализовать с помощью всех остальных (или же такая реализация довольно сложна и неэффективна).

      Представленный в примере интерфейс очереди не минимален — методы element() и peek(), а также poll() и remove() можно выразить друг через друга. Минимальный интерфейс очереди получился бы, например, если выбросить пару методов element() и remove().

      Большое значение минимальности интерфейса уделяют, если размер модулей оказывает сильное влияние на производительность программы. Например, при проектировании модулей операционной системы — чем меньше она занимает места в памяти, тем больше его останется для приложений, непосредственно необходимых пользователям.

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

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

      Скажем, весь интерфейс очереди можно было бы свести к одной операции Object queue(Object o, boolean remove), которая добавляет в очередь объект, указанный в качестве первого параметра, если это не null, а также возвращает объект в голову очереди (или null, если очередь пуста) и удаляет его, если в качестве второго параметра указать true. Однако такой интерфейс явно сложнее для понимания, чем представленный выше.

  • Разделение ответственности.

    Основной принцип выделения модулей — создание отдельных модулей под каждую задачу, решаемую системой или необходимую в качестве составляющей для решения ее основных задач.

    Пример.

    Класс 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. Сам же алгоритм построения этого представления основывается на этой форматной строке и на самих времени и дате.

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

    • Разделение интерфейса и реализации.

      Этот принцип используется при отделении внешне видимой структуры модуля, описания задач, которые он решает, от способов решения этих задач.

      Пример такого разделения — отделение интерфейса абстрактного списка java.util.List<E> от многих возможных реализаций этого интерфейса, например, java.util.ArrayList<E>, java.util.LinkedList<E>. Первый из этих классов реализует список на основе массива, а второй — на основе ссылочной структуры данных.

  • Слабая связность (coupling) модулей и сильное сродство (cohesion) функций в одном модуле.

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

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

    Так, можно добавить в интерфейс очереди метод void println(String), отправляющий строку на стандартный вывод. Но он совсем не связан с остальными и с задачами, решаемыми очередью. Следовательно, трудоемкость анализа и внесения изменений в полученную систему будет значительно выше — ведь изменения в контексте разных задач возникают обычно независимо. Поэтому гораздо лучше поместить такой метод в другой модуль.

Переиспользование.

Этот принцип требует избегать повторений описаний одних и тех же знаний — в виде структур данных, действий, алгоритмов, одного и того же кода — в разных частях системы. Вместо этого в хорошо спроектированной системе выделяется один источник, одно место фиксации для каждого элемента знаний и организуется его переиспользование во всех местах, где нужно использовать этот элемент знаний. Такая организация позволяет при необходимости (например, при исправлении ошибки или расширении имеющихся возможностей) удобным образом модифицировать код и документы системы в соответствии с новым содержанием элементов знаний, поскольку каждый из них зафиксирован ровно в одном месте.

Примером может служить организация библиотечных классов java.util.TreeSet и java.util.TreeMap. Первый класс реализует хранение множества элементов, на которых определен порядок, в виде сбалансированного дерева. Второй класс реализует то же самое для ассоциативного массива или словаря (map), если определен порядок его ключей. Все алгоритмы работы со сбалансированным деревом в обоих случаях одинаковы, поэтому имеет смысл реализовать их только один раз. Если посмотреть на код этих классов в библиотеке JDK от компании Sun, можно увидеть, что ее разработчики так и поступили — класс TreeSet реализован как соответствующий ассоциативный массив TreeMap, в котором ключи представляют собой множество хранимых значений, а значение в любой паре (ключ, значение) равно null.

Лекция 1: 123 || Лекция 2 >
Владислав Нагорный
Владислав Нагорный

Подскажите, пожалуйста, планируете ли вы возобновление программ высшего образования? Если да, есть ли какие-то примерные сроки?

Спасибо!

Лариса Парфенова
Лариса Парфенова

1) Можно ли экстерном получить второе высшее образование "Программная инженерия" ?

2) Трудоустраиваете ли Вы выпускников?

3) Можно ли с Вашим дипломом поступить в аспирантуру?

 

Андрей Швецов
Андрей Швецов
Россия, Александровск, школа-гимназия №2 им. Островского, 2005
Анна Оганян
Анна Оганян
Россия, Москва, МГОУ