четверг, 25 мая 2017 г.

Как неизменяемый объект может обладать состоянием и поведением?

Я часто слышу этот аргумент против неизменных объектов: "Да, они полезны, когда состояние не меняется, но в нашем случае мы имеем дело с часто меняющимися объектами. Мы просто не можем позволить себе создавать новый документ каждый раз, когда нам просто нужно изменить его название." Здесь я не согласен: название объекта не является состоянием документа, если вам нужно часто его менять. Это поведение. Документ может и должен быть неизменным, если он является хорошим объектом, даже если его заголовок часто изменяется. Позвольте мне объяснить, как это сделать.


Идентичность, состояние и поведение

В принципе, в каждом объекте есть три элемента: идентичность, состояние и поведение. Идентичность - это то, что отличает наш документ от других объектов. Состояние - это то, что документ знает о себе (a.k.a. «инкапсулированные знания»), а поведение - это то, что документ может сделать для нас по запросу. Например, это изменяемый документ:
class Document {
  private int id;
  private String title;
  Document(int id) {
    this.id = id;
  }
  public String getTitle() {
    return this.title;
  }
  public String setTitle(String text) {
    this.title = text;
  }
  @Override
  public String toString() {
    return String.format("doc #%d about '%s'", this.id, this.text);
  }
}
Попробуем использовать этот изменяемый объект:
Document first = new Document(50);
first.setTitle("How to grill a sandwich");
Document second = new Document(50);
second.setTitle("How to grill a sandwich");
if (first.equals(second)) { // FALSE
  System.out.println(
    String.format("%s is equal to %s", first, second)
  );
}
Здесь мы создаем два объекта и затем модифицируем их инкапсулированные состояния. Очевидно, first.equals (second) возвратит false, потому что два объекта имеют разные идентификаторы, даже если они инкапсулируют одно и то же состояние.

Метод toString () предоставляет поведение документа - документ может преобразовать себя в строку.

Чтобы изменить заголовок документа, мы просто снова вызываем его setTitle ():
first.setTitle("How to cook pasta");
Проще говоря, мы можем многократно использовать объект, изменяя его внутреннее состояние. Это быстро и удобно, не правда ли? Быстро, да. Удобно, не очень. Читайте дальше.

Неизменяемые объекты не имеют идентичности

Как я уже упоминал ранее, неизменность - одна из достоинств хорошего объекта, и очень важная. Хороший объект неизменен, а хорошее программное обеспечение содержит только неизменяемые объекты. Основное отличие между неизменяемыми и изменяемыми объектами состоит в том, что неизменяемый не имеет идентичности, а его состояние никогда не изменяется. Вот неизменный вариант того же документа:
@Immutable
class Document {
  private final int id;
  private final String title;
  Document(int id, String text) {
    this.id = id;
    this.title = text;
  }
  public String title() {
    return this.title;
  }
  public Document title(String text) {
    return new Document(this.id, text);
  }
  @Override
  public boolean equals(Object doc) {
    return doc instanceof Document
      && Document.class.cast(doc).id == this.id
      && Document.class.cast(doc).title.equals(this.title);
  }
  @Override
  public String toString() {
    return String.format(
      "doc #%d about '%s'", this.id, this.text
    );
  }
}
Этот документ является неизменяемым, и его состояние (идентификатор) является его идентификатором. Давайте посмотрим, как мы можем использовать этот неизменяемый класс (кстати, я использую @Immutable аннотацию из jcabi-аспектов)
Document first = new Document(50, "How to grill a sandwich");
Document second = new Document(50, "How to grill a sandwich");
if (first.equals(second)) { // TRUE
  System.out.println(
    String.format("%s is equal to %s", first, second)
  );
}
Мы не можем изменить документ. Когда нам нужно изменить заголовок, мы должны создать новый документ:
Document first = new Document(50, "How to grill a sandwich");
first = first.title("How to cook pasta");
Каждый раз, когда мы хотим изменить его инкапсулированное состояние, нам также нужно изменить его идентификатор, потому что нет идентичности. Состояние - это тождество. Посмотрите на код метода equals () выше - он сравнивает документы по их идентификаторам и названиям. Теперь id + название документа - его личность!

Что о частых изменениях?

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

Ответ прост. Название документа не должно быть частью его состояния. Вместо этого заголовок должен быть его поведением. Рассмотрим, пример:
@Immutable
class Document {
  private final int id;
  Document(int id) {
    this.id = id;
  }
  public String title() {
    // read title from storage
  }
  public void title(String text) {
    // save text to storage
  }
  @Override
  public boolean equals(Object doc) {
    return doc instanceof Document
      && Document.class.cast(doc).id == this.id;
  }
  @Override
  public String toString() {
    return String.format("doc #%d about '%s'", this.id, this.title());
  }
}
Концептуально говоря, этот документ выступает в роли прокси реального документа, который, например, хранит заголовок где-нибудь в файле. Это то, что должен делать хороший объект - быть прокси реальной сущности. Документ предоставляет две функции: чтение заголовка и сохранение заголовка. Вот как будет выглядеть интерфейс:
@Immutable
interface Document {
  String title();
  void title(String text);
}
Title () читает заголовок документа и возвращает его в виде строки, а заголовок (String) сохраняет его обратно в документ. Представьте себе настоящий бумажный документ с заголовком. Вы просите объект прочитать этот заголовок из бумаги или стереть существующий и написать над ним новый текст. Этот документ является «копией», используемой в этих методах.

