Thursday, February 08, 2007

Declarative with groovy

กำลังทำงานอยู่ชิ้นหนึ่ง งานนี้เป็นงานรับข้อมูลงบการเงินเข้ามา
แล้วทำการสรุปพวก financial ratio ต่างๆออกมา
โดยสูตรของการคำนวณขึ้นอยู่กับ context ของผู้ที่ส่งงบเข้ามาด้วย
(เช่น สูตร ratio ของงบการเงินของธนาคาร ย่อมต่างจาก งบการเงินของพวกโรงงาน)
รายละเอียดปลีกย่อยมียุบยับ แต่ละไว้แล้วกัน

ประเด็นก็คือ เราอยากเขียนโปรแกรมให้กำหนดสูตรในลักษณะ declarative ได้
แทนที่จะเขียนเป็น method หรือ class ใน java แบบปกติ
เมื่ออยากได้ solution ที่เป็น declarative ก็เลยต้องมองหาพวก script language เข้ามาช่วย
ก็เลยหยิบเอา groovy มาลองทดสอบดู

ในเบื้องต้น เรามีสูตรแบบนี้อยู่
current_ratio = สินทรัพย์หมุนเวียน / หนี้สินหมุนเวียน


เขียนเป็น groovy ตรงๆก็คือ
current_ratio = element('asset') / element('liabilities')

โดย method element คือ helper ที่ช่วยดึงข้อมูลจากงบการเงิน

แต่พอลองทดสอบดู ก็พบว่า เราไม่สามารถ bind method element เข้าไปตรงๆได้
ต้องแปลงสูตรให้เป็น
current_ratio = env.element('asset') / env.element('liabilities')

การ evaluate code นี้ทำได้โดย

Binding binding = new Binding();
binding.setVariable("env", helperObject);
GroovyShell shell = new GroovyShell(binding);
shell.evaluate(src);
float current_ratio = binding.getVariable("current_ratio");

ใช้งานได้ แต่ดูแล้ว การที่ต้องใช้ "env" ก่อนนั้นดูไม่งามเลย
นอกจากนี้ยังมีประเด็นเรื่อง fix ชื่อสูตรไว้ใน code ด้วย
ประกอบกับที่พอนำ idea ไปคุยกับ user, user บอกว่า
อยากให้ script มันสามารถ include กันได้ด้วย
เพราะว่า sector แต่ละ sector มันมี common ratio อยู่

ก็เลยออกแบบใหม่
เอา Closure เข้ามาช่วย
สุดท้ายได้หน้าตาประมาณนี้ออกมา
formulas.declare {
include 'common.script'

formula('current ratio') {
element('assets')/element('liabilities')
}

formula('quick ratio') {
element('cash') + .... / element('liabilities')
}
}

ซึ่งการ evaluate script นี้จะยังไม่ได้ผลลัพท์ทันที
แต่จะได้ Closure จำนวนหนึ่งออกมา ซึ่งเราจะเก็บไว้ก่อน
แล้วค่อยนำ Closure พวกนี้ไป run ใน context ใดๆที่เราต้องการ

code ที่ใช้อ่าน formula เข้ามา หน้าตาเป็นแบบนี้
พระเอกของเรื่องนี้คือ closure.setDelegate(..)
public FormulaSet load(String name) throws IOException {
final FormulaSet ret = new FormulaSet();
Binding binding = new Binding();
binding.setVariable("formulas", new Delegate() {

public void declare(Closure c) {
c.setDelegate(this);
c.call();
}

public void formula(String name, Closure c) {
Formula f = new FormulaImpl(name, c);
ret.put(name, f);
}

public void include(String name) throws IOException {
FormulaSet set = FormulaLoaderImpl.this.load(name);
ret.add(set);
}

});
GroovyShell shell = new GroovyShell(binding);
InputStream src = getClass().getClassLoader().
getResourceAsStream(scriptPath + name);
if (src == null) throw new IOException("script " + name + " not found!");
Script script = shell.parse(src);
script.run();
return ret;
}

Related link from Roti

No comments: