Создаем свой язык на Groovy

Основная проблема императивных языков программирования — их низкая приближенность к естественным языкам.

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

Например, у меня есть иерархия классов работы с заказами на продукты для клиентов:
// Клиент
class Customer {
    int inn
    String name
    String address
    String phone
}

// Клиенты
class Customers {
    Customer findByInn(inn)
    void add(Customer customer)
}

// Продукт
class Product {
    String article
    String name
    double price
}

// Продукты
class Products {
    Product findByArticle(article)
    void add(Product product)
}

// Заказ
class Order {
    int num
    Customer customer
    List<OrderDetail> details = []

    OrderDetail findByPos(pos)
    void add(OrderDetail detail)
}

// Товар заказа
class OrderDetail {
    int pos
    Product product
    def count = 1
    def getSum() { count * product.price }
}

// Заказы
class Orders {
    Order findByNum(num)
    void add(Order order)
}


Сама бизнес логика описания работы будет выглядеть вот так:
// Списки бизнес-сущностей
def customers = new Customers()
def products = new Products()
def orders = new Orders()

// Добавление клиента
customers.add(new Customer(inn: 1234, name: "Клиент", address: "Россия", 
							phone: "+74951002030"))

// Добавление продукта
products.add(new Product(article: "a100", name: "Товар 1", price: 100.00))
products.add(new Product(article: "a200", name: "Товар 2", price: 200.00))

// Добавление заказа
def order = new Order(num: 1, customer: customers.findByInn(1234))
order.add(new OrderDetail(pos: 1, 
			product: products.findByArticle("a100"), count: 1))
order.add(new OrderDetail(pos: 2, 
			product: products.findByArticle("a200"), count: 1))
orders.add(order)



Благодаря изяществу Groovy, код достаточно простой и читабельный. Но и пример не сложный. В реальной жизни, при написании сложной бизнес логики управления объектами, код будет выглядеть громоздко и плохо читаться. Получается, я имею некий API, который управляется только из кода, сложно пишется и нелегко читается.

В Groovy есть возможность упростить свою жизнь, написав собственный декларативный язык разметки для легкого описания выполнения нужных действий. Вот как будет выглядеть на языке разметки аналог вышеописанной бизнес логики:
AddCustomer(inn: 1234, name: "Клиент", address: "Россия", phone: "+74951002030")

AddProduct(article: "a100", name: "Товар 1", price: 100.00)
AddProduct(article: "a200", name: "Товар 2", price: 200.00)

AddOrder(num: 1, customer: 1234) {
	Detail(pos: 1, product: "a100", count: 1)
	Detail(pos: 2, product: "a200", count: 1)
}


Такой код вообще не нуждается в комментариях — он имеет высокую читабельность.

Для реализации этого языка потребуется написать билдер на Groovy. У Groovy есть абстрактный класс BuilderSupport, от которого нужно наследоваться, чтобы создать свой билдер. В наследуемом классе потребуется перекрыть ряд методов, которые Groovy будет автоматически вызывать при разборе языка разметки в коде. Вот как будет выглядеть класс билдера:
public class MyBuilder extends BuilderSupport {
	public Customers customers
	public Products products
	public Orders orders

	// Назначение ноде родительской ноды
	protected void setParent(Object parent, Object child) {
	}

	// Создать ноду без параметров
	protected Object createNode(Object name) {
		if (name != "call") 
			throw new Exception("Node required parameters")
		new Node(null, name);
	}

	// Создать ноду с привязанным к ней объектом
	protected Object createNode(Object name, Object value) {
		throw new Exception("Node required parameters")
	}

	// Создать ноду с параметрами
	protected Object createNode(Object name, Map attributes) {
		// Получаем родительскую текущую ноду
		Node parent = getCurrent()
		def result
		
		// Анализируем имя ноды
		switch (name) {
			case "AddCustomer":
				result = addCustomer(attributes)
				break
			case "AddProduct":
				result = addProduct(attributes)
				break
			case "AddOrder":
				result = addOrder(attributes)
				break
			case "Detail":
				if (parent == null || 
					parent.name() != "AddOrder")
					throw new Exception(
						"Detail must be specified with only AddOrder")

				result = addOrderDetail(parent.value(), attributes)
				break
			defailt:
				throw new Exception("Unknown node ${name}")
		}
		new Node(null, name, attributes, result);
	}

	// Создать ноду с параметрами и привязанным к ней объектом
	protected Object createNode(Object name, Map attributes, Object value) {
		throw new Exception("Node ${name} can not support objects")
	}

	// Добавляем клиента
	def addCustomer(Map params) {
		def customer = new Customer(inn: params.inn, name: params.name, 
						address: params.address, phone: params.phone)
		customers.add(customer)
		println "Added customer ${customer.inn}: ${customer.name}"
		customer
	}
	
	// Добавляем продукт
	def addProduct(Map params) {
		def product = new Product(article: params.article, name: params.name, 
						price: params.price)
		products.add(product)
		println "Added product ${product.article}: ${product.name}"
		product
	}
	
	// Добавляем заказ
	def addOrder(Map params) {
		def order = new Order(num: 1, customer: customers.findByInn(params.customer))
		orders.add(order)
		println "Added order ${order.num} from customer ${order.customer.name}"
		order
	}
	
	// Добавляем строку заказа
	def addOrderDetail(Order order, Map params) {
		def count = params.count?:1
		def detail = new OrderDetail(pos: params.pos, 
						product: products.findByArticle(params.product), 
						count: count)
		order.add(detail)
		println "Added into order ${order.num} detail pos ${detail.pos} " +
				"with product ${detail.product.name}"
		detail
	}
}

