Туторіал по Google Protocol Buffers (частина II)

Перелічування та Вкладені Класи
Згенерований код включає перелічення (Java 5+) PhoneType, вкладене в Person:

public static enum PhoneType {
  MOBILE(0, 0),
  HOME(1, 1),
  WORK(2, 2),
  ;
  ...
}

Вкладений тип Person.PhoneNumber згенерований, як і очікувалось, як вкладений в Person клас.

Конструктори на противагу Повідомленням

Всі повідомлення класів, згенеровані за допомогою protocol buffer компілятора, є сталими (immutable). Як тільки створений об'єкт повідомлення, то він не може бути зміненим, так само, як і Java String тип. Для побудови повідомлення спочатку потрібно створити конструктор, задати певним полям необхідними значеннями, а потім викликати метод конструктора build().

Можна помітити, як кожен метод конструктора, що модифікує повідомлення, вертає інший конструктор. В дійсності, повернений об'єкт є тим самим конструктором, з якого викликається метод. Для зручності він вертається так, щоб можна було послідовно зв'язати між собою декілька сеттерів в просту стрічку коду.

Ось приклад створення екземпляру Person:

Person john =
  Person.newBuilder()
    .setId(1234)
    .setName("John Doe")
    .setEmail("jdoe@example.com")
    .addPhone(
      Person.PhoneNumber.newBuilder()
        .setNumber("555-4321")
        .setType(Person.PhoneType.HOME))
    .build();

Стандартні Методи Повідомлень

Кожне повідомлення та конструктор класу містять також ряд інших методів, які дозволяють контролювати або маніпулювати цілим повідомленням, включаючи:
  • isInitialized(): перевіряє чи задані всі обов'язкові поля.

  • toString(): вертає повідомлення в представленні, яке є доступним людському розумінню, зокрема корисне при відлагодженні.

  • mergeFrom(Message other): (тільки конструктор) з'єднує вміст інших в поточне повідомлення, перезаписує окремі поля та зчеплює ті, котрі повторюються.

  • clear(): (тільки конструктор) встановлює всі поля назад в порожній стан.

Ці методи реалізувують Message і Message.Builder інтерфейси спільно із всіма Java повідомленнями та конструкторами. Для отримання додаткової інформації див. complete API documentation for Message.

Аналіз та Серіалізація (Serialization)

Нарешті кожен клас protocol buffer-а має методи для написання і читання повідомлень з вибраного типу, використовуючи двійковий формат protocol buffer-у. До них відносяться:
  • byte[] toByteArray();: серіалізує повідомлення та вертає масив байтів, що містить рядок байтів.

  • static Person parseFrom(byte[] data);: аналізує повідомлення з даного масиву байтів.

  • void writeTo(OutputStream output);: серіалізує повідомлення і записує його в OutputStream.

  • static Person parseFrom(InputStream input);: читає та аналізує повідомлення з InputStream.

Ось лише кілька з варіантів, передбачених для аналізує та серіалізаціі. Знову ж таки, див. Message API reference для повного списку.

Protocol Buffers та O-O ескіз В основному класи protocol buffer-а скидають дані holder-ам (подібно до structs в C++), проте вони не дуже дружать з об'єктною моделлю. Найкращий спосіб поведінки із згенерованими класами полягає в тому, щоб згорнути згенерований клас protocol buffer-а у конкретний клас програми. Згортання protocol buffer-ів також є хорошою ідеєю при відсутньому керуванні над ескізом .proto файлу (якщо, скажімо, повторно використовувати певний з іншого проекту). У цьому випадку можна використовувати обгортку класу (wrapper class) до розробки інтерфейсу більш належного для особливого середовища даної програми: приховування деяких даних і методів, показ придатних функцій і т. д. Не можна додавати поведінку до згенерованих класів за їхнім наслідуванням. Це дозволить розірвати внутрішні механізми і взагалі не є приємливою об'єктно-орієнтованою практикою.

Написання Повідомлення

