Объекты против структур данных

В мире Java-разработки есть объекты и есть объекты. Первое более важно.

Еще не запутались?

Объектно-ориентированное программирование (ООП) познакомило мир с концепцией объектов (маленькая буква «о»). Объекты ООП - это границы инкапсуляции вокруг состояния, которые обеспечивают какое-то публичное поведение. Концепция объектов не ограничивается каким-либо конкретным языком. Фактически, мы видим объекты, представленные в языках на основе классов, таких как Java / C # / Ruby / Python, а также в языках на основе прототипов, таких как JavaScript.

Java представила Java-класс под названием «Object» (с большой буквой «O»). Объекты Java - это экземпляры класса Object (включая все подклассы). Эти объекты являются языковыми конструкциями, а не концептуальными конструкциями.

Таким образом, возникает вопрос: квалифицируются ли объекты Java как объекты ООП? Что ж, это зависит от обстоятельств. В этой статье рассматриваются конкретные обстоятельства использования объектов структуры данных по сравнению с объектами ООП.

Структуры данных

Рассмотрим структуру данных, представляющую человека, которая включает имя, фамилию и номер телефона. Как эта структура данных может выглядеть на различных процедурных языках?

Структура данных человека в C:

struct Person {
  char firstName[20];
  char lastName[20];
  char phoneNumber[10];
};

Структура данных человека в Паскале:

type
  Person = record
    firstName : string;
    lastName : string;
    phoneNumber : string;
  end

В Java такая же структура данных может выглядеть так:

public class Person {
  public String firstName;
  public String lastName;
  public String phoneNumber;
}

Структура данных Java «технически» отличается от версий C и Pascal, потому что структура данных Java - это класс, а не структура или запись. Но отличается ли Java Person функционально от структуры C или записи Pascal? Нет. Функционально это то же самое. Все 3 структуры данных предоставляют 3 строковых поля, которые можно читать или писать.

Важным моментом является то, что объект Java Person вообще не является «объектом», это структура данных. Объект Person существует для организации некоторых данных в единую сущность, которую можно передавать и управлять как единое целое - точно так же, как структура C и запись Pascal.

Но что, если объект Java Person выглядит так:

public class Person {
  private String mFirstName;
  private String mLastName;
  private String mPhoneNumber;
  public String getFirstName() {
    return mFirstName;
  }
  public void setFirstName(String firstName) {
    mFirstName = firstName;
  }
  public String getLastName() {
    return mLastName;
  }
  public void setLastName(String lastName) {
    mLastName = lastName;
  }
  public String getPhoneNumber() {
    return mPhoneNumber;
  }
  public void setPhoneNumber(String phoneNumber) {
    mPhoneNumber = phoneNumber;
  }
}

Является ли Java-объект реальным объектом? Теперь у него есть частные данные и общедоступные методы. Это ведь должен быть объект ООП?

Даже с общедоступными геттерами и сеттерами Person по-прежнему является структурой данных. Изменилось ли его назначение? Изменилось ли его поведение? Нет. Объект Person будет по-прежнему использоваться таким же образом и таким же образом, как и его предшественник, принадлежащий общедоступной собственности. Версия Person для получения / установки по-прежнему на 100% представляет собой структуру данных.

Хорошо, а что, если мы начнем добавлять поведение в Person, например:

public class Person {
  //... same getters/setters as before, except one:
  public void setPhoneNumber(String phoneNumber) throws FormatException {
    validatePhoneNumber(phoneNumber);
    mPhoneNumber = phoneNumber;
  }
  private void validatePhoneNumber(String phoneNumber) throws FormatException {
    // Do validation here to ensure we have a legit phone number.
    // Throw an exception if its invalid.
  }
}

Теперь у нас есть настоящий объект ООП? Эта последняя версия Person теперь включает поведение проверки и даже использует частный метод для реализации проверки.

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

Сколько бы помады вы ни нанесли на эту свинью, Person - это структура данных, а не объект.

Объекты ООП

Как выглядит объект ООП? Похоже на услугу. Объект ООП - это конструкция, которая что-то делает - ведет себя и действует.

В структуре данных Person есть имя, фамилия и номер телефона. Объект Person ходит, бежит, прыгает и говорит. Объект Person делает что-то.

Вот несколько примеров объектов ООП:

public interface PhoneNumberValidator {
  boolean validate(String phoneNumber);
}

PhoneNumberValidator проверяет, что данная строка представляет собой правильно отформатированный номер телефона. В валидаторе нет индикации внутреннего состояния. Может быть, у валидатора есть состояние, а может и нет, но мы знаем, что он предлагает услугу проверки номера телефона.

public interface PersonDataStore {
  Person getPeople(PeopleQuery query);
  void addPerson(Person person);
  void removePerson(Person person);
}

PersonDataStore предоставляет механизм для хранения и запроса структур данных Person. В этом примере и Person, и PeopleQuery являются структурами данных - они просто организуют информацию. Однако PersonDataStore - это объект, предоставляющий услуги. А именно, PersonDataStore может взять структуру данных PeopleQuery и найти все структуры данных Person, которые соответствуют критериям в запросе.

Сохраняется ли PersonDataStore в памяти? Сохраняется ли он на диске? Индексирует ли он данные? Как клиент PersonDataStore, мы ничего из этого не знаем, и нам все равно. Нас интересует только то, что делает PersonDataStore, потому что PersonDataStore является объектом ООП.

Путаница в объектах и ​​структурах данных

Снова рассмотрим структуру данных Person. В последнем примере мы добавили поведение проверки номера телефона. Вы можете представить, что нам могут понадобиться аналогичные проверки для имени и фамилии. Более того, когда мы добавляем дополнительные поля в эту структуру данных, эти поля также могут нуждаться в проверке.

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

Принцип открытости / закрытости:

Каждое новое поле требует открытия класса Person и добавления кода для проверки.

Принцип единой ответственности:

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

Принцип разделения интерфейса:

Представьте, что вас интересует только сбор телефонного номера. Когда вы работаете с экземпляром Person, вы зависите не только от метода validatePhoneNumber (), но также от методов validateFirstName () и validateLastName (). Таким образом, вы зависите от методов, которые вам не требуются, и, следовательно, вы нарушили принцип разделения интерфейса.

Данные против поведения

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

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

Вместо этого определите поведение на основе ваших сценариев использования, определите структуры данных на основе ваших требований ввода-вывода, а затем определите объекты для их соединения. Например, Android Activity может определять метод, подобный следующему:

// Called when the user finishes entering a phone number.
private void onPhoneNumberSet() {
  String phoneNumber = mPhoneNumberTextView.getText().toString();
  boolean isValid = mPhoneNumberValidator.validate(phoneNumber);
  if (isValid) {
    mPerson.setPhoneNumber(phoneNumber);
  } else {
    // Notify user of problem.
  }
}

В этом примере Person остается собственной структурой данных, которая не знает о поведении проверки. Затем есть объект PhoneNumberValidator, который знает, как проверить строку номера телефона, но ничего не знает о структуре данных Person. Наконец, содержащий Activity управляет проверкой номера телефона, за которой следует установка данных номера телефона в структуре данных Person.

Когда пришло время отправить данные Person в источник данных (веб или локальную БД), вы можете представить себе что-то вроде этого:

private void doSubmit() {
  mPersonDataStore.addPerson(mPerson);
}

Мы не знаем, обращается ли PersonDataStore к локальной БД, к веб-серверу или даже к локальной памяти. Все, что мы знаем, - это то, что наша ответственность за сценарий использования заключается в сборе и хранении данных Person. Мы использовали объекты ООП для проверки ввода, а теперь мы используем объект ООП для хранения данных. Во время этого процесса данные были собраны в структуре данных Person.

К чему вся суета?

Почему кого-то должно волновать это тонкое различие между структурами данных и объектами?

Структуры данных являются государственными.

Позвольте мне повторить это для эффекта.

Структуры данных являются государственными.

Следовательно, передача структур данных означает совместное использование состояния, а общее состояние является корнем всего зла. Причина, по которой были изобретены объекты ООП, заключалась в том, чтобы предоставить парадигму, в которой общее состояние можно было бы минимизировать и контролировать (вот почему мы должны Пакетировать с умом).

Думайте о структурах данных как о формате обмена внутри вашего кода между вашими объектами ООП. Рассмотрим следующий псевдокод:

objectA.doThing1();
objectA.doThing2();
myDataStructure = objectA.extractState();
objectB.setState(myDataStructure);
objectB.doThing3();
objectB.doThing4();

Мы используем objectA для работы. Затем мы извлекаем состояние objectA, получая структуру данных. Мы инициализируем объект B, отправляя структуру данных, полученную от A, и теперь объект B выполняет некоторую работу. В этом примере подразумевается неизменность myDataStructure, так что изменение myDataStructure не будет иметь никакого скрытого внутреннего воздействия на objectA или objectB.

Структура данных просто предоставляет механизм для перемещения по организованному состоянию. Он не предлагает никакого собственного поведения. Вот как мы должны стремиться использовать структуры данных в нашем коде.

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

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

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

Хакерский полдень - это то, с чего хакеры начинают свои дни. Мы часть семьи @AMI. Сейчас мы принимаем заявки и рады обсудить рекламные и спонсорские возможности.

Если вам понравился этот рассказ, мы рекомендуем прочитать наши Последние технические истории и Современные технические истории. До следующего раза не воспринимайте реалии мира как должное!