Основная тема данного курса — методы разработки "больших" и
Каждый человек, хоть раз написавший какую-либо программу, достаточно хорошо может представить себе, как разработать "небольшую" программу, решающую обычно одну конкретную несложную задачу и предназначенную, чаще всего, для использования одним человеком или узкой группой людей.
Примером может служить программа, вычисляющая достаточно много (но не слишком, не больше 30000) знаков числа $$\pi$$.
Воспользуемся следующими формулами:
$$\arctan(x) = x – x^{3}/3 + x^{5}/5 – x^{7}/7 + … + (-1)^{n}x^{2n+1}/(2n+1) + O(x^{2n+3}) \\ \pi /4 = \arctan(1) = 4*\arctan(1/5) – \arctan(1/239)$$Соответсвующая программа на языке Java может выглядеть примерно так:
public class PiCalculator
{
//Позволяет при вычислениях с повышенной точностью умножать и делить на числа
// <= 42949 = ( 2^32 mod CLUSTER_SIZE )
//Эта константа должна быть степенью 10 для простоты представления чисел.
private final static long CLUSTER_SIZE = 100000;
//Определенное значение этого поля позволяет сосчитать
// numberOfClusters * lg( CLUSTER_SIZE )
//точных цифр.
private static int numberOfClusters;
private static void print(long a[])
{
for(int i = 0; i < numberOfClusters + 1; i++)
{
if (i == 0) System.out.print("" + a[i] + '.');
else
{
StringBuffer s = new StringBuffer();
long z = CLUSTER_SIZE/10;
while(z > 0)
{
if (z > a[i]) { s.append(0); z /= 10; }
else break;
}
if (z != 0) s.append(a[i]);
System.out.print(s);
}
}
System.out.println();
}
private static void lndiv(long a[], int n)
{
for(int i = 0; i < numberOfClusters + 1; i++)
{
if (i != numberOfClusters)
{
a[i+1] += (a[i]%n)*CLUSTER_SIZE;
a[i] /= n;
}
else a[i] /= n;
}
}
private static void lnadd(long a[], long b[])
{
for(int i = numberOfClusters; i >= 0; i--)
{
if (i != 0)
{
a[i-1] += (a[i] + b[i])/CLUSTER_SIZE;
a[i] = (a[i] + b[i])%CLUSTER_SIZE;
}
else
a[i] = (a[i] + b[i])%CLUSTER_SIZE;
}
}
private static void lnsub(long a[], long b[])
{
for(int i = numberOfClusters; i >= 0; i--)
{
if (i != 0)
{
if (a[i] < b[i]) { b[i-1]++; a[i] += CLUSTER_SIZE; }
a[i] -= b[i];
}
else
a[i] -= b[i];
}
}
public static void main (String[] args)
{
int i, j, numberOfDigits = 100, numberOfSteps;
if (args.length > 0) numberOfDigits = Integer.parseInt(args[0]);
numberOfSteps = (int)(((numberOfDigits + 1)/(Math.log(5)/Math.log(10)) - 1)/2+1);
numberOfClusters = (int)(numberOfDigits/(Math.log(CLUSTER_SIZE)/Math.log(10))+1);
long a1[] = new long[numberOfClusters + 1];
long b1[] = new long[numberOfClusters + 1];
long c1[] = new long[numberOfClusters + 1];
long a2[] = new long[numberOfClusters + 1];
long b2[] = new long[numberOfClusters + 1];
long c2[] = new long[numberOfClusters + 1];
a1[0] = 16;
a2[0] = 4;
lndiv(a1, 5);
lndiv(a2, 239);
System.arraycopy(a1, 0, c1, 0, numberOfClusters + 1);
System.arraycopy(a2, 0, c2, 0, numberOfClusters + 1);
for(j = 1; j < numberOfSteps; j++)
{
lndiv(a1, 25);
lndiv(a2, 239);
lndiv(a2, 239);
System.arraycopy(a1, 0, b1, 0, numberOfClusters + 1);
System.arraycopy(a2, 0, b2, 0, numberOfClusters + 1);
lndiv(b1, 2*j+1);
lndiv(b2, 2*j+1);
if (j%2 == 0) { lnadd(c1, b1); lnadd(c2, b2); }
else { lnsub(c1, b1); lnsub(c2, b2); }
}
lndiv(a1, 25);
lndiv(a1, 2*numberOfSteps + 1);
System.out.println("Оценка точности результата:");
print(a1);
lnsub(c1, c2);
System.out.println("Результат:");
print(c1);
}
}
Данная программа — "небольшая", как по размерам (~150 строк), так и по другим признакам:
Обычно
Примером "большой" программы может служить стандартная библиотека классов Java, входящая в
Строго говоря, ни одно из указанных свойств не является обязательным для того, чтобы программу можно было считать "большой", но при наличии двух-трех из них достаточно уверенно можно утверждать, что она "большая".
На основании некоторых из перечисленных свойств можно сделать вывод, что "большая" программа или программная система чаще всего представляет собой не просто код или исполняемый файл, а включает еще и набор проектной и пользовательской документации.
Для разработки программных систем требуются особые методы — как уже говорилось, их нельзя написать "нахрапом". Изучением организационных, инженерных и технических аспектов создания ПО, включая методы разработки, занимается дисциплина, называемая
Важно отметить, что практически полезная
Большинство опытных разработчиков и исследователей считают, что практически значимые программные системы всегда содержат ошибки. При переходе от "небольших" программ к "большим" понятие "правильной" программы становится практически бессмысленным. Говоря о программной системе, (в отличие от приведенной выше программы вычисления числа?), нельзя утверждать, что она "правильная", т.е. всегда правильно решает все поставленные перед ней задачи. Этот факт связан как с практической невозможностью полного доказательства или проверки этого, так и с тем, что смысл существования программной системы — удовлетворение потребностей и запросов большого количества различных заинтересованных лиц. А эти потребности не только нечетко определены, различны для разных групп пользователей и иногда противоречивы, но и значительно изменяются с течением времени.
В связи с этим, вместо рассмотрения "правильных" и "неправильных" программных систем, в силу практического отсутствия первых, рассматривают "достаточно качественные" и "недостаточно качественные".
Поэтому и основные проблемы разработки
Часто программное обеспечение (ПО) нельзя рассматривать отдельно от программно-аппаратной системы, куда оно входит в качестве составной части. Изучением вопросов, связанных с разработкой и эксплуатацией программно-аппаратных систем, занимается системная инженерия. В ее рамки попадает огромное количество проблем, связанных с аппаратной частью систем и обеспечением нужного уровня интеграции программной и аппаратной составляющих. Мы только изредка будем затрагивать вопросы, касающиеся системной инженерии в целом, в основном ограничиваясь аспектами, относящимися непосредственно к ПО.
В данном курсе будут рассматриваться различные подходы к решению проблем разработки, связанных с обеими составляющими дилеммы "ресурсы–качество" при создании
Кроме того, особое внимание в курсе уделяется одному из подходов к разработке
Проблемы, связанные с управлением ресурсами разработки, в том числе, с планированием отдельных действий во времени, созданием эффективных команд разработчиков, относятся к управлению проектами, которому будет посвящена последняя лекция курса.
На основании опыта конструирования больших систем разработаны так называемые технологические процессы, содержащие достаточно детальные описания разных аспектов их создания и эксплуатации. Эти описания дают ответы на вопросы о том, как должна вестись разработка, какие лица должны в ней участвовать и на каких этапах, какие виды деятельности и в какой последовательности должны выполняться, какие документы являются их входными данными и какие документы, модели, другие части программной системы должны быть подготовлены в результате каждой отдельной работы. Элементы таких методик будут упоминаться на всем протяжении курса. Также будут рассматриваться отраслевые стандарты, содержащие описание выработанных на основе большого количества реальных проектов подходов к построению
При практической разработке больших систем, однако, стоит помнить, что все общеметодические рекомендации имеют границы применимости, и чем детальнее они определяют действия разработчиков, тем вероятнее, что что-то пойдет не так, как это предусматривается авторами методик. Кроме того, огромное количество вспомогательных по сути документов, оформление которых часто требуется подобными методиками, иногда затрудняет понимание основных целей проекта, принимаемых в его ходе решений и сути происходящего в нем. Оно также может приводить к имитации усердной работы при отсутствии реальных продвижений к нужным результатам.
Протест сообщества разработчиков против подобной бюрократизации разработки программ и попыток механического использования теоретических рекомендаций вылился в популярное сейчас движение живой разработки ПО (Agile Software Development). Одним из примеров "живого" процесса разработки является набор техник, известный как экстремальное программирование (
Помимо методических рекомендаций, при конструировании больших систем часто используются прагматические принципы работы со
Абстракция является универсальным подходом к рассмотрению сложных вещей. Интеллект одного человека достаточно ограничен и просто не в силах иметь дело сразу со всеми элементами и свойствами систем большой сложности. Известно, что человеку крайне тяжело держать в голове одновременно десяток-полтора различных мыслей, а в современных системах число различных существенных аспектов доходит до сотен. Для того чтобы каким-либо образом работать с такими системами, мы пользуемся своей возможностью абстрагироваться, т.е. отвлекаться от всего, что несущественно для достижения поставленной в данной момент частной цели и не влияет на те аспекты рассматриваемого предмета, которые для этой цели важны.
Чтобы перейти от абстрактного представления к более конкретному, используется обратный процесс последовательного уточнения. Рассмотрев систему в каждом аспекте в отдельности, мы пытаемся объединить результаты анализа, добавляя аспекты по одному и обращая при этом внимание на возможные взаимные влияния и возникающие связи между элементами, выявленными при анализе отдельных аспектов.
Систему хранения идентификаторов пользователей Интернет-магазина можно представить как множество целых чисел, забыв о том, что эти числа — идентификаторы пользователей, и о том, что все это как-то связано с Интернет-магазином.
Затем описанную модель системы хранения идентификаторов пользователей Интернет-магазина можно уточнить, определив конкретную реализацию множества чисел, например, на основе сбалансированных красно-черных деревьев (см. [2], раздел 14, глава III и JDK классы java.util.TreeSet и java.util.TreeMap ).
Рассматривая задачу передачи данных по сети, можно временно абстрагироваться от большинства проблем организации связи и заниматься только одним аспектом — организацией надежной передачи данных в нужной последовательности. При этом можно предполагать, что мы каким-нибудь образом умеем передавать данные между двумя компьютерами в сети, хотя, быть может, и с потерями и с нарушением порядка их прибытия по сравнению с порядком отправки. Установленные ограничения выделяют достаточно узкий набор задач. Любое их решение представляет собой некоторый протокол передачи данных транспортного уровня, т.е. нацеленный именно на надежную упорядоченную доставку данных. Выбирая такой протокол из уже существующих, например, TCP, или разрабатывая новый, мы производим уточнение исходной общей задачи передачи данных.
Другой способ уточнения — перейти к рассмотрению протоколов, обеспечивающих исходные условия для нашей первой абстракции, т.е. возможность вообще что-то передавать по сети. При этом возникают протоколы нижележащих уровней — сетевого (отвечают за организацию связи между не соединенными непосредственно компьютерами при наличии между ними цепи машин, соединенных напрямую), канального (такие протоколы отвечают за определение формата передаваемых данных и надежность передачи отдельных элементов информации между двумя физически соединенными компьютерами) и физического (отвечают за определение физического носителя передаваемого сигнала и правильность интерпретации таких сигналов обеими машинами, в частности, за конкретный способ передачи битов с помощью электрических сигналов или радиоволн).
Модульность — принцип организации больших систем в виде наборов подсистем, модулей или компонентов. Этот принцип предписывает организовывать
Выделение четких интерфейсов для взаимодействия упрощает интеграцию, позволяя проводить ее на основе явно очерченных возможностей этих интерфейсов, без обращения к многочисленным внутренним элементам модулей, что привело бы к росту сложности.
Примером разбиения на модули может служить структура пакетов и классов библиотеки JDK. Классы, связанные с основными сущностями языка Java и виртуальной машины, собраны в пакете java.lang. Вспомогательные широко применяемые в различных приложениях классы, такие как коллекции, представления даты и пр., собраны в java.util. Классы, используемые для реализации потокового ввода-вывода — в пакете java.io, и т.д.
Интерфейсом класса служат его общедоступные методы, а интерфейсом пакета — его
Другой пример модульности — принятый способ организации протоколов передачи данных. Мы уже видели, что удобно выделять несколько уровней протоколов, чтобы на каждом решать свои задачи. При этом надо определить, как информация передается от машины к машине при помощи всего этого многоуровневого механизма. Обычное решение таково: для каждого уровня определяется способ передачи информации с верхнего уровня или на верхний уровень — предоставляемые данным уровнем службы. Точно так же определяется, в каких службах нижнего уровня нуждается верхний, т.е. как передать данные на нижний уровень и получить их оттуда. После этого каждый протокол на данном уровне может быть сформулирован в терминах обращений к нижнему уровню и должен реализовать операции-службы, необходимые верхнему. Это позволяет заменять протокол-модуль на одном уровне без внесения изменений в другие.
Хорошее разбиение системы на модули — непростая задача. При ее выполнении привлекаются следующие дополнительные принципы.
Модули должны взаимодействовать друг с другом через четко определенные интерфейсы и скрывать друг от друга внутреннюю информацию — внутренние данные, детали реализации интерфейсных операций.
При этом интерфейс модуля обычно значительно меньше, чем набор всех операций и данных в нем.
Например, класс java.util.Queue<type E>, реализующий функциональность очереди элементов типа E, имеет следующий интерфейс.
E element() |
Возвращает элемент, стоящий в голове очереди, не изменяя ее. Создает исключение NoSuchElementException, если очередь пуста |
boolean |
Вставляет, если возможно, данный элемент в конец очереди. Возвращает true, если вставка прошла успешно, false — если иначе |
E |
Возвращает элемент, стоящий в голове очереди, не изменяя ее. Возвращает 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 { … }
}
Этот принцип объединяет ряд свойств, которыми должны обладать хорошо спроектированные интерфейсы.
Например, добавление в интерфейс очереди метода, позволяющего получить любой ее элемент по его номеру в очереди, сделало бы этот интерфейс не вполне адекватным — он превратился бы почти в интерфейс списка, который используется для решения других задач. Очереди же используются там, где полная функциональность списка не нужна, а реализация очереди может быть сделана более эффективной.
Например, отсутствие в интерфейсе очереди метода сделало бы его бесполезным — никому не нужна очередь, из которой можно брать элементы, а класть в нее ничего нельзя.
Более тонкий пример — методы element() и . Нужда в них возникает, если программа не должна изменять очередь, и в то же время ей нужно узнать, какой элемент лежит в ее начале. Отсутствие такой возможности потребовало бы создавать собственное дополнительное хранилище элементов в каждой такой программе.
Представленный в примере интерфейс очереди не минимален — методы element() и , а также 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>. Первый из этих классов реализует список на основе массива, а второй — на основе ссылочной структуры данных.
Оба эти принципа используются для
И наоборот, "сродство" функций, выполняемых одним модулем, должно быть как можно выше. Хотя на уровне кода причины этого "сродства" могут быть разными — работа с одними и теми же данными, зависимость от работы друг друга, необходимость синхронизации при параллельном выполнении и пр. — цена их разделения должна быть достаточно высокой. Наиболее существенно то, что эти функции решают тесно связанные друг с другом задачи.
Так, можно добавить в интерфейс очереди метод void println(String), отправляющий строку на стандартный вывод. Но он совсем не связан с остальными и с задачами, решаемыми очередью. Следовательно, трудоемкость анализа и внесения изменений в полученную систему будет значительно выше — ведь изменения в контексте разных задач возникают обычно независимо. Поэтому гораздо лучше поместить такой метод в другой модуль.
Этот принцип требует избегать повторений описаний одних и тех же знаний — в виде структур данных, действий, алгоритмов, одного и того же кода — в разных частях системы. Вместо этого в хорошо спроектированной системе выделяется один источник, одно место фиксации для каждого элемента знаний и организуется его
Примером может служить организация библиотечных классов java.util.TreeSet и java.util.TreeMap. Первый класс реализует хранение множества элементов, на которых определен порядок, в виде сбалансированного дерева. Второй класс реализует то же самое для ассоциативного массива или словаря (map), если определен порядок его ключей. Все алгоритмы работы со сбалансированным деревом в обоих случаях одинаковы, поэтому имеет смысл реализовать их только один раз. Если посмотреть на код этих классов в библиотеке JDK от компании Sun, можно увидеть, что ее разработчики так и поступили — класс TreeSet реализован как соответствующий ассоциативный массив TreeMap, в котором ключи представляют собой множество хранимых значений, а значение в любой паре (ключ, значение) равно null.
Основная тема данного курса — методы разработки "больших" и
Каждый человек, хоть раз написавший какую-либо программу, достаточно хорошо может представить себе, как разработать "небольшую" программу, решающую обычно одну конкретную несложную задачу и предназначенную, чаще всего, для использования одним человеком или узкой группой людей.
Примером может служить программа, вычисляющая достаточно много (но не слишком, не больше 30000) знаков числа $$\pi$$.
Воспользуемся следующими формулами:
$$\arctan(x) = x – x^{3}/3 + x^{5}/5 – x^{7}/7 + … + (-1)^{n}x^{2n+1}/(2n+1) + O(x^{2n+3}) \\ \pi /4 = \arctan(1) = 4*\arctan(1/5) – \arctan(1/239)$$Соответсвующая программа на языке Java может выглядеть примерно так:
public class PiCalculator
{
//Позволяет при вычислениях с повышенной точностью умножать и делить на числа
// <= 42949 = ( 2^32 mod CLUSTER_SIZE )
//Эта константа должна быть степенью 10 для простоты представления чисел.
private final static long CLUSTER_SIZE = 100000;
//Определенное значение этого поля позволяет сосчитать
// numberOfClusters * lg( CLUSTER_SIZE )
//точных цифр.
private static int numberOfClusters;
private static void print(long a[])
{
for(int i = 0; i < numberOfClusters + 1; i++)
{
if (i == 0) System.out.print("" + a[i] + '.');
else
{
StringBuffer s = new StringBuffer();
long z = CLUSTER_SIZE/10;
while(z > 0)
{
if (z > a[i]) { s.append(0); z /= 10; }
else break;
}
if (z != 0) s.append(a[i]);
System.out.print(s);
}
}
System.out.println();
}
private static void lndiv(long a[], int n)
{
for(int i = 0; i < numberOfClusters + 1; i++)
{
if (i != numberOfClusters)
{
a[i+1] += (a[i]%n)*CLUSTER_SIZE;
a[i] /= n;
}
else a[i] /= n;
}
}
private static void lnadd(long a[], long b[])
{
for(int i = numberOfClusters; i >= 0; i--)
{
if (i != 0)
{
a[i-1] += (a[i] + b[i])/CLUSTER_SIZE;
a[i] = (a[i] + b[i])%CLUSTER_SIZE;
}
else
a[i] = (a[i] + b[i])%CLUSTER_SIZE;
}
}
private static void lnsub(long a[], long b[])
{
for(int i = numberOfClusters; i >= 0; i--)
{
if (i != 0)
{
if (a[i] < b[i]) { b[i-1]++; a[i] += CLUSTER_SIZE; }
a[i] -= b[i];
}
else
a[i] -= b[i];
}
}
public static void main (String[] args)
{
int i, j, numberOfDigits = 100, numberOfSteps;
if (args.length > 0) numberOfDigits = Integer.parseInt(args[0]);
numberOfSteps = (int)(((numberOfDigits + 1)/(Math.log(5)/Math.log(10)) - 1)/2+1);
numberOfClusters = (int)(numberOfDigits/(Math.log(CLUSTER_SIZE)/Math.log(10))+1);
long a1[] = new long[numberOfClusters + 1];
long b1[] = new long[numberOfClusters + 1];
long c1[] = new long[numberOfClusters + 1];
long a2[] = new long[numberOfClusters + 1];
long b2[] = new long[numberOfClusters + 1];
long c2[] = new long[numberOfClusters + 1];
a1[0] = 16;
a2[0] = 4;
lndiv(a1, 5);
lndiv(a2, 239);
System.arraycopy(a1, 0, c1, 0, numberOfClusters + 1);
System.arraycopy(a2, 0, c2, 0, numberOfClusters + 1);
for(j = 1; j < numberOfSteps; j++)
{
lndiv(a1, 25);
lndiv(a2, 239);
lndiv(a2, 239);
System.arraycopy(a1, 0, b1, 0, numberOfClusters + 1);
System.arraycopy(a2, 0, b2, 0, numberOfClusters + 1);
lndiv(b1, 2*j+1);
lndiv(b2, 2*j+1);
if (j%2 == 0) { lnadd(c1, b1); lnadd(c2, b2); }
else { lnsub(c1, b1); lnsub(c2, b2); }
}
lndiv(a1, 25);
lndiv(a1, 2*numberOfSteps + 1);
System.out.println("Оценка точности результата:");
print(a1);
lnsub(c1, c2);
System.out.println("Результат:");
print(c1);
}
}
Данная программа — "небольшая", как по размерам (~150 строк), так и по другим признакам:
Обычно
Примером "большой" программы может служить стандартная библиотека классов Java, входящая в
Строго говоря, ни одно из указанных свойств не является обязательным для того, чтобы программу можно было считать "большой", но при наличии двух-трех из них достаточно уверенно можно утверждать, что она "большая".
На основании некоторых из перечисленных свойств можно сделать вывод, что "большая" программа или программная система чаще всего представляет собой не просто код или исполняемый файл, а включает еще и набор проектной и пользовательской документации.
Для разработки программных систем требуются особые методы — как уже говорилось, их нельзя написать "нахрапом". Изучением организационных, инженерных и технических аспектов создания ПО, включая методы разработки, занимается дисциплина, называемая
Важно отметить, что практически полезная
Большинство опытных разработчиков и исследователей считают, что практически значимые программные системы всегда содержат ошибки. При переходе от "небольших" программ к "большим" понятие "правильной" программы становится практически бессмысленным. Говоря о программной системе, (в отличие от приведенной выше программы вычисления числа?), нельзя утверждать, что она "правильная", т.е. всегда правильно решает все поставленные перед ней задачи. Этот факт связан как с практической невозможностью полного доказательства или проверки этого, так и с тем, что смысл существования программной системы — удовлетворение потребностей и запросов большого количества различных заинтересованных лиц. А эти потребности не только нечетко определены, различны для разных групп пользователей и иногда противоречивы, но и значительно изменяются с течением времени.
В связи с этим, вместо рассмотрения "правильных" и "неправильных" программных систем, в силу практического отсутствия первых, рассматривают "достаточно качественные" и "недостаточно качественные".
Поэтому и основные проблемы разработки
Часто программное обеспечение (ПО) нельзя рассматривать отдельно от программно-аппаратной системы, куда оно входит в качестве составной части. Изучением вопросов, связанных с разработкой и эксплуатацией программно-аппаратных систем, занимается системная инженерия. В ее рамки попадает огромное количество проблем, связанных с аппаратной частью систем и обеспечением нужного уровня интеграции программной и аппаратной составляющих. Мы только изредка будем затрагивать вопросы, касающиеся системной инженерии в целом, в основном ограничиваясь аспектами, относящимися непосредственно к ПО.
В данном курсе будут рассматриваться различные подходы к решению проблем разработки, связанных с обеими составляющими дилеммы "ресурсы–качество" при создании
Кроме того, особое внимание в курсе уделяется одному из подходов к разработке
Проблемы, связанные с управлением ресурсами разработки, в том числе, с планированием отдельных действий во времени, созданием эффективных команд разработчиков, относятся к управлению проектами, которому будет посвящена последняя лекция курса.
На основании опыта конструирования больших систем разработаны так называемые технологические процессы, содержащие достаточно детальные описания разных аспектов их создания и эксплуатации. Эти описания дают ответы на вопросы о том, как должна вестись разработка, какие лица должны в ней участвовать и на каких этапах, какие виды деятельности и в какой последовательности должны выполняться, какие документы являются их входными данными и какие документы, модели, другие части программной системы должны быть подготовлены в результате каждой отдельной работы. Элементы таких методик будут упоминаться на всем протяжении курса. Также будут рассматриваться отраслевые стандарты, содержащие описание выработанных на основе большого количества реальных проектов подходов к построению
При практической разработке больших систем, однако, стоит помнить, что все общеметодические рекомендации имеют границы применимости, и чем детальнее они определяют действия разработчиков, тем вероятнее, что что-то пойдет не так, как это предусматривается авторами методик. Кроме того, огромное количество вспомогательных по сути документов, оформление которых часто требуется подобными методиками, иногда затрудняет понимание основных целей проекта, принимаемых в его ходе решений и сути происходящего в нем. Оно также может приводить к имитации усердной работы при отсутствии реальных продвижений к нужным результатам.
Протест сообщества разработчиков против подобной бюрократизации разработки программ и попыток механического использования теоретических рекомендаций вылился в популярное сейчас движение живой разработки ПО (Agile Software Development). Одним из примеров "живого" процесса разработки является набор техник, известный как экстремальное программирование (
Помимо методических рекомендаций, при конструировании больших систем часто используются прагматические принципы работы со
Абстракция является универсальным подходом к рассмотрению сложных вещей. Интеллект одного человека достаточно ограничен и просто не в силах иметь дело сразу со всеми элементами и свойствами систем большой сложности. Известно, что человеку крайне тяжело держать в голове одновременно десяток-полтора различных мыслей, а в современных системах число различных существенных аспектов доходит до сотен. Для того чтобы каким-либо образом работать с такими системами, мы пользуемся своей возможностью абстрагироваться, т.е. отвлекаться от всего, что несущественно для достижения поставленной в данной момент частной цели и не влияет на те аспекты рассматриваемого предмета, которые для этой цели важны.
Чтобы перейти от абстрактного представления к более конкретному, используется обратный процесс последовательного уточнения. Рассмотрев систему в каждом аспекте в отдельности, мы пытаемся объединить результаты анализа, добавляя аспекты по одному и обращая при этом внимание на возможные взаимные влияния и возникающие связи между элементами, выявленными при анализе отдельных аспектов.
Систему хранения идентификаторов пользователей Интернет-магазина можно представить как множество целых чисел, забыв о том, что эти числа — идентификаторы пользователей, и о том, что все это как-то связано с Интернет-магазином.
Затем описанную модель системы хранения идентификаторов пользователей Интернет-магазина можно уточнить, определив конкретную реализацию множества чисел, например, на основе сбалансированных красно-черных деревьев (см. [2], раздел 14, глава III и JDK классы java.util.TreeSet и java.util.TreeMap ).
Рассматривая задачу передачи данных по сети, можно временно абстрагироваться от большинства проблем организации связи и заниматься только одним аспектом — организацией надежной передачи данных в нужной последовательности. При этом можно предполагать, что мы каким-нибудь образом умеем передавать данные между двумя компьютерами в сети, хотя, быть может, и с потерями и с нарушением порядка их прибытия по сравнению с порядком отправки. Установленные ограничения выделяют достаточно узкий набор задач. Любое их решение представляет собой некоторый протокол передачи данных транспортного уровня, т.е. нацеленный именно на надежную упорядоченную доставку данных. Выбирая такой протокол из уже существующих, например, TCP, или разрабатывая новый, мы производим уточнение исходной общей задачи передачи данных.
Другой способ уточнения — перейти к рассмотрению протоколов, обеспечивающих исходные условия для нашей первой абстракции, т.е. возможность вообще что-то передавать по сети. При этом возникают протоколы нижележащих уровней — сетевого (отвечают за организацию связи между не соединенными непосредственно компьютерами при наличии между ними цепи машин, соединенных напрямую), канального (такие протоколы отвечают за определение формата передаваемых данных и надежность передачи отдельных элементов информации между двумя физически соединенными компьютерами) и физического (отвечают за определение физического носителя передаваемого сигнала и правильность интерпретации таких сигналов обеими машинами, в частности, за конкретный способ передачи битов с помощью электрических сигналов или радиоволн).
Модульность — принцип организации больших систем в виде наборов подсистем, модулей или компонентов. Этот принцип предписывает организовывать
Выделение четких интерфейсов для взаимодействия упрощает интеграцию, позволяя проводить ее на основе явно очерченных возможностей этих интерфейсов, без обращения к многочисленным внутренним элементам модулей, что привело бы к росту сложности.
Примером разбиения на модули может служить структура пакетов и классов библиотеки JDK. Классы, связанные с основными сущностями языка Java и виртуальной машины, собраны в пакете java.lang. Вспомогательные широко применяемые в различных приложениях классы, такие как коллекции, представления даты и пр., собраны в java.util. Классы, используемые для реализации потокового ввода-вывода — в пакете java.io, и т.д.
Интерфейсом класса служат его общедоступные методы, а интерфейсом пакета — его
Другой пример модульности — принятый способ организации протоколов передачи данных. Мы уже видели, что удобно выделять несколько уровней протоколов, чтобы на каждом решать свои задачи. При этом надо определить, как информация передается от машины к машине при помощи всего этого многоуровневого механизма. Обычное решение таково: для каждого уровня определяется способ передачи информации с верхнего уровня или на верхний уровень — предоставляемые данным уровнем службы. Точно так же определяется, в каких службах нижнего уровня нуждается верхний, т.е. как передать данные на нижний уровень и получить их оттуда. После этого каждый протокол на данном уровне может быть сформулирован в терминах обращений к нижнему уровню и должен реализовать операции-службы, необходимые верхнему. Это позволяет заменять протокол-модуль на одном уровне без внесения изменений в другие.
Хорошее разбиение системы на модули — непростая задача. При ее выполнении привлекаются следующие дополнительные принципы.
Модули должны взаимодействовать друг с другом через четко определенные интерфейсы и скрывать друг от друга внутреннюю информацию — внутренние данные, детали реализации интерфейсных операций.
При этом интерфейс модуля обычно значительно меньше, чем набор всех операций и данных в нем.
Например, класс java.util.Queue<type E>, реализующий функциональность очереди элементов типа E, имеет следующий интерфейс.
E element() |
Возвращает элемент, стоящий в голове очереди, не изменяя ее. Создает исключение NoSuchElementException, если очередь пуста |
boolean |
Вставляет, если возможно, данный элемент в конец очереди. Возвращает true, если вставка прошла успешно, false — если иначе |
E |
Возвращает элемент, стоящий в голове очереди, не изменяя ее. Возвращает 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 { … }
}
Этот принцип объединяет ряд свойств, которыми должны обладать хорошо спроектированные интерфейсы.
Например, добавление в интерфейс очереди метода, позволяющего получить любой ее элемент по его номеру в очереди, сделало бы этот интерфейс не вполне адекватным — он превратился бы почти в интерфейс списка, который используется для решения других задач. Очереди же используются там, где полная функциональность списка не нужна, а реализация очереди может быть сделана более эффективной.
Например, отсутствие в интерфейсе очереди метода сделало бы его бесполезным — никому не нужна очередь, из которой можно брать элементы, а класть в нее ничего нельзя.
Более тонкий пример — методы element() и . Нужда в них возникает, если программа не должна изменять очередь, и в то же время ей нужно узнать, какой элемент лежит в ее начале. Отсутствие такой возможности потребовало бы создавать собственное дополнительное хранилище элементов в каждой такой программе.
Представленный в примере интерфейс очереди не минимален — методы element() и , а также 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>. Первый из этих классов реализует список на основе массива, а второй — на основе ссылочной структуры данных.
Оба эти принципа используются для
И наоборот, "сродство" функций, выполняемых одним модулем, должно быть как можно выше. Хотя на уровне кода причины этого "сродства" могут быть разными — работа с одними и теми же данными, зависимость от работы друг друга, необходимость синхронизации при параллельном выполнении и пр. — цена их разделения должна быть достаточно высокой. Наиболее существенно то, что эти функции решают тесно связанные друг с другом задачи.
Так, можно добавить в интерфейс очереди метод void println(String), отправляющий строку на стандартный вывод. Но он совсем не связан с остальными и с задачами, решаемыми очередью. Следовательно, трудоемкость анализа и внесения изменений в полученную систему будет значительно выше — ведь изменения в контексте разных задач возникают обычно независимо. Поэтому гораздо лучше поместить такой метод в другой модуль.
Этот принцип требует избегать повторений описаний одних и тех же знаний — в виде структур данных, действий, алгоритмов, одного и того же кода — в разных частях системы. Вместо этого в хорошо спроектированной системе выделяется один источник, одно место фиксации для каждого элемента знаний и организуется его
Примером может служить организация библиотечных классов java.util.TreeSet и java.util.TreeMap. Первый класс реализует хранение множества элементов, на которых определен порядок, в виде сбалансированного дерева. Второй класс реализует то же самое для ассоциативного массива или словаря (map), если определен порядок его ключей. Все алгоритмы работы со сбалансированным деревом в обоих случаях одинаковы, поэтому имеет смысл реализовать их только один раз. Если посмотреть на код этих классов в библиотеке JDK от компании Sun, можно увидеть, что ее разработчики так и поступили — класс TreeSet реализован как соответствующий ассоциативный массив TreeMap, в котором ключи представляют собой множество хранимых значений, а значение в любой паре (ключ, значение) равно null.
Для получения официальных документов о завершении программы дополнительного профессионального образования (удостоверения о повышении квалификации, дипломов о профессиональной переподготовке и MBA) необходимо предоставить:
Внимание! Вы можете не заказывать доставку бумажной версии официального документы, а скачать его в электронном виде и распечатать самостоятельно. Информация о выданном документе в течение 1 месяца загружается в Федеральную информационную систему «Федеральный реестр сведений о документах об образовании и (или) о квалификации, документах об обучении» - ФИС ФРДО.
Доступ на новый сайт осуществляется с использованием адреса электронной почты, который был указан вами при регистрации на "старом". Мы постарались перенести все ваши данные с прежнего ресурса, однако не исключена вероятность потери части информации.
При возникновении проблемы со входом, воспользуйтесь функцией сброса пароля
Если вы обнаружите несоответствия, пожалуйста, сообщите нам.