Чистый код. Создание, анализ и рефакторинг — страница 16 из 94

Существует веская причина для ограничения доступа к переменным в программах: мы не хотим, чтобы другие программисты зависели от них. Мы хотим иметь возможность свободно менять тип или реализацию этих переменных так, как считаем нужным. Тогда почему же многие программисты автоматически включают в свои объекты методы чтения/записи, предоставляя доступ к приватным переменным так, словно они являются открытыми?

Абстракция данных

Давайте сравним между собой листинги 6.1 и 6.2. В обоих случаях код представляет точку на декартовой плоскости. Однако в одном случае реализация открыта, а в другом она полностью скрыта от внешнего пользователя.


Листинг 6.1. Конкретная реализация Point

public class Point {

  public double x;

  public double y;

}


Листинг 6.2. Абстрактная реализация Point

public interface Point {

  double getX();

  double getY();

  void setCartesian(double x, double y);

  double getR();

  double getTheta();

  void setPolar(double r, double theta);

}

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

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

С другой стороны, листинг 6.1 явно реализован в прямоугольных координатах, а пользователь вынужден работать с этими координатами независимо. Более того, такое решение раскрывает реализацию даже в том случае, если бы переменные были объявлены приватными, и мы использовали одиночные методы чтения/записи.

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

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


Листинг 6.3. Конкретная реализация Vehicle

public interface Vehicle {

  double getFuelTankCapacityInGallons();

  double getGallonsOfGasoline();

}


Листинг 6.4. Абстрактная реализация Vehicle

Abstract Vehicle

public interface Vehicle {

  double getPercentFuelRemaining();

}

В обоих примерах вторая реализация является предпочтительной. Мы не хотим раскрывать подробности строения данных. Вместо этого желательно использовать представление данных на абстрактном уровне. Задача не решается простым использованием интерфейсов и/или методов чтения/записи. Чтобы найти лучший способ представления данных, содержащихся в объекте, необходимо серьезно поразмыслить. Бездумное добавление методов чтения и записи — худший из всех возможных вариантов.

Антисимметрия данных/объектов

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

Возьмем процедурный пример из листинга 6.5. Класс Geometry работает с тремя классами геометрических фигур. Классы фигур представляют собой простые структуры данных, лишенные какого-либо поведения. Все поведение сосредоточено в классе Geometry.


Листинг 6.5. Процедурные фигуры

public class Square {

  public Point topLeft;

  public double side;

}


public class Rectangle {

  public Point topLeft;

  public double height;

  public double width;

}


public class Circle {

  public Point center;

  public double radius;

}


Листинг 6.5 (продолжение)

public class Geometry {

  public final double PI = 3.141592653589793;

  public double area(Object shape) throws NoSuchShapeException

  {

    if (shape instanceof Square) {

      Square s = (Square)shape;

      return s.side * s.side;

    }

    else if (shape instanceof Rectangle) {

      Rectangle r = (Rectangle)shape;

      return r.height * r.width;

    }

    else if (shape instanceof Circle) {

      Circle c = (Circle)shape;

      return PI * c.radius * c.radius;

    }

    throw new NoSuchShapeException();

  }

}

Объектно-ориентированный программист недовольно поморщится и пожалуется на процедурную природу реализации — и будет прав. Но возможно, его презрительная усмешка не обоснована. Подумайте, что произойдет при включении в Geometry функции perimeter(). Классы фигур остаются неизменными! И все остальные классы, зависящие от них, тоже остаются неизменными! С другой стороны, при добавлении новой разновидности фигур мне придется изменять все функции Geometry, чтобы они могли работать с ней. Перечитайте еще раз. Обратите внимание на то, что эти два условия диаметрально противоположны.

Теперь рассмотрим объектно-ориентированное решение из листинга 6.6. Метод area() является полиморфным, класс Geometry становится лишним. Добавление новой фигуры не затрагивает ни одну из существующих функций, но при добавлении новой функции приходится изменять все фигуры![24]


Листинг 6.6. Полиморфные фигуры

Polymorphic Shapes

public class Square implements Shape {

  private Point topLeft;

  private double side;