Тепер слід спробувати використати дані класи protocol buffer-у. Спочатку необхідно, щоб програма адресна книжка могла записувати персональні подробиці у файл адресної книжки. Для цього потрібно створити і заповнити екземпляри даних класів protocol buffer-у, а потім записати їх у вихідний потік.

Ось програма, яка зчитує AddressBook з файлу, додає до нього новий Person, що базований на даних, введенних користувачем, а також знову записує новий AddressBook назад у файл. Виділені ті частини, які наряму викликають або посилають код, згенерований protocol компілятором.

import com.example.tutorial.AddressBookProtos.AddressBook;
import com.example.tutorial.AddressBookProtos.Person;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.InputStreamReader;
import java.io.IOException;
import java.io.PrintStream;

class AddPerson {
  // This function fills in a Person message based on user input.
  static Person PromptForAddress(BufferedReader stdin,
                                 PrintStream stdout) throws IOException {
    Person.Builder person = Person.newBuilder();

    stdout.print("Enter person ID: ");
    person.setId(Integer.valueOf(stdin.readLine()));

    stdout.print("Enter name: ");
    person.setName(stdin.readLine());

    stdout.print("Enter email address (blank for none): ");
    String email = stdin.readLine();
    if (email.length() > 0) {
      person.setEmail(email);
    }

    while (true) {
      stdout.print("Enter a phone number (or leave blank to finish): ");
      String number = stdin.readLine();
      if (number.length() == 0) {
        break;
      }

      Person.PhoneNumber.Builder phoneNumber =
        Person.PhoneNumber.newBuilder().setNumber(number);

      stdout.print("Is this a mobile, home, or work phone? ");
      String type = stdin.readLine();
      if (type.equals("mobile")) {
        phoneNumber.setType(Person.PhoneType.MOBILE);
      } else if (type.equals("home")) {
        phoneNumber.setType(Person.PhoneType.HOME);
      } else if (type.equals("work")) {
        phoneNumber.setType(Person.PhoneType.WORK);
      } else {
        stdout.println("Unknown phone type.  Using default.");
      }

      person.addPhone(phoneNumber);
    }

    return person.build();
  }

  // Main function:  Reads the entire address book from a file,
  //   adds one person based on user input, then writes it back out to the same
  //   file.
  public static void main(String[] args) throws Exception {
    if (args.length != 1) {
      System.err.println("Usage:  AddPerson ADDRESS_BOOK_FILE");
      System.exit(-1);
    }

    AddressBook.Builder addressBook = AddressBook.newBuilder();

    // Read the existing address book.
    try {
      addressBook.mergeFrom(new FileInputStream(args[0]));
    } catch (FileNotFoundException e) {
      System.out.println(args[0] + ": File not found.  Creating a new file.");
    }

    // Add an address.
    addressBook.addPerson(
      PromptForAddress(new BufferedReader(new InputStreamReader(System.in)),
                       System.out));

    // Write the new address book back to disk.
    FileOutputStream output = new FileOutputStream(args[0]);
    addressBook.build().writeTo(output);
    output.close();
  }
}

Читання Повідомлення

Звісно, що адресна книга не буде приносити великої користі, якщо з неї не можна буде отримати жодної інформації! Цей приклад читає файл, створений на основі вище описаного прикладу та виводить всю інформацію в ньому.

import com.example.tutorial.AddressBookProtos.AddressBook;
import com.example.tutorial.AddressBookProtos.Person;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.PrintStream;

class ListPeople {
  // Iterates though all people in the AddressBook and prints info about them.
  static void Print(AddressBook addressBook) {
    for (Person person: addressBook.getPersonList()) {
      System.out.println("Person ID: " + person.getId());
      System.out.println("  Name: " + person.getName());
      if (person.hasEmail()) {
        System.out.println("  E-mail address: " + person.getEmail());
      }

      for (Person.PhoneNumber phoneNumber : person.getPhoneList()) {
        switch (phoneNumber.getType()) {
          case MOBILE:
            System.out.print("  Mobile phone #: ");
            break;
          case HOME:
            System.out.print("  Home phone #: ");
            break;
          case WORK:
            System.out.print("  Work phone #: ");
            break;
        }
        System.out.println(phoneNumber.getNumber());
      }
    }
  }

