habrahabr

google protocol buffers: наследование, поиск

  • воскресенье, 15 июня 2014 г. в 03:10:34
http://habrahabr.ru/post/226225/

Статья состоит из двух частей. В первой части содержится вольный пересказ статьи о статьи о наследовании в protobuf. Вторая часть посвещенна назойливой рекламе самописному «велосипеду» для работы с фреймворком.

Статья не содержит ответа на вопрос «что такое google protocol buffers» и не привязана какому-то конкретному языку програмирования.


Итак, постановка задачи:
  • Как имплементировать полиморфизм, при работе с protocol buffers.
  • Как искать и находить нужные данные, имея большой файл с сообщениями.




Часть I: Наследование


Предположим, наш протокол содержит три класса объектов: Square, Circle и Polygon. Предположим так же, что все они имеют поле color и поле id. В этом месте у нас появляется несколько причин унаследовать их от общего предка Shape. И, наверное, если бы мы писали на языке с поддержкой полиморфизма, наш код выглядел бы следуюшим образом:
псевдокод
     	enum Color { 
     		RED, 
     		GREEN, 
     		BLUE
     	}
     	
     	
     	struct Point {
     		int x;
     		int y;
     	}
     	
     	
     	struct Shape  {
     		int   id;
     		Color color;
     	}
     	
     	struct Square extends Shape {
     		Point  corner;
     		int    width;
     	}
     	
     	struct Circle  extends Shape {
     		Point  center;
     		int    radius;
     	}
     	
     	struct Polygon  extends Shape {
     		Point [] points;
     	}




К сожалению, google protocol buffers не поддерживают иерархий. Jon Parise рассматривает три варианта обхода этого ограничения.

Использование опциональных полей

При этом подходе мы создаем отдельную структуру для каждого класса-наследника, а структура Shape содержит опциональные поля на каждый случай.
Такой подход имеет серьезный недосток, нельзя создать новый класс наследник, не поменяв базовый класс. Вслучае, если вы расширяете чужой протокол, это может стать проблеммой.
Кроме того, такой подход позволяет создать никому не известный квадрато-круг, инициализировав оба (square и circle) поля.
geom-1.proto
	enum Color {
		RED   = 1;
		GREEN = 2;
		BLUE  = 3;
	}

	message Point {
		required fixed32 x = 1;
		required fixed32 y = 2;
	}

	message Square  {
		required Point   corner = 1;
		required fixed32 width  = 2;
	}

	message Circle  {
		required Point   center = 1;
		required fixed32 radius = 2;
	}

	message Polygon  {
		repeated Point   points = 1;
	}

	message Shape  {
		required TYPE    type  = 1;
	    required fixed32 id    = 2;
		optional Color color   = 3;

		// наследование
		optional Square  square = 4;
		optional Circle  circle = 5;
		optional Polygon polygon= 6;
	}




Вложеная серилизация

Другой подход подраумевает создание создание структуры Shape с общими для наследников полями, и добавления еще одного поля, где лежат уже сериализованые поля наследника.

Так же не самый удачный вариант, так как «сериализатор» не будет «распаковывать» содержимое поля subclass, и соответственно не будет проведена проверка целостности.
Да и вообще, не красиво как-то.
geom-2.proto
	enum TYPE {
		SQUARE  = 1;
		CIRCLE  = 2;
		POLYGON = 3;
	}

	enum Color {
		RED   = 1;
		GREEN = 2;
		BLUE  = 3;
	}

	message Point {
		required fixed32 x = 1;
		required fixed32 y = 2;
	}

	message Square  {
		required Point   corner = 1;
		required fixed32 width  = 2;
	}

	message Circle  {
		required Point   center = 1;
		required fixed32 radius = 2;
	}

	message Polygon  {
		repeated Point   points = 1;
	}

	message Shape  {
		required TYPE    type  = 1;
	    required fixed32 id    = 2;
		optional Color color   = 3;

		// наследование
		required bytes subclass = 4; 		
	}



вложеные расширения

Третий (рекомендованый) подход, похож на первый, но вместо опциональных полей используются расширения. Для борьбы с квадрато-кругом заводится поле type.
geom-final.proto
	enum TYPE {
		SQUARE  = 1;
		CIRCLE  = 2;
		POLYGON = 3;
	}

	enum Color {
		RED   = 1;
		GREEN = 2;
		BLUE  = 3;
	}

	message Point {
		required fixed32 x = 1;
		required fixed32 y = 2;
	}

	message Shape  {
		required TYPE    type  = 1;
	    required fixed32 id    = 2;
		optional Color color   = 3;

		extensions 4 to max;
	}

	message Square  {
		extend Shape {
			required Square shape = 5;
		}  

		required Point   corner = 1;
		required fixed32 width  = 2;
	}

	message Circle  {
		extend Shape {
			required Circle shape = 6;
		}  

		required Point   center = 1;
		required fixed32 radius = 2;
	}

	message Polygon  {
		extend Shape {
			required Polygon shape = 7;
		}  

		repeated Point   points = 1;
	}




Часть II: Поиск


И так, у нас есть файл с описанием структуры сообщений (geom.proto). Наша програма отработала и создала большой файл с самими сообщениям. Хотелось бы находить в нем нужную информацию, а простым текстовым поиском это не всегда возможно.

Например:
  • Найдти окружности пересекаюшие оси координат.
  • Найдти полигоны с пустым множеством точек
  • Найдти полигоны с больше чем 10-ю точками.

Согласитесь, обычный grep нам здесь не поможет.

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

Вернемся с примерам задач:
  • type == "CIRCLE" && ((center.x - radius) < 0 || (center.y - radius) < 0)) // Найдти окружности пересекаюшие оси координат.
  • type == "POLYGON" && #points == 0 // Найдти полигоны с пустым множеством точек
  • type == "POLYGON" && #points > 10 // Найдти полигоны с больше чем 10-ю точками.


Все это, и многое другое умеет делать самописный велосипед.

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



Надеюсь, что велосипед найдет своего пользователя, а в месте с этим и появится необходимость в подробном топике о синтаксе и возможностях велосипеда.
Спасибо.