Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
ТП шпоры.docx
Скачиваний:
10
Добавлен:
20.09.2019
Размер:
115.03 Кб
Скачать

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 не соответствует принципу открытия-закрытия, поскольку не может быть закрыта для новых типов форм. Если бы потребовалось расширить эту функцию, чтобы она могла нарисовать список форм, включающих треугольники, то эту функцию пришлось бы изменить.

  1. Принцип подстановки Лискоу.

Принцип 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 подводит нас к очень важному заключению. Модель, рассматриваемая отдельно от общей структуры, не может быть однозначно оценена в плане своей пригодности. Пригодность конкретной модели может быть определена толь­ко в отношении ее клиентов/области использова­ния.