  // Main function:  Reads the entire address book from a file and prints all
  //   the information inside.
  public static void main(String[] args) throws Exception {
    if (args.length != 1) {
      System.err.println("Usage:  ListPeople ADDRESS_BOOK_FILE");
      System.exit(-1);
    }

    // Read the existing address book.
    AddressBook addressBook =
      AddressBook.parseFrom(new FileInputStream(args[0]));

    Print(addressBook);
  }
}

Розширення Protocol Buffer-у

Рано чи пізно, після запуску коду, що використовує даний protocol buffer, безсумніву потрібно буде «покращити» визначення protocol buffer-у. Якщо необхідно, щоб нові buffer-и були сумісними з попередніми версіями, а попередні buffer-и бути сумісні з наступними версіями — виглядає, як майже необхідне — то існують деякі правила, які потрібно дотримуватися. У новій версії protocol buffer-у:
  • слід не змінювати номери теґів всіх присутніх полів.

  • слід не створювати і не стирати жодні з обов'язкових полів.

  • можна стирати необов'язкові поля та поля, що повторюються.

  • можна створювати необов'язкові поля та поля, що повторюються, проте потрібно використовувати свіжі номери теґів (тобто, номери теґів, які ніколи не використовувалися в protocol buffer-і, навіть в стертих полях).

(Ось деякі вийнятки до правил, проте їх рідко використовують.)

Якщо дотримуватися цих правил, старий код буде радісно читати нові повідомлення і просто ігнорувати всі нові поля. Для старого коду, необов'язкові поля, які стерли, просто матимуть свої значення за замовчуванням, а стерті поля, які повторювались, будуть порожніми. Новий код також буде відкрито зчитувати старі повідомлення. Однак, майте на увазі, що нові необов'язкові поля не будуть присутні у старих повідомленнях, так що вам доведеться або прямо перевірити чи у них встановлено has_ або забезпечити допустимим по замовчуванню значенням у даному .proto файлі з [default = value] після номеру теґу. Якщо значення за замовчуванням не є визначеним для необов'язкового елемента, то по замовчуванню замість нього використовується значення конкретного типу: для стрічок (string) значенням за замовчуванням є порожня стрічка, для булівських (boolean) — false, для числових (numeric) типів — нуль. Слід також зауважити, що якщо додано нове поле, яке повторюється, то даний новий код не зможе повідомити чи воно залишилось порожнім (для нового коду) чи взагалі коли-небудь було встановлене (для старого коду), оскільки немає has_ прапорця (flag) для нього.

Вдосконалене Застосування

Можливе використовування Protocol buffer-ів виходить за рамки простих аксессорів та серіалізаціі. Для впевненості можна досліджувати Java API reference, щоб дізнатися, що ще можна робити з ними.

Однією з ключових можливостей, що надаються класами протоколу повідомлення є відображення (reflection). Можна ітерувати над полями повідомлення та маніпулювати їхніми значеннями без написання коду порівняно з будь-яким конкретним типом повідомлення. Одним з найкорисніших способів використання відображення є для перетворення протоколу повідомлень в інші кодування та з них, таких як XML або JSON. А більш передове застосування відображення надає можливість знаходити відмінності між двома повідомлення одного й того ж типу, або розробляти свого роду «регулярні вирази для протокольних повідомлень», в яких можна записувати вирази, що відповідають певному змістові повідомлення. Якщо використати фантазію, то можна застосовувати Protocol Buffer-и в набагато ширшому колі проблем, ніж можна було очікувати на початку!

Відображення постачається як складова Message та Message.Builder інтерфейсів.
  • +3
  • 6 липня 2009, 15:52
  • mari

Коментарі (0)

RSS згорнути / розгорнути

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.