8 страница из 13
Тема
ma точно описывает окружающий мир. Каждый раз, делая шаг, мы ставим свои здоровье и безопасность на верность формулы F = Gm1m2/r2.

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

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

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

Тестирование

Однажды Дейкстра сказал: «Тестирование показывает присутствие ошибок, а не их отсутствие». Иными словами, тестированием можно доказать неправильность программы, но нельзя доказать ее правильность. Все, что дает тестирование после приложения достаточных усилий, — это уверенность, что программа действует достаточно правильно.

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

Такие доказательства неправильности можно применить только к доказуемым программам. Недоказуемую программу — например, из-за неумеренного использования goto — нельзя считать правильной, сколько бы тестов к ней ни применялось.

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

Заключение

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

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

Именно эти ограничения мы будем подробно изучать в последующих главах.

Глава 5. Объектно- ориентированное программирование


Как мы увидим далее, для создания хорошей архитектуры необходимо понимать и уметь применять принципы объектно-ориентированного программирования (ОО). Но что такое ОО?

Один из возможных ответов на этот вопрос: «комбинация данных и функций». Однако, несмотря на частое цитирование, этот ответ нельзя признать точным, потому что он предполагает, что o.f() — это нечто отличное от f(o). Это абсурд. Программисты передавали структуры в функции задолго до 1966 года, когда Даль и Нюгор перенесли кадр стека функции в динамическую память и изобрели ОО.

Другой распространенный ответ: «способ моделирования реального мира». Это слишком уклончивый ответ. Что в действительности означает «моделирование реального мира» и почему нам может понадобиться такое моделирование? Возможно, эта фраза подразумевает, что ОО делает программное обеспечение проще для понимания, потому что оно становится ближе к реальному миру, но и такое объяснение слишком размыто и уклончиво. Оно не отвечает на вопрос, что же такое ОО.

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

Давайте исследуем эти понятия по очереди.

Инкапсуляция?

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

Эта идея определенно не уникальная для ОО. Например, в языке C имеется превосходная поддержка инкапсуляции. Рассмотрим простую программу на C:


point.h

struct Point;

struct Point* makePoint(double x, double y);

double distance (struct Point *p1, struct Point *p2);


point.c

#include "point.h"

#include <stdlib.h>

#include <math.h>


struct Point {

  double x,y;

};


struct Point* makepoint(double x, double y) {

  struct Point* p = malloc(sizeof(struct Point));

  p->x = x;

  p->y = y;

  return p;

}


double distance(struct Point* p1, struct Point* p2) {

  double dx = p1->x - p2->x;

  double dy = p1->y - p2->y;

  return sqrt(dx*dx+dy*dy);

}

Пользователи point.h не имеют доступа к членам структуры Point. Они могут вызывать функции makePoint() и distance(), но не имеют никакого представления о реализации структуры Point и функций для работы с ней.

Это отличный пример поддержки инкапсуляции не в объектно-ориентированном языке. Программисты на C постоянно использовали подобные приемы. Мы можем объявить структуры данных и функции в заголовочных файлах и реализовать их в файлах реализации. И наши пользователи никогда не получат доступа к элементам в этих файлах реализации.

Но затем пришел объектно-ориентированный C++ и превосходная инкапсуляция в C оказалась разрушенной.

По техническим причинам[12] компилятор C++ требует определять переменные-члены класса в заголовочном файле. В результате объектно-ориентированная версия предыдущей программы Point приобретает такой вид:


point.h

class Point {

public:

  Point(double x, double y);

  double distance(const Point& p) const;

private:

  double x;

  double y;

};


point.cc

#include "point.h"

#include <math.h>


Point::Point(double x, double y)

: x(x), y(y)

{}


double Point::distance(const Point& p) const {

  double dx = x-p.x;

  double dy = y-p.y;

  return sqrt(dx*dx + dy*dy);

}

Теперь пользователи заголовочного файла point.h знают о переменных-членах x и y! Компилятор не позволит обратиться к ним непосредственно, но клиент все равно знает об их существовании. Например, если имена этих членов изменятся, файл point.cc придется скомпилировать заново! Инкапсуляция оказалась разрушенной.

Введением в язык ключевых слов public, private и protected инкапсуляция была частично восстановлена. Однако это был лишь грубый прием (хак), обусловленный технической необходимостью компилятора видеть все переменные-члены в заголовочном файле.

Языки Java и C# полностью отменили деление на заголовок/реализацию, ослабив инкапсуляцию еще больше. В этих языках невозможно разделить объявление и определение класса.

По описанным причинам трудно согласиться, что ОО зависит от строгой инкапсуляции. В действительности многие языки ОО практически не имеют принудительной инкапсуляции[13].

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

Наследование?

Языки ОО не улучшили инкапсуляцию, зато они дали нам наследование.

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

Взгляните на дополнение к нашей исходной программе point.h на языке C:


namedPoint.h

struct NamedPoint;


struct NamedPoint* makeNamedPoint(double x, double y, char* name);

void setName(struct NamedPoint* np, char* name);

char* getName(struct NamedPoint* np);


namedPoint.c

#include

Добавить цитату