Фундаментальные принципы объектно ориентированного проектирования (Часть 1): Абстракция
Вольный перевод статьи Fundamental Object Oriented Design principles (Part 1): Abstractionopen in new window
Рассмотрим принцип Абстракции используемый в объектно ориентированных языках программирования.
Это первая часть серии об объектно ориентированном проектировании. Для того чтобы понять почему изучение шаблонов проектирования важно, мы должны начать с рассмотрения того что такое хороший объектно ориентированный дизайн приложения.
ОО-языки предоставляют уникальные возможности, которые содействуют хорошему дизайну приложения. Рассматриваемые принципы проектирования могут быть применены и для не объектно ориентированных языков, но с большими сложностями.
Эта статья предполагает что вы знакомы как минимум с одним ОО языком программирования. C++, Delphi, C# и Java являются примерами объектно ориентированных языков. Настоящий ОО язык программирования имеет классы, объекты и очень часто интерфейсы.
Что такое Абстракция?
Абстракция, в общем смысле, означает оперирование кодом и структурой данных на высоком уровне, без необходимости понимания конкретной природы структуры данных. Ниже я опишу конкретные формы абстракции лежащие в основе ООП. Большинство из них должны быть знакомы вам, но я перечислю их в явной форме для того чтобы ссылаться на них из более сложных принципов проектирования в будущих постах.
Взаимодействие с объектами используя информацию описанную в классе
Объектно ориентированные языки программирования содержат концепцию классов. Класс — это определение, которое используется для создания объектов. Мы можем оперировать объектом через свойства и методы объявленные в классе, без понимания их действительной реализации. Также мы можем ожидать одинакового поведения от объектов одного и того же класса.
var
LCircle: TCircle;
LArea: double;
begin
LCircle := TCircle.Create; // Мы берём класс TCircle и создаём на основе него объект
// Мы можем устанавливать свойства в то время как объект будет сам обрабатывать побочные эффекты
LCircle.Radius := 3;
// Мы можем вызывать высокоуровневые методы объекта
LCircle.Translate(10, 15);
LArea := LCircle.GetArea();
end;
2
3
4
5
6
7
8
9
10
11
12
13
Пока никаких сюрпризов. Всё что нам нужно было определено в TCircle
и мы знаем что мы можем использовать свойства и методы класса который мы использовали для создания объекта.
Взаимодействие с объектами через информацию описанную в классе-предке
Объектно ориентированные языки программирования позволяют наследовать классы. При правильно ОО проектировании классы потомки к предкам находятся в отношении "является". Для примера мы можем наследовать классы TLine
и TArc
от класса TCurve
. В этом примере предполагаем, что TCurve определяет тип линии (LineType
).
procedure SetLinetype(ACurveList: TList<TCurve>; ALineType: Linetype_Enum); // Linetype_Enum не определено в этом посте
var
LCurve: TCurve;
begin
for LCurve in ACurveList do
LCurve.LineType := ALineType;
end;
2
3
4
5
6
7
В этом примере мы не беспокоимся о том содержит ли список линии (TLine
) или дуги (TArc
) или другие неизвестные типы. Нас волнует только чтобы объекты были типа TCurve
(или его наследники). Но нам не нужно знать действительно ли TCurve
сейчас является TArc
или TLine
.
Взаимодействие с объектом используя информацию описанную в виртуальном (или абстрактном) классе
Рассматривая класс TCurve
мы можем представить что он может быть абстракцией любого количества типов, не только TArc
и TLine
. Например, у нас может класс TSpline
или TElipseArc
которые тоже относятся через отношение "является" классу TCurve
как к предку. Если подумать о геометрической кривой в общем виде мы можем представить методы и свойства которые могут быть абстрагированы. Например, мы можем сказать "кривая может иметь только одну точку начала и одну точку окончания", так что мы можем объявить функцию которая возвращает начальную точку и конечную точку.
type
TCurve = class
public
function GetStartPoint: TPoint; virtual; abstract; // TPoint не определён в этом посте
end;
TArc = class(TCurve)
public
function GetStartPoint: TPoint; override;
end;
TLine = class(TCurve)
public
function GetStartPoint: TPoint; override;
end;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Ключевое слово virtual в Delphi означает, что даже если мы вызовем этот метод в родительском типе (TCurve
), то будет использоваться реализация наиболее специфичного класса (TArc
или TLine
). Потомки могут заменить реализацию на свою собственную. Мы рассмотрим это более подробно в концепции Полиморфизма в 3 части.
Для понимания давайте рассмотрим следующее
var
LCurve1, LCurve2, LCurve3: TCurve; // Все переменные объявлены с типом родителя
LStartPoint1, LStartPoint1, LStartPoint3: TPoint; // TPoint не опредлён в этом посте
begin
LCurve1 := nil;
LCurve2 := nil;
LCurve3 := nil;
try
LCurve1 := TArc.Create; // Создаются объекты типов потомков, но присваиваются переменной с типом предка
LCurve2 := TLine.Create;
LCurve3 := TCurve.Create; // Допустимая инструкция, так как класс не помечен как абстрактный, абстрактный только отдельный метод
LStartPoint1 := LCurve1.GetStartPoint; // Будет вызван TArc.GetStartPoint
LStartPoint1 := LCurve2.GetStartPoint; // Будет вызван TLine.GetStartPoint
// LStartPoint3 := LCurve3.GetStartPoint; // Будет вызван an abstract error
finally
LCurve3.Free;
LCurve2.Free;
LCurve1.Free;
end;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
При объявлении GetStartPoint
в классе TArc
мы использовали директиву override. Если затем мы ссылаемся на объект через переменную типа TCurve
и вызываем GetStartPoint
то будет вызван TArc.GetStartPoint
. Ключевое слово abstract в Delphi означает что если мы вызовем GetStartPoint
у потомка который не предоставляет реализацию то мы получим "Abstract Error", эта ошибка означает что метод не был переопределён в потомках. Если мы уберём ключевое слово abstract то нам нужно обязательно добавить базовую реализацию на уровне класса TCurve
. Если потомки не реализуют этот метод то будет вызываться базовая реализация без появления исключения.
Другой уровень абстракции - сделать весь класс абстрактным. Имеет смысл делать класс абстрактным если он настолько высоко в иерархии, что нет смысла создавать объекты этого класса. В данный момент проверки компилятара при создании объектов такого класса нет, но она может появится в будущих версиях.
TCurve = class abstract
public
function GetStartPoint: TPoint; virtual; abstract;
end;
2
3
4
Абстракция через интерфейсы
Существует заблуждение что интерфейсы это просто абстрактные классы. В случае классов между классом-потомком и классом-предком (включая абстрактные классы) существует отношение "является". Наиболее распространённое отношение между классом и интерфейсом — "поддерживает". Интерфейс может рассматриваться как контракт. Если класс поддерживает интерфейс, то класс берет на себя обязательство выполнить требования интерфейса, или делегировать их. Подробнее про делегирования интерфейсов будет в следующих статьях.
Интерфейсы даже более абстрактны чем абстрактные классы: один интерфейс может быть применён для нескольких иерархий классов одновременно. Можно даже создать свою иерархию наследования интерфейсов независящую от иерархии наследования классов к которым эти интерфейсы применяются. По сравнению с интерфейсами иерархии классов содержат структуры и функции которые трудно менять.
Рассмотрим пример который показывает отличия интерфейса от абстрактного класса.
IComparable = interface(IInterface)
function CompareTo(AObject: TObject): Integer;
end;
2
3
Очевидно что эта сущность не может войти не в одну конкретную иерархию классов. Этот интерфейс может быть применён к любому классу который потенциально можно сравнивать с другим классом. Фактически интерфейс IComparable
должен применяется ко многим иерархиям классов. Применённый к классу интерфейс заставляет реализовать метод в классе. Мы же ссылаемся на этот метод на высоком уровне абстракции интерфейса. Ниже пример процедуры которая сортирует любой список объектов которые поддерживают интерфейс IComparable
.
procedure SortComparableList(AList: TList<IComparable>);
procedure QuickSort(ALeftIdx, ARightIdx: Integer);
var
i, j : Integer;
LPivotItem: TObject;
LTempItem: IComparable;
begin
repeat
i := ALeftIdx;
j := ARightIdx;
LPivotItem := TObject(AList[(ALeftIdx + ARightIdx) shr 1]);
repeat
while AList[i].CompareTo(LPivotItem) < 0 do
Inc(i);
while AList[j].CompareTo(LPivotItem) > 0 do
Dec(j);
if i <= j then
begin
if (i <> j) then
begin
LTempItem := Items[i];
Items[i] := Items[j];
Items[j] := LTempItem;
end;
Inc(i);
Dec(j);
end;
until i > j;
if ALeftIdx < j then
QuickSort(ALeftIdx, j);
ALeftIdx := i;
until i >= ARightIdx;
end;
begin
if AList.Count > 1 then
QuickSort( 0, AList.Count - 1);
end;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
В итоге
Абстракция позволяет нам фокусироваться только на той области кода которая важна для нас. Она позволяет нам обобщать функциональность повышая читаемость и упрощая повторное использование кода. Также она упрощает понимание того что происходит в программе на высоком уровне.