- •3 Системное программное обеспечение.
- •Инструментарий технологии программирования.
- •Пакеты прикладных программ.
- •Суть поиска подходящих объектов при проектировании.
- •Специфицирование интерфейсов объекта.
- •Специфицирование реализации объектов.
- •Механизмы повторного использования.
- •Сравнение структур времени выполнения и времени компиляции.
- •Проектирование с учетом будущих изменений
- •Прикладные программы. Инструментальные библиотеки. Каркасы приложений.
- •Признаки плохого проекта.
- •Принцип персональной ответственности
- •15. Принцип открытия – закрытия (оср)
- •17. Принцип инверсии зависимостей.
- •Обратное влияние клиентов на интерфейсы
15. Принцип открытия – закрытия (оср)
Программные объекты (классы, модули, функции и т.д.) должны быть открыты для расширения, но в то же время закрыты для модификации. Когда одно изменение в программе вызывает каскад изменений в зависимых модулях, проект начинает проявлять признаки закрепощенности. Модули, соответствующие принципу открытия-закрытия, имеют два основных атрибута (признака). 1."Открыто для расширения". Это означает, что поведение модуля может быть расширено. По мере изменения требований приложения можно расширить модуль за счет включения новых типов поведения, соответствующих этим изменениям. Другими словами, можно изменить то, что делает модуль. 2."Закрыто для модификации". В результате расширения поведения модуля изменения в исходном или двоичном коде модуля не производятся. Двоичная исполняемая версия модуля, будь то в связанной библиотеке DLL, или Java .jar, остается неизменной.
На первый взгляд может показаться, что два описанных атрибута находятся в неравном положении по отношению друг к другу. Обычно расширение поведения модуля имеет место в результате изменения исходного кода этого модуля. Модуль, который невозможно изменить, обычно считается модулем с фиксированным поведением. На рис. 1 представлен простой проект, не соответствующий принципу открытия-закрытия. Классы Client и Server определены. Класс Client использует класс Server. Если бы объекту Client потребовалось использовать другой объект сервера, то класс клиента нужно было бы изменить, чтобы назвать новый класс сервера.
Client |
|
Server |
|
На рис.2 представлен подходящий проект, соответствующий принципу открытия-закрытия. В этом случае класс Clientlnterface — это абстрактный класс с абстрактными функциями-членами. Клиентский класс использует эту абстракцию; тем не менее, объекты класса Client будут использовать объекты производного класса Server. Клиенту необходимо выполнить определенную работу, и он может описать эту работу в рамках абстрактного интерфейса, представленного объектом Clientlnterface. Подтипы Clientlnterface могут использовать этот интерфейс любым выбранным им способом. Таким образом, поведение, определенное в клиенте, может быть расширено и модифицировано путем создания новых подтипов клиентского интерфейса.
|
|
|
|
|
|
|
|
|
Client |
> |
«interface» Client Interface |
|
Policy |
|
|
|
|
+ PolicyFunctionO - ServiceFunction{) |
|
||||
|
|
|
A |
|
|
||
|
|
|
|
A |
|
||
|
|
|
Implementation |
|
|||
|
|
|
- ServiceFunction() |
|
В языке С при использовании процедурных методик, не соответствующих принципу открытия-закрытия, возникшая проблема решается способом, представленным в листинге. Первый элемент каждого набора — это код типа, определяющий структуру данных как окружность либо как квадрат. Функция DrawAllShapes выполняет обход массива указателей для этих структур данных, проверяя тип кода, а также выполняя вызов соответствующей функции.
enum ShapeType {circle, square};
struct Shape {
ShapeType itsType;
}
—circle.h
struct Circle {
ShapeType itsType;
double itsRadius;
Point itsCenter;
};
void DrawCircle(struct Circle*);
—square.h
struct Square {
ShapeType itsType;
double itsSide;
Point itsTopLeft;
};
void DrawSquare(struct Square*);
—drawAllShapes.cc
typedef struct Shape *ShapePointer;
void DrawAllShapes(ShapePointer list[], int n)
{
int i;
for (i=0; i<n; i++)
{
struct Shape* s = list[i];
switch (s->itsType)
{
case square:
DrawSquare((struct Square*)s);
break;
case circle:
DrawCircle((struct Circle*)s);: break;
} }
Функция DrawAllShapes не соответствует принципу открытия-закрытия, поскольку не может быть закрыта для новых типов форм. Если бы потребовалось расширить эту функцию, чтобы она могла нарисовать список форм, включающих треугольники, то эту функцию пришлось бы изменить.
Принцип подстановки Лискоу.
Принцип LSP может быть сформулирован следующим образом. ПОДТИПЫ ДОЛЖНЫ БЫТЬ ЗАМЕНЯЕМЫ ИХ ИСХОДНЫМИ ТИПАМИ. Значимость этого правила становится очевидной в случае, если рассмотреть последствия его нарушений. Предположим, что у нас есть функция /, содержащая в качестве аргумента указатель или ссылку на базовый класс В. Также представим, что существует производная от В (сокращенно D), которая при подстановке в функцию / под видом В вызывает изменения в поведении последней. В этом случае D игнорирует принцип LSP. Очевидно, что D представляет собой одну из "неустойчивостей" /. Разработчики функции / могут подвергнуться искушению провести определенные тесты, которые покажут что поведение функции не изменяется при подстановке в нее производной функции D. Такое тестирование отрицает основные принципы ОСР, поскольку оно не охватывает весь диапазон значений производных от В. Нарушение принципа LSP зачастую приводит к тому, что информация о типах в процессе исполнения (RTTI) применяется в стиле, не соответствующем принципам ОСР. Обычно в таких случаях для определения подходящего типа объекта (не вызывающего изменения в поведении нужной функции) используются операторы if или последовательности операторов if/else. struct Point {double х,у;};
struct Shape { enum ShapeType {square, circle) itsType; Shape(ShapeType t) : Shape(ShapeType t) : itsType(t) {} }; struct Circle : public Shape { Circle() : Shape(circle) {}; void Draw() const; Point itsCenter; double itsRadius; }; struct Square : public Shape {Square() : Shape(square) {}; void Draw() const; Point itsTopLeft; double itsSide; }; void DrawShape(const ShapesS s) {if (s.itsType == Shape::square) static_cast<const Square&>(s).Drawf() else if (s.itsType == Shape::circle) static_cast<const Circle&>(s).Draw();
что функция DrawShape, код которой приведен в листинге , противоречит принципам ОСР. Она должна работать с любыми производными классами Shape и изменяться при появлении новых таких производных классов. В действительности же пристальное рассмотрение данной функции сразу выявляет ее "слабые места". Существуют и менее явные случаи нарушения принципа LSP. Рассмотрим приложение, в котором используется класс Rectangle.
class Rectangle
{public: void SetWidth(double w) {itsWidth=w;} void SetHeight(double h) {itsHeight=w;} double GetHeightO const {return itsHeight;} double GetWidthO const {return itsWidth;} private: Point itsTopLeft; double itsWidth; double itsHeight;);
Представим себе, что это приложение работает вполне корректно и установлено на многих Web-узлах. Как часто бывает при разработке популярного ПО, пользователи время от времени требуют изменить отдельные компоненты программы. Допустим, что в один прекрасный день пользователям потребовалась возможность манипулировать квадратами (название класса Rectangle на русский язык переводится как "прямоугольник") в дополнение к возможности работы с прямоугольниками. По всем правилам и во всех случаях квадрат представляет собой прямоугольник. Поэтому логично рассматривать класс Square как производный от класса Rectangle. Квадрат представляет собой частный случай прямоугольника, поэтому класс Square должен быть производным от класса Rectangle. Однако такой способ мышления может привести к некоторым незаметным на первый взгляд, но довольно серьезным проблемам. Как правило, эти проблемы нельзя предусмотреть заранее, до тех пор, пока они не "проявятся" в программном коде. Принцип LSP подводит нас к очень важному заключению. Модель, рассматриваемая отдельно от общей структуры, не может быть однозначно оценена в плане своей пригодности. Пригодность конкретной модели может быть определена только в отношении ее клиентов/области использования.