В этом классе перекрыто два абстрактных метода setParent и createNode. setParent вызывается при назначении дочерней ноде родителя и в моей логике не используется. А вот в createNode как раз и вызывается на каждый элемент разметки. В зависимости от синтаксиса описания ноды разметки, вызывается один из четырех перегруженных методов createNode. Моей синтаксис предполагает, что у элементов всегда есть параметры. Поэтому я прописал необходимую функциональность в нужный метод и добавил исключения во все остальные методы createNode. Это позволит проконтролировать и исключить неправильный синтаксис описания вызова методов. Единственное исключение было сделано для рутовой метки call, которая автоматически создается первой при запуске билдера без параметров. Класс билдера я расширил конструктором, в который передаются созданные объекты списков клиентов, продуктов и заказов. Так же описал в классе методы добавления бизнес сущностей. Ничего сложного — все замечательно видно в коде и по комментариям в нем.

А вот конечный код пользования созданным языком разметки с проверкой результатов:
// Списки бизнес-сущностей
def customers = new Customers()
def products = new Products()
def orders = new Orders()

// Создать объект билдера
def myApi = new MyBuilder(customers: customers, products: products, orders: orders)

// Вызвать билдер
myApi {
	AddCustomer(inn: 1234, name: "Клиент", address: "Россия", phone: "+74951002030")
	
	AddProduct(article: "a100", name: "Товар 1", price: 100.00)
	AddProduct(article: "a200", name: "Товар 2", price: 200.00)
	
	AddOrder(num: 1, customer: 1234) {
		Detail(pos: 1, product: "a100", count: 1)
		Detail(pos: 2, product: "a200", count: 1)
	}
}

// Печать результатов
println "\n*** Result ***"
println "Customers:"
println customers
println "Products:"
println products
println "Orders:"
println orders

Результат:
Added customer 1234: Клиент
Added product a100: Товар 1
Added product a200: Товар 2
Added order 1 from customer Клиент
Added into order 1 detail pos 1 with product Товар 1
Added into order 1 detail pos 2 with product Товар 2

*** Result ***
Customers:
{inn=1234, name=Клиент, address=Россия, phone=+74951002030}
Products:
{article=a100, name=Товар 1, price=100.0}
{article=a200, name=Товар 2, price=200.0}
Orders:
{num=1, customer=Клиент,
detail={pos=1, product=Товар 1, count=1, sum=100.0};
{pos=2, product=Товар 2, count=1, sum=200.0}}

Все работает :)

Резюмируя можно сказать, что область применения у билдеров большая. Я, например, сейчас на его основе разрабатываю язык описания трансформации данных для своего open source проекта GETL (ETL на базе Groovy). С помощью билдера можно легко разработать синтаксис, который позволит собирать SQL запросы в коде или выводить информацию в иерархическом собственном форматированном виде. Да и штатные XML/JSON маркеры думаю теперь не представляют из себя тайны. Язык разметок можно использовать не только в коде программ на Groovy, но и как блоки описания объектов и действий, которые вынесены в отдельные файлы. Блоки описания можно прямо в runtime считывать из файлов и выполнять с помощью метода EVAL. Так как блоки хорошо формализованы, то для них можно легко написать собственный GUI разработки бизнес логики обычными пользователями.

Примеров можно привести множество. Но самое главное, не стоит забывать — все вышесказанное замечательно без каких либо усилий работает на Java! Никто не мешает в Groovy обвязать любые Java классы и методы своим языком разметки, на котором писать бизнес логику, которая дальше используется в Java приложениях. Эти возможности стоят того, чтобы начать использовать в Java волшебный Groovy :)

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

Ну, и что?
Реклама
Комментарии 4
  • +2
    Я хоть и имею опыт реализации DSL на groovy, но статья все равно выглядит мануалом «как нарисовать сову» — сначала показывается обычный подход, ок, всем все понятно. Далее — «в groovy можно написать свой builder», и дается код, который это делает. И «резюмируется», что область применения у этого большая. Если бы я не был знаком с groovy, у меня возникли бы следующие вопросы — как это работает? что за класс BuilderSupport, который мы расширяем? Что за методы CreateNode и SetParent, которые мы перегружаем, когда они вызываются? Как, черт возьми, работают замыкания в groovy? (точнее, я даже не знал бы, что для работы билдеров, используются замыкания с модменой контекста).
    ИМХО, статья вызывает больще вопросов, чем что-то объясняет. Простую и доходчивую книгу «Groovy for domain-specific languages» я в свое время прочитал за вечер. Кстате, есть перевод на русский.
    • 0
      Спасибо за критику, учту! Писал статью в расчете на то, что читатель знаком с Groovy и может в Groovy API найти описание BuilderSupport. Когда вызываются методы CreateNode и setParent вроде написал вкратце, в API есть более подробное описание. Книга отличная, всем кто заинтересовался DSL на Groovy было бы здорово почитать. Вот только я не встречался с русскоязычным переводом, не могли бы Вы кинуть ссылку, где ее можно купить или скачать?
      • 0
        Прошу прощения, насчет перевода ошибся. Оригинал настолько легко и быстро читался, что остались впечатления, как будто это перевод :-)
      • 0
        Выглядит не на 30% понятно, но заманчиво.
        А можно ли где-нибудь найти реализацию этого «билдера»? Мне кажется, что на Swift он отсутствует, что несколько мотивирует портировать его туда.
        А если и не портировать, то может какие-то идеи впитать.

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

        Самое читаемое