
Create your own language on Groovy
- From the sandbox
- Tutorial
The main problem of imperative programming languages is their low proximity to natural languages.
OOP solved this problem in parts, sorting the data and logic into classes of objects, but it still looks difficult to understand. The main problem here is that imperative languages are not adapted to work with object hierarchies and nested method calls.
For example, I have a hierarchy of classes for working with product orders for customers:
The business logic of the job description will look like this:
Thanks to the grace of Groovy, the code is quite simple and readable. But the example is not complicated. In real life, when writing complex business object management logic, the code will look bulky and poorly read. It turns out, I have a certain API that is controlled only from code, it is difficult to write and not easy to read.
Groovy has the opportunity to simplify your life by writing your own declarative markup language to easily describe how you perform the necessary actions. Here's what the analogue of the above business logic will look like in a markup language:
Such code does not need comments at all - it has high readability.
To implement this language, you need to write a builder in Groovy. Groovy has an abstract class BuilderSupport, from which you need to inherit to create your own builder. In an inherited class, you will need to override a number of methods that Groovy will automatically call when parsing the markup language in the code. Here's what the builder class will look like:
Two abstract methods setParent and createNode are overridden in this class. setParent is called when the parent node is assigned to the node and is not used in my logic. But in createNode it’s just called for each markup element. Depending on the syntax of the description of the markup node, one of the four overloaded createNode methods is called. My syntax assumes that elements always have parameters. Therefore, I registered the necessary functionality in the desired method and added exceptions to all other createNode methods. This will allow you to control and eliminate the incorrect syntax for describing method calls. The only exception was made for the call root label, which is automatically created first when the builder starts without parameters. I expanded the builder class with the constructor into which the created objects of customer lists are transferred, products and orders. Also described in the class methods for adding business entities. Nothing complicated - everything is remarkably visible in the code and in the comments in it.
And here is the final code for using the created markup language with checking the results:
Result:
Everything works :)
Summarizing, we can say that the scope of the builders is large. For example, I’m now based on it to develop a data transformation description language for my open source GETL project (Groovy-based ETL). With the help of a builder, you can easily develop a syntax that allows you to collect SQL queries in code or display information in a hierarchical native formatted form. Yes, and I think regular XML / JSON markers now are not secrets. The markup language can be used not only in Groovy code, but also as blocks for describing objects and actions that are taken out in separate files. Description blocks can be read directly from files in runtime and executed using the EVAL method. Since the blocks are well formalized, for them you can easily write your own GUI for developing business logic by ordinary users.
There are many examples. But most importantly, do not forget - all of the above works fine in Java without any effort! In Groovy, no one bothers with binding any Java classes and methods to their markup language, in which to write business logic, which is further used in Java applications. These features are worth starting to use magical Groovy in Java :)
You can download the full texts of the class used in the article from here .
OOP solved this problem in parts, sorting the data and logic into classes of objects, but it still looks difficult to understand. The main problem here is that imperative languages are not adapted to work with object hierarchies and nested method calls.
For example, I have a hierarchy of classes for working with product orders for customers:
// Клиент
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 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)
}
The business logic of the job description will look like this:
// Списки бизнес-сущностей
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)
Thanks to the grace of Groovy, the code is quite simple and readable. But the example is not complicated. In real life, when writing complex business object management logic, the code will look bulky and poorly read. It turns out, I have a certain API that is controlled only from code, it is difficult to write and not easy to read.
Groovy has the opportunity to simplify your life by writing your own declarative markup language to easily describe how you perform the necessary actions. Here's what the analogue of the above business logic will look like in a markup language:
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)
}
Such code does not need comments at all - it has high readability.
To implement this language, you need to write a builder in Groovy. Groovy has an abstract class BuilderSupport, from which you need to inherit to create your own builder. In an inherited class, you will need to override a number of methods that Groovy will automatically call when parsing the markup language in the code. Here's what the builder class will look like:
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
}
}
Two abstract methods setParent and createNode are overridden in this class. setParent is called when the parent node is assigned to the node and is not used in my logic. But in createNode it’s just called for each markup element. Depending on the syntax of the description of the markup node, one of the four overloaded createNode methods is called. My syntax assumes that elements always have parameters. Therefore, I registered the necessary functionality in the desired method and added exceptions to all other createNode methods. This will allow you to control and eliminate the incorrect syntax for describing method calls. The only exception was made for the call root label, which is automatically created first when the builder starts without parameters. I expanded the builder class with the constructor into which the created objects of customer lists are transferred, products and orders. Also described in the class methods for adding business entities. Nothing complicated - everything is remarkably visible in the code and in the comments in it.
And here is the final code for using the created markup language with checking the results:
// Списки бизнес-сущностей
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
Result:
Added customer 1234: Customer
Added product a100: Product 1
Added product a200: Product 2
Added order 1 from customer Customer
Added into order 1 detail pos 1 with product Product 1
Added into order 1 detail pos 2 with product Product 2
*** Result * **
Customers:
{inn = 1234, name = Client, address = Russia, phone = + 74951002030}
Products:
{article = a100, name = Item 1, price = 100.0}
{article = a200, name = Item 2, price = 200.0}
Orders:
{num = 1, customer = Customer,
detail = {pos = 1, product = Product 1, count = 1, sum = 100.0};
{pos = 2, product = Product 2, count = 1, sum = 200.0}}
Everything works :)
Summarizing, we can say that the scope of the builders is large. For example, I’m now based on it to develop a data transformation description language for my open source GETL project (Groovy-based ETL). With the help of a builder, you can easily develop a syntax that allows you to collect SQL queries in code or display information in a hierarchical native formatted form. Yes, and I think regular XML / JSON markers now are not secrets. The markup language can be used not only in Groovy code, but also as blocks for describing objects and actions that are taken out in separate files. Description blocks can be read directly from files in runtime and executed using the EVAL method. Since the blocks are well formalized, for them you can easily write your own GUI for developing business logic by ordinary users.
There are many examples. But most importantly, do not forget - all of the above works fine in Java without any effort! In Groovy, no one bothers with binding any Java classes and methods to their markup language, in which to write business logic, which is further used in Java applications. These features are worth starting to use magical Groovy in Java :)
You can download the full texts of the class used in the article from here .