Теперь мы можем делать частые изменения в неизменяемом документе, и документ остается неизменным. Это не перестает быть неизменным, поскольку его состояние (id) не изменяется. Это тот же документ, хотя мы меняем его название, потому что название не является состоянием документа. Это что-то в реальном мире, вне документа. Документ - это всего лишь прокси между нами и этим «что-то». Чтение и запись заголовка - это поведение документа, а не его состояние.

Изменяемая память

Единственный вопрос, который мы до сих пор не получили ответа, - это что такое «копия» и что произойдет, если нам нужно сохранить название документа в памяти?

Давайте посмотрим на это с точки зрения объектного мышления. У нас есть объект-документ, который должен представлять реальную сущность в объектно-ориентированном мире. Если такой объект является файлом, мы легко реализуем методы title (). Если такой объект является объектом Amazon S3, мы также легко реализуем методы чтения и записи заголовков, сохраняя объект неизменным. Если такой объект является страницей HTTP, у нас нет никаких проблем при реализации чтения или записи заголовка, что сохраняет неизменный объект. У нас нет проблем, пока существует реальный документ, который имеет свою собственную идентичность. Наши методы чтения и записи названия свяжутся с этим реальным документом и извлекут или обновят его название.

Проблемы возникают, когда такая сущность не существует в реальном мире. В этом случае нам нужно создать свойство изменяемого объекта под названием title, прочитать его через title () и изменить его через заголовок (String). Но объект неизменен, поэтому мы не можем иметь изменяемое свойство в нем - по определению! Что мы делаем?

Думать.

Как может быть, что наш объект не представляет реальность? Помните, реальный мир - это все вокруг среды обитания объекта. Возможно ли, что объект не представляет никого и действует сам по себе? Нет, это невозможно. Каждый объект является представителем реального мира. Итак, кто это представляет, если мы хотим сохранить заголовок внутри него и у нас нет файла или страницы HTTP за объектом?

Он представляет собой компьютерную память.

Заголовок неизменяемого документа # 50, «Как зажарить сэндвич», хранится в памяти, занимая 23 байта пространства. Документ должен знать, где хранятся эти байты, и он должен быть способен читать их и заменять их чем-то другим. Эти 23 байта являются реальным объектом, который представляет объект. Байты не имеют никакого отношения к состоянию объекта. Они представляют собой изменчивую реальную сущность, похожую на файл, страницу HTTP или объект Amazon S3.

К сожалению, Java (и многие другие современные языки) не позволяют напрямую обращаться к памяти компьютера. Вот как мы могли бы спроектировать наш класс, если бы такой прямой доступ был возможен:
@Immutable
class Document {
  private final int id;
  private final Memory memory;
  Document(int id) {
    this.id = id;
    this.memory = new Memory();
  }
  public String title() {
    return new String(this.memory.read());
  }
  public void title(String text) {
    this.memory.write(text.getBytes());
  }
}
Этот класс памяти будет реализован JDK изначально, и все остальные классы будут неизменными. Класс Memory будет иметь прямой доступ к куче памяти и будет отвечать за malloc и бесплатные операции на уровне операционной системы. Наличие такого класса позволит нам сделать все классы Java неизменными, включая StringBuffer, ByteArrayOutputStream и т.д.
 
Роль объекта состоит в том, чтобы сделать кусок данных живым, оживить его, но не стать частью данных

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

Какое практическое решение?

К сожалению, у нас нет такого класса представления памяти в Java, Ruby, JavaScript, Python, PHP и многих других языках высокого уровня. Похоже, дизайнеры языков не поняли идею живых объектов против мертвых данных, что печально. Мы вынуждены смешивать данные с состояниями объектов, используя одни и те же языковые конструкции: объектные переменные и свойства. Может быть, когда-нибудь у нас будет такой класс памяти в Java и других языках, но до тех пор у нас есть несколько вариантов.

Используйте C++. В нем и подобных низкоуровневых языках можно напрямую обращаться к памяти и обрабатывать данные в памяти так же, как мы имеем дело с внутрифайловыми или в-HTTP-данными. В C++ мы можем создать этот класс Memory и использовать его точно так, как мы объяснили выше.

Используйте массивы. В Java массив представляет собой структуру данных с уникальным свойством - он может быть изменен при объявлении как final. Вы можете использовать массив байтов как изменяемую структуру данных внутри неизменяемого объекта. Это суррогатное решение, концептуально напоминающее класс Memory, но гораздо более примитивное.
 
По возможности старайтесь избегать данных в памяти. В некоторых областях это легко сделать; Например, в веб-приложениях, обработке файлов, адаптерах ввода-вывода и т.д. Однако в других доменах это проще сказать, чем сделать. Например, в играх, алгоритмах обработки данных и графическом интерфейсе большинство объектов оживляют данные в памяти, главным образом потому, что память является единственным ресурсом, который у них есть. В этом случае, без класса Memory, вы в конечном итоге работаете с изменяемыми объектами :( Нет обходного пути.

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

Хороший объект - неизменный аниматор изменяемых данных. Несмотря на то, что он неизменен, а данные изменяемы, он жив и данные мертвы в рамках среды обитания объекта.


Комментариев нет:

Отправить комментарий