Фундаментальные принципы объектно ориентированного проектирования (Часть 1): Абстракция

Вольный перевод статьи Fundamental Object Oriented Design principles (Part 1): Abstraction

Рассмотрим принцип Абстракции используемый в объектно ориентированных языках программирования.

Это первая часть серии об объектно ориентированном проектировании. Для того чтобы понять почему изучение шаблонов проектирования важно, мы должны начать с рассмотрения того что такое хороший объектно ориентированный дизайн приложения.

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

Эта статья предполагает что вы знакомы как минимум с одним ОО языком программирования. C++, Delphi, C# и Java являются примерами объектно ориентированных языков. Настоящий ОО язык программирования имеет классы, объекты и очень часто интерфейсы.

Что такое Абстракция?

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

Взаимодействие с объектами используя информацию описанную в классе

Объектно ориентированные языки программирования содержат концепцию классов. Класс — это определение, которое используется для создания объектов. Мы можем оперировать объектом через свойства и методы объявленные в классе, без понимания их действительной реализации. Также мы можем ожидать одинакового поведения от объектов одного и того же класса.

Пока никаких сюрпризов. Всё что нам нужно было определено в TCircle и мы знаем что мы можем использовать свойства и методы класса который мы использовали для создания объекта.

Взаимодействие с объектами через информацию описанную в классе-предке

Объектно ориентированные языки программирования позволяют наследовать классы. При правильно ОО проектировании классы потомки к предкам находятся в отношении "является". Для примера мы можем наследовать классы TLine и TArc от класса TCurve. В этом примере предполагаем, что TCurve определяет тип линии (LineType).

В этом примере мы не беспокоимся о том содержит ли список линии (TLine) или дуги (TArc) или другие неизвестные типы. Нас волнует только чтобы объекты были типа TCurve (или его наследники). Но нам не нужно знать действительно ли TCurve сейчас является TArc или TLine.

Взаимодействие с объектом используя информацию описанную в виртуальном (или абстрактном) классе

Рассматривая класс TCurve мы можем представить что он может быть абстракцией любого количества типов, не только TArc и TLine. Например, у нас может класс TSpline или TElipseArc которые тоже относятся через отношение "является" классу TCurve как к предку. Если подумать о геометрической кривой в общем виде мы можем представить методы и свойства которые могут быть абстрагированы. Например, мы можем сказать "кривая может иметь только одну точку начала и одну точку окончания", так что мы можем объявить функцию которая возвращает начальную точку и конечную точку.

Ключевое слово virtual в Delphi означает, что даже если мы вызовем этот метод в родительском типе (TCurve), то будет использоваться реализация наиболее специфичного класса (TArc или TLine). Потомки могут заменить реализацию на свою собственную. Мы рассмотрим это более подробно в концепции Полиморфизма в 3 части.

Для понимания давайте рассмотрим следующее

При объявлении GetStartPoint в классе TArc мы использовали директиву override. Если затем мы ссылаемся на объект через переменную типа TCurve и вызываем GetStartPoint то будет вызван TArc.GetStartPoint. Ключевое слово abstract в Delphi означает что если мы вызовем GetStartPoint у потомка который не предоставляет реализацию то мы получим "Abstract Error", эта ошибка означает что метод не был переопределён в потомках. Если мы уберём ключевое слово abstract то нам нужно обязательно добавить базовую реализацию на уровне класса TCurve. Если потомки не реализуют этот метод то будет вызываться базовая реализация без появления исключения.

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

Абстракция через интерфейсы

Существует заблуждение что интерфейсы это просто абстрактные классы. В случае классов между классом-потомком и классом-предком (включая абстрактные классы) существует отношение "является". Наиболее распространённое отношение между классом и интерфейсом — "поддерживает". Интерфейс может рассматриваться как контракт. Если класс поддерживает интерфейс, то класс берет на себя обязательство выполнить требования интерфейса, или делегировать их. Подробнее про делегирования интерфейсов будет в следующих статьях.

Интерфейсы даже более абстрактны чем абстрактные классы: один интерфейс может быть применён для нескольких иерархий классов одновременно. Можно даже создать свою иерархию наследования интерфейсов независящую от иерархии наследования классов к которым эти интерфейсы применяются. По сравнению с интерфейсами иерархии классов содержат структуры и функции которые трудно менять.

Рассмотрим пример который показывает отличия интерфейса от абстрактного класса.

Очевидно что эта сущность не может войти не в одну конкретную иерархию классов. Этот интерфейс может быть применён к любому классу который потенциально можно сравнивать с другим классом. Фактически интерфейс IComparable должен применяется ко многим иерархиям классов. Применённый к классу интерфейс заставляет реализовать метод в классе. Мы же ссылаемся на этот метод на высоком уровне абстракции интерфейса. Ниже пример процедуры которая сортирует любой список объектов которые поддерживают интерфейс IComparable.

В итоге

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

Если вы нашли ошибку, пожалуйста, выделите фрагмент текста и нажмите Ctrl+Enter.

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *