Sunday, July 01, 2007

Chart with Flex

ในงานล่าสุด UI element ต้องมีการแสดง chart ด้วย
โดยเป็น chart ที่มีลักษณะเป็น dynamic, control จาก javascript ได้
ตอนแรก ก็ใช้ plotkit ซึ่งเป็น opensource
แต่พอเอาไปให้ user ดู ก็เจอติเรื่องความสวยงาม
ซึ่งจริงๆถ้ามีเวลา ก็อาจจะไล่แก้โปรแกรม plotkit ทำสวยได้
แต่เมื่อไม่มีเวลา ก็เลยหาโอกาสเสียตังค์ลองใช้ flex ดู

ความรู้สึกหลังจากได้ทดลองพัฒนา component ดู
พบว่า
1. mxml ดีกว่าที่คิดไว้เยอะ
2. model การ binding parameter ของ component ก็งามดี
3. actionscript เขียนง่ายมาก, ถ้ามี background java + javascript
ก็น่าจะผ่านฉลุย
4. api library ออกแบบไว้ดี, ชอบที่เราสามารถ customize nature ของ widget
ได้ด้วยโดยการ pass customize function เข้าไปเป็น parameter (callback)
5. การ integrate กับ javascript ผ่านทาง ExternalInterface ก็ง่ายดาย
6. ถ้าใช้ SDK พัฒนา จะเบื่อกับ build time ที่นานเหลือเกิน (เดี๋ยวนี้ไม่กี่ิวิ ก็เริ่มทนไม่ได้แล้ว)
7. flex builder มันใช้ eclipse เป็นฐาน, ความคุ้นเคยต่างๆ ก็เลยเต็มร้อย

หน้าตาของ component ที่ทำออกมา



ตัวอย่าง javascript ที่ใช้ provide ข้อมูลให้ chart component
document.chart.setBaseAtZero(true);
datas = [{Year: 2005, ROE: 1000, Revenue: 200},
{Year: 2006, ROE: 500, Revenue: 300},
{Year: 2007, ROE: 300, Revenue: 1000}];

series = [{name: "ROE", displayName: "ROE"},
{name: "Revenue", displayName: "Revenue"}];

document.chart.loadChart("Compare", datas, series, "Year");


ส่วนอันนี้คือ mxml ที่เขียน

<?xml version="1.0"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" initialize="initApp()"
paddingLeft="0"
paddingBottom="0"
paddingRight="0"
paddingTop="0">

<mx:Script>
<![CDATA[
import mx.charts.CategoryAxis;
import mx.charts.series.ColumnSeries;
import mx.charts.chartClasses.IAxis;

public function initApp():void {
if (ExternalInterface.available) {
ExternalInterface.addCallback("loadChart", loadChart);
ExternalInterface.addCallback("setBaseAtZero", setBaseAtZero);
ExternalInterface.addCallback("setLabelPrecision", setLabelPrecision);
}
}

private function setLabelPrecision(value:int):void {
numberFormatter.precision = value;
}

private function setBaseAtZero(value:Boolean):void {
linearAxis.baseAtZero = value;
}

private function loadChart(description:String, datas:Array, maps:Array, category:String):void {
panel.title = description;
var mySeries:Array = new Array();
for each (var obj:Object in maps) {
var ss:ColumnSeries = new ColumnSeries();
ss.xField = category;
ss.yField = obj.name;
ss.displayName = obj.displayName;
mySeries.push(ss);
}

myChart.series = mySeries;

var cat:CategoryAxis = new CategoryAxis();
cat.categoryField = category;
myChart.horizontalAxis = cat;

myChart.dataProvider = datas;

}

private function labelFunction(labelValue:Object, previousValue:Object, axis:IAxis):String {
return numberFormatter.format(labelValue);
}
]]>



</mx:Script>

<mx:NumberFormatter id="numberFormatter" precision="0"
useThousandsSeparator="true" useNegativeSign="true"/>

<mx:Panel id="panel" title="ColumnChart and BarChart Controls Example"
height="100%" width="100%" layout="horizontal">

<mx:ColumnChart id="myChart" height="100%" width="80%"
paddingLeft="5" paddingRight="5"
showDataTips="true" >

<mx:verticalAxis>
<mx:LinearAxis id="linearAxis" baseAtZero="true" labelFunction="labelFunction"/>
</mx:verticalAxis>

</mx:ColumnChart>

<mx:Legend dataProvider="{myChart}" width="20%" />

</mx:Panel>
</mx:Application>


User confirm เมื่อไร ก็เตรียมเสียตังค์ 900 เหรียญ

Related link from Roti

Monday, June 25, 2007

Lisp in Autocad

เคยสงสัยเหมือนกันว่า ทำไป AutoCAD ถึงใช้ Lisp
Lisp is ideally suited to the unstructured interaction that characterises the design process. Unlike programming languages such as C and FORTRAN, which force one to organise a problem entirely before programming, Lisp encourages exploring various approaches to a problem interactively, exactly as CAD helps a designer.

หลายคนที่เคยเขียนแต่ Java อย่างเดียว
จะจิตนาการถึงข้อนี้ไม่ออก
ส่วนตัวผม (ซึ่งหาเลี้ยงชีพด้วย java เช่นกัน)
พอจะเห็นเค้าลางๆ จากการอ่านหนังสือ AI ของ Norvig
Link
"Why Lisp"

Related link from Roti

fatherhood

หลังจากมีลูกก็คิดว่าตัวเองเปลี่ยนไปหลายอย่าง
วันนี้อ่านเจอ มีคนตั้งสมมติฐานว่า
การตั้งท้องและการคลอดไม่ใช่ว่าจะทำให้ผู้หญิงเปลี่ยนไปเพียงฝ่ายเดียว
ผู้ชาย ก็มีการเปลี่ยนแปลงเกิดขึ้นเช่นกัน
เช่น
1. ช่วงระหว่างการตั้งท้อง ผู้ชายจะน้ำหนักขึ้น 20 %
ว่ากันว่า เก็บไว้เปลี่ยนเป็นพลังงานตอนเลี้ยงลูกอ่อน
(roof: คุณลดน้ำหนักอยู่ ระวังไม่มีแรงเลี้ยงลูกนะ)

2. หลังจากคลอดแล้ว ฮอร์โมน Testosterone จะลดลง 1 ใน 3
ซึ่งมีผลหลายอย่าง เช่น
  • มีความอดทนในการอุ้มลูกได้นานขึ้น
  • ลดความก้าวร้าว แทนที่จะออกไปหาเรื่องแข่งขันกับคนอื่น
    ก็จะสงบเสงี่ยมมากขึ้น (ภาษาอังกฤษเขาใช้คำว่า too busy fighting other men)
  • ความสนใจในหญิงอื่นที่ไม่ใช่ภรรยา จะลดลง
    (จากประสบการณ์ตรง เห็นพ้องกับข้อนี้มาก)

ทั้งหมดนี้ ทำให้คุณพ่อ หันมาสนใจเปลี่ยนผ้าอ้อมให้ลูกมากขึ้น

3. มีการเปลี่ยนแปลงสมองในส่วน prefrontal cortex
ส่งผลให้สมองส่วน planning + memory skill ทำงานได้ดีขึ้น
(ประสบการณ์ตรง: พอมีลูกสองคนจะรู้้สึกว่า ข้อนี้มีประโยชน์มาก)

อ่านต่อได้ใน
http://www.slate.com/id/2168389/nav/ais/

Related link from Roti

Thursday, June 21, 2007

Tapestry5 กับ ภาษาไทย

veer เปิดประเด็นปัญหานี้ขึ้นมา ผมก็เลยไปสรุปเพิ่มเติมมา แบ่งเป็น 3 ประเด็นคือ

update: คุณ pa แจ้งว่า มีการแก้ไข case นี้แล้ว
โดยกำหนดให้ set ค่า encoding ผ่านทาง META tag ใน Html template แทน
ซึ่งจะครอบคลุม case 1.2 และ 2

update(27/06/2007): ทดสอบการแก้ไขที่คุณ pa แจ้งแล้ว พบ bug Tapestry-1605


Note: เนื่องจาก T5 ยังอยู่ในช่วยพัฒนา
วิธีการที่ว่ามานี้ ในอนาคตคาดว่าจะมีการ implement กลไกที่ง่ายกว่านี้

1. ประเด็นแรกก็คือ ถ้าเราใส่ภาษาไทยลงไปใน template file ตรงๆ (แทนที่จะใช้กลไก localize ของ T5)
จะแสดงผลให้ถูกต้องได้อย่างไร

ประเด็นนี้ แยกเป็นประเด็นย่อยได้ 2 เรื่องคือ
1.1 ประเด็นการ parse template file ด้วย encoding ที่ถูกต้อง
ถ้าเราไปแกะ code ของ tapestry ดู จะเห็นว่า คนที่รับผิดชอบในการ
parse template ก็คือ TemplateParserImpl
ซึ่งภายในจะใช้กลไก org.xml.sax.helpers.XMLReader ในการ parse
ดังนั้น ปัญหาในจุดนี้ น่าจะแก้ได้โดยการระบุ encoding บนหัว template file

<?xml version="1.0" encoding="UTF-8"?>

Note: ผมไม่เจอปัญหานี้ เนื่องจาก enviroment ผม มี default locale เป็น UTF-8 อยู่แล้ว

1.2 ประเด็นการของ print output ออกไปยัง response stream
ด้วย encoding ที่ถูกต้อง
จากข้อ 1.1 หลังจากได้ character ที่ถูกต้องแล้ว
ก็ต้อง print ออกไปให้ถูก encoding ด้วย
ตรงนี้ เห็นมีพูดถึงใน mailing-list, เท่าที่ดูหลายๆอันแล้ว ผมชอบวิธีนี้มากสุด
โดยการ replace PageResponseRenderer ด้วยการใช้กลไก decorator
    public static PageResponseRenderer decoratePageResponseRenderer(
@InjectService("PageMarkupRenderer")
final PageMarkupRenderer markupRenderer,
@InjectService("MarkupWriterFactory")
final MarkupWriterFactory markupWriterFactory, final Object delegate)
{

return new PageResponseRenderer()
{
public void renderPageResponse(Page page, Response response) throws IOException
{
MarkupWriter writer = markupWriterFactory.newMarkupWriter();
markupRenderer.renderPageMarkup(page, writer);
PrintWriter pw = response.getPrintWriter("text/html; charset=UTF-8");
writer.toMarkup(pw);
pw.flush();
}
};
}
}


2. ประเด็นของ form input element ที่ submit เป็นภาษาไทย เข้ามา
ตรงนี้ เราต้องแก้ไขด้วยการใส่ filter เข้าไป set ค่า encoding ให้กับ servlet request ก่อน
โดยปกติเราจะใช้ servlet filter ช่วย set ให้ก็ได้
แต่วิธีที่ลงให้ดูนี้ จะใช้กลไกของ IOC container ของ tapestry มาจัดการให้แทน
    public void contributeRequestHandler(OrderedConfiguration<RequestFilter> configuration,
@InjectService("TimingFilter")
RequestFilter filter,
@InjectService("Utf8Filter")
RequestFilter utf8Filter)
{
// Each contribution to an ordered configuration has a name, When necessary, you may
// set constraints to precisely control the invocation order of the contributed filter
// within the pipeline.

configuration.add("Timing", filter);
configuration.add("encoding", utf8Filter);
}

public RequestFilter buildUtf8Filter(
@InjectService("RequestGlobals")
final RequestGlobals requestGlobals ) {
return new RequestFilter() {
public boolean service( Request request, Response response, RequestHandler handler ) throws IOException {
requestGlobals.getHTTPServletRequest().setCharacterEncoding( "UTF-8" );
return handler.service( request, response );
}
};
}


3. ประเด็นกรณี uri ที่ submit เข้ามา, มีภาษาไทย encode เข้ามาด้วย
กรณีนี้ รู้สึกจะเป็นกับ Tomcat เจ้าเดียว
ให้ configure file server.xml เพิ่ม
URIEncoding="UTF-8"

Related link from Roti

Wednesday, June 06, 2007

Spring Configuration

โดยปกติ คนที่ใช้ spring มักจะบ่นเรื่อง Configuration file ที่เต็มไปด้วย xml
ถึงกับมี solution จำนวนหนึ่งออกมาทดแทน เช่นไปใช้ groovy หรือไปใช้ annotation

เนื่องจากผมก็มีความเบื่อหน่ายใน xml file เหมือนกัน
ดังนั้นตอนนี้ จึงเริ่มมองหาวิธีที่จะมาทดแทน
สำหรับวิธีการ config สำหรับ spring ที่ผมสนใจตอนนี้ ก็มี
  • Annotation-based configuration
    วิธีนี้จะ bundle มาใน spring 2.1
  • Java Configuration
    แยกเป็น project มาต่างหาก โดยอยู่ใน project JavaConfig

ประเด็นหลักๆที่ต่างกันของ 2 วิธี ก็คือ แนวคิดเรื่องการแยกข้อมูลการ configuration ออกจาก source code
ซึ่งวิธีที่สอง จะดูดีกว่าในแง่นี้ เนื่องจากไม่มี code เกี่ยวกับ configuration ปนอยู่ใน bean เลย

ผมทดลองใช้โจทย์ ใน post ที่เคยเขียนเรื่อง Tapestry 5 IoC
มาลอง config ทั้ง 2 วิธีดู
เริ่มด้วยวิธีแรกก่อน
@Component("indexService")
public class IndexServiceImpl implements IndexService {

public void index(Document document, Long key) {
...
}

}

@Repository
public class RepositoryServiceImpl implements RepositoryService {

public Long persistent(Document document) {
...
return id;
}

}

@Component("uploadService")
public class UploadServiceImpl implements UploadService {

private IndexService indexService;
private RepositoryService repositoryService;
private Map<String, Parser> parserMap = new HashMap<String, Parser>();

public void upload(InputStream input, String type) {
Parser parser = parserMap.get(type);
Document document = parser.parse(input);
Long key = repositoryService.persistent(document);
indexService.index(document, key);
}

@Autowired
public void setIndexService(IndexService indexService) {
this.indexService = indexService;
}

@Autowired
public void setRepositoryService(RepositoryService repositoryService) {
this.repositoryService = repositoryService;
}

@Resource(name="parserMap")
public void setParserMap(Map<String, Parser> parserMap) {
this.parserMap = parserMap;
}
}

จะเห็นมี annotaion อยู่ดังนี้
  • @Component -> เข้าข่ายพวกที่เราเคยประกาศ bean id=".." ใน xml file
  • @Repository -> ต่างกับ Component ตรงที่จะใช้กับพวกที่เป็น Data access layer
    ,เข้าใจว่าตั้งชื่อให้ต่างกัน component เพื่อที่จะได้นำไปใช้อย่างอื่นได้ด้วยเช่น
    เอาไป implement พวก automatic translation of exception
  • @Autowired -> เป็นการ inject โดยใช้ type
  • @Resource -> ใช้ annotation ของ JSR-250 มา inject bean พวกที่ declare ไว้ใน xml file แบบเดิม
    หรือแบบที่ซับซ้อนกว่านั้นเช่น lookup จาก jndi

การใช้วิธีแรกนี้ ยังมีปัญหาไม่สามารถ config พวก map property ผ่านทาง annotation ได้
จึงต้องใช้ xml เข้ามาผสมด้วย
<?xml version="1.0" encoding="UTF-8"?> 
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-2.1.xsd"
>

<context:component-scan base-package="anno"/>

<bean id="parserMap" class="java.util.HashMap">
<constructor-arg>
<map>
<entry key="pdf">
<bean class="anno.impl.PdfParser"/>
</entry>
</map>
</constructor-arg>
</bean>

</beans>

จากข้างบน จะเห็นว่า เราใช้ component-scan เข้ามากำหนดว่า
เพื่อที่จะได้ข้อมูล bean ที่ทำ annotation ไว้, spring ต้อง scan กลุ่ม package ใดบ้าง

เห็นแล้วก็ยังไม่ชอบใจนัก
ลองดูวิธีที่ 2 บ้าง
ในวิธี Java Configuration นี้ เราจะ implement class ขึ้นมาแทน xml file
โดยการกำหนด @Configuration ไว้ที่ class,
ซึ่ง spring ก็จะใช้ class ที่มี annotation นี้ในการ config beans
@Configuration
public class MyConfiguration {

@Bean
public IndexService indexService() {
return new IndexServiceImpl();
}

@Bean
public RepositoryService repositoryService() {
return new RepositoryServiceImpl();
}

@Bean
private Map<String, Parser> parserMap() {
HashMap<String, Parser> map = new HashMap<String, Parser>();
map.put("pdf", new PdfParser());
return map;
}

@Bean
public UploadService uploadService() {
UploadServiceImpl impl = new UploadServiceImpl();
impl.setIndexService(indexService());
impl.setRepositoryService(repositoryService());
impl.setParserMap(parserMap());
return impl;
}

}

Note: พวก bean RepositoryServiceImpl, IndexServiceImpl, ...
เขียนแบบ POJO แท้ๆ ไม่ต้องมี Annotation ใดๆ ใส่ไว้ให้รบกวนลูกตาเลย

Note: ให้สังเกตุว่า parserMap เป็น private method ซึ่งเท่ากับว่า bean
นี้ไม่สามารถ access จาก method context.getBean('parserMap') ได้

เราสามารถใช้วิธี Java Configuration ผสมกับ xml ได้เช่นกัน
อย่างสมมติว่า เราต้องการให้ parserMap ถูก config จากข้างนอก
ก็สามารถ เขียนส่วน Configuration ใหม่ดังนี้
        @ExternalBean
public Map<String, Parser> parserMap() {
return null;
}

และไปกำหนด xml ข้างนอกแบบนี้
<beans>

<bean id="parserMap" class="java.util.HashMap">
<constructor-arg>
<map>
<entry key="pdf">
<bean class="anno.impl.PdfParser"/>
</entry>
</map>
</constructor-arg>
</bean>

<bean class="anno.configuration.MyConfiguration"/>

<bean class="org.springframework.config.java.process.ConfigurationPostProcessor"/>

</beans>


ถึงตอนนี้แล้ว ผมออกจะชอบวิธีที่ 2 มากกว่า

Related link from Roti

Tuesday, June 05, 2007

Unit Testing in Tapestry 5

testing ใน tapestry 5 แบ่งออกเป็น 2 พวกคือ
1. พวก unit testing, สามารถใช้ได้ทั้ง testng หรือ junit
class ต่างๆที่ช่วยในการ test อยู่ใน package tapestry-core
2. พวก integrated testing, อันนี้เป็นการ test ผ่าน browser โดยใช้ selenium เข้ามาช่วย,
class ที่ช่วยในการ test อยู่ใน package tapestry-test

class ที่เป็นพระเอกในเรื่อง unit testing ก็คือ PageTester
ลองดูตัวอย่างวิธีใช้ง่ายๆ

สมมติเรามี start page หน้าตาแบบนี้
<html xmlns:t="http://tapestry.apache.org/schema/tapestry_5_0_0.xsd">
<head>
<title>Start Page</title>
</head>
<body>
<p id="greeting">${greeting}</p>
</body>
</html>

แล้วก็มี page class หน้าตาแบบนี้
public class Start
{
@Inject
private HelloService helloService;

public String getGreeting() {
return helloService.getGreeting();
}
}

ตัว Test case ก็จะเขียนอย่างนี้
public class TestStartPage extends Assert {
@Test
public void testPage() {
String appPackage = "demo";
String appName = "Test";
PageTester tester = new PageTester(appPackage, appName, "src/main/webapp");

Document document = tester.renderPage("Start");
assertEquals("hi", document.getElementById("greeting").getChildText());
}
}

จาก code ที่เราเห็นใน class "Start" เราจะเห็นว่า
มันมีการ inject service ที่ชื่อ "HelloService" เข้ามา
ปัญหาก็คือ ในตอน run จริง, service นี้จะได้มาจาก springframework
แต่ในขั้น unit testing นี้เราไม่อยากลากเอา service ตัวจริงๆเข้ามา test ด้วย
ดังนั้น เราต้องทำการสร้าง stub ของ HelloService ขึ้นมาก่อน
public class HelloServiceStub implements HelloService {

public String getGreeting() {
return "hi";
}

}

การนำ stub ไปใช้ ก็ทำได้โดยผ่านการ configuration ด้วย Module class
package demo.services;

public class TestModule {

public static HelloService buildHelloService() {
return new HelloServiceStub();
}
}

โดยชื่อและ package ของ TestModule ต้องสัมพันธ์กับ parameter ที่เรา pass ให้ PageTester
String appPackage = "demo";        
// นำไป solve หา class โดยใช้ชื่อ class = appPackage + ".services." + appName + "Module"
String appName = "Test";
PageTester tester = new PageTester(appPackage, appName, "src/main/webapp");


ส่วนการนำไปใช้จริง เราสามารถเขียนให้ซับซ้อน โดย simulate การ click
หรือการ submit ได้ด้วย ยกตัวอย่าง
        @Test
public void testPage() {
String appPackage = "demo.bid";
String appName = "Test";
PageTester tester = new PageTester(appPackage, appName, "src/main/webapp");

Document document = tester.renderPage("Start");

// ทดสอบ click action link
document = tester.clickLink(document.getElementById("plus"));
// assert ว่าค่าที่ render กลับมา เปลี่ยนไปอย่างที่ต้องการไหม
Node node = document.getElementById("c1").getChildren().get(1);
node = node.getChildren().get(5);
assertEquals("1", node.getChildText());
}

Related link from Roti

Saturday, June 02, 2007

ต้นไม้

ภาพถ่ายของ Jocke Berglund



links

Related link from Roti

Wednesday, May 30, 2007

จ่าย,ไม่จ่าย - ซื้อ, ไม่ซื้อ

Washington Post มีบทความน่าสนใจเรื่อง
Mental Accounting

เขาเกริ่นนำว่า
สมมติว่าเราไปซื้อตั๋วดูหนัง 170 บาท
แล้วเกิดทำตั๋วหนังหายไป
ถามว่า เราจะซื้อตั๋วหนังใบใหม่ไหม

กับอีกสถานการณ์หนึ่ง
สมมติว่าเราไปซื้อตั๋วหนังเหมือนกัน
แต่ก่อนจะซื้อ
เราเปิดกระเป๋าตังค์แล้วพบว่า
เงินเราหายไป 170 บาท
ถามว่า เราจะซื้อตั๋วดูหนังหรือไม่

ด้วยมูลค่าการสูญเสียที่เท่ากัน
(จากการทดลอง พบว่า)
46 % ของกลุ่มแรก ตกลงใจซื้อตั๋ว
88 % ของกลุ่มหลัง ตัดสินใจซื้อตั๋ว

Related link from Roti

Monday, May 28, 2007

Tapestry 5 Ioc

เมื่อรู้ว่า T5 เปลี่ยนวิธี config ไปแล้ว
ผมในฐานะที่มี learning style เป็นแบบที่ต้องทดลองทำด้วยตัวเองจึงจะเข้าใจ,
ก็เลยลองทดสอบ config tapestry5 IoC เปรียบเทียบกับ spring

เริ่มด้วยการสมมติสถานการณ์ก่อน
สมมติว่าเรามี service ที่ใช้ในการ upload content เข้า repository
public interface UploadService {

public void upload(InputStream input, String type);

}

การทำงานภายในของ service นี้ก็คือ
1. ทำการ parse content, ซึ่งวิธี parse ก็ขึ้นอยู่กับ type ของ content
2. delegate การจัดเก็บ content ให้กับ RepositoryService เป็นคนทำ
3. ทำ index ของ content นั้น โดย delegate ให้กับ IndexService เป็นคนจัดการ
public class UploadServiceImpl implements UploadService {

private IndexService indexService;
private RepositoryService repositoryService;
private Map<String, Parser> parserMap = new HashMap<String, Parser>();

public void upload(InputStream input, String type) {
Parser parser = parserMap.get(type);
Document document = parser.parse(input);
Long key = repositoryService.persistent(document);
indexService.index(document, key);
}

public void setIndexService(IndexService indexService) {
this.indexService = indexService;
}

public void setRepositoryService(RepositoryService repositoryService) {
this.repositoryService = repositoryService;
}

public void setParserMap(Map<String, Parser> parserMap) {
this.parserMap = parserMap;
}
}

ถ้าร้อย service ด้วย spring เราจะต้องเขียนดังนี้
<beans>
<bean id="indexService" class="test.service.impl.IndexServiceImpl"/>

<bean id="repositoryService" class="test.service.impl.RepositoryServiceImpl"/>

<bean id="uploadService" class="test.service.impl.UploadServiceImpl">
<property name="indexService" ref="indexService"/>
<property name="repositoryService" ref="repositoryService"/>
<property name="parserMap">
<map>
<entry>
<key><value>pdf</value></key>
<bean class="test.service.impl.PdfParser"/>
</entry>
<entry>
<key><value>word</value></key>
<bean class="test.service.impl.MsWordParser"/>
</entry>
</map>
</property>
</bean>
</beans>


พอมาเป็น Tapestry5 IoC
สิ่งที่เปลี่ยนไปอย่างมาก ก็คือ "ไม่มี xml อีกแล้ว"
เมื่อไม่มี xml, เราก็ต้อง define module descriptor เป็น java แทน
public class MyAppModule {

/**
* bind ใช้กับกรณีพวก simple bean, ไม่ต้องมีอะไรให้ set มากมาย
*/

public static void bind(ServiceBinder binder) {
binder.bind(RepositoryService.class, RepositoryServiceImpl.class);
binder.bind(IndexService.class, IndexServiceImpl.class);
}

/**
* กรณีพวก bean ที่ซับซ้อนมากขึ้น ก็ให้ตั้งชื่อ method ว่า build นำหน้า
* แล้วก็กำหนด parameter ตาม dependency ที่ต้องการ
* โดยลำดับจะเป็นอะไรก่อนหลังก็ได้
*/

public static UploadService buildUploadService(
IndexService indexer,
RepositoryService repositoryService,
Map<String, Parser> configuration) {

UploadServiceImpl impl = new UploadServiceImpl();
impl.setIndexService(indexer);
impl.setRepositoryService(repositoryService);
impl.setParserMap(configuration);
return impl;
}

/**
* ตรงนี้แหล่ะที่ tapestry5 IoC ยังเหนือกว่า Spring
* เราสามารถกำหนด contribute ที่ทำหน้าที่ provide configuration ให้กับ builder ข้างบน
* โดย contribute method สามารถ plugin เข้ามาจาก module อื่นๆได้
* ทำให้มันมีลักษณะเป็น modular มากกว่า spring
*/

public static void contributeUploadService(MappedConfiguration<String, Parser> configuration) {
configuration.add("pdf", new PdfParser());
configuration.add("word", new MsWordParser());
}
}

เวลา run ก็
        public void run() {
RegistryBuilder builder = new RegistryBuilder();
builder.add(MyAppModule.class, ParserModule.class);

Registry registry = builder.build();

UploadService service = registry.getService(UploadService.class);
service.upload(null, "pdf");
}

ขยายความเพิ่มเติมเรื่อง contribution ของ Tapestry5 อีกนิดหนึ่ง
contribution ใน T5 (จริงๆมันมีตั้งแต่ version 4 แล้ว)
มันมีลักษณะเป็น aggregate นั่นคือ มันจะ scan หา contribution ทั้งหมดที่มี
ไม่ว่าจะอยู่ใน jar file หรือใน classpath
จากนั้น จึง aggregate และส่งต่อให้ builder method
,feature ที่ไกล้เคียงสุดของ spring สำหรับกรณีนี้ ก็คือ collection merging (spring version 2)

Note: ทั้ง spring และ tapestry5 IoC ต่างมี feature พวก auto binding ทั้งคุ่
แต่ผมไม่นิยมใช้ เนื่องจากการประกาศว่าต้องใช้อะไรบ้าง มันชัดเจนกว่าในตอนที่ต้องไล่ code เพื่อแก้ไขโปรแกรม

Related link from Roti

Thursday, May 24, 2007

tapestry 5

ช่วงนี้ลูกหลับกลางวันเป็นเวลา เลยได้มีโอกาสลองเล่น tapestry 5
หลังจาก download tutorial มาทดสอบดูแล้ว
ก็พบ issue ที่น่าสนใจดังนี้
1. เดิม tapestry หรือ framework อื่นๆมักจะใช้ servlet
เป็นจุดตั้งต้น framework ของตัวเอง
แต่ tapestry 5 เปลี่ยนไปใช้ filter เป็นจุดเริ่มต้น
<filter>
<filter-name>app</filter-name>
<filter-class>org.apache.tapestry.TapestryFilter</filter-class>
</filter>

<filter-mapping>
<filter-name>app</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

น่าสนใจมาก ว่าการเปลี่ยนแบบนี้จะนำมาซึ่งข้อดีและข้อเสียอะไรบ้าง

2. การเปลี่ยน page เดิมจะใช้วิธี forward เป็นหลัก
แต่ใน tapestry5 จะใช้ redirect เป็นหลักแทน
ซึ่งข้อดี ก็คือการ handle กรณี user กด refresh จะทำได้ถูกต้องขึ้น

3. การเริ่มต้นสร้างโปรเจคใหม่ทำได้ง่ายมาก (อิทธิพลจาก rails)
ด้วยการ integrate กับ maven, ทำให้เรา create new project,
running project(start server) ได้ภายใน 2 command line.

4. loop การแก้ไข + ทดสอบ + แก้ไข + ทดสอบ ...
โดยไม่ต้อง restart server ทำได้เรียบร้อยดี
ยกเว้น case ที่เราเพิ่ม page ใหม่ๆ เข้าไปหลังจาก start server ไปแล้ว
ที่ต้องมีการ restart server ใหม่

5. ระยะเวลาการ start server เร็วกว่าเดิมแบบไม่เห็นฝุ่น
เดิมประมาณ 3-7 วินาที ของใหม่ หลัก 300 millisec
ที่เป็นเช่นนี้เพราะมีการ rewrite Ioc container ใหม่

6. เปลี่ยน Ioc(Inversion of Control) ไปใช้ java code + annotaion
แทน xml file, อันนี้แหล่ะตัวงงเลย เพราะผมใช้ xml ใน spring มาจนเคยตัวแล้ว

Related link from Roti

Wednesday, May 23, 2007

semaphore

ช่วงนี้คุณลูกบ้ารถไฟ คุณพ่อก็เลยต้องบ้าตามไปด้วย
หลังจากอ่านหนังสือประวัติรถไฟ คำแรกที่เห็นแล้วคุ้นตาอย่างยิ่งก็คือ
"semaphore" ซึ่งในทาง programming เราใช้ในการจัดลำดับการใช้ทรัพยากรร่วมกัน
ก็เลยทำให้เกิดความสงสัยว่า ศัพท์คำนี้, พวกชาวคอมพิวเตอร์ไปยืมแนวคิดมาจากไหนอย่างไร

ลองเปิด dictionary ดู ก็พบว่า
semaphore มีรากศัทพ์มาจากสองคำคือ
sema -> sign
phoros -> carrying
รวมกันแล้ว ก็คือ system ที่ใช้ส่งข้อมูลระหว่างกัน โดยอาศัยการมองเห็นเป็นสำคัญ

ใน wikipedia เขาเล่าว่า ฝรั่งเศษ เป็นคนเริ่มใช้ก่อน
โดยเริ่มนำมาใช้อย่างจริงจังในช่วงปี 1790-1795
ซึ่งเป็นช่วงที่เกิดการปฎิวัติ (French Revolution)
โดยเขาจะสร้างสถานี (staion) ใล่กันไป
แต่ละสถานีอยู่ห่างในระยะที่มองเห็นกันได้
เมื่อมี information ที่จะส่ง สถานีที่ส่งก็จะปรับ sign (แล้วแต่ว่าเลือกออกแบบ sign ไว้อย่างไร)
ของตัวเอง, สถานีถัดไปเมื่อมองเห็นก็จะปรับของตัวเองตาม
copy กันเป็นทอดๆไป จนกว่าจะถึงปลายทาง
(จะเห็นว่ามันเหมือนวิธีส่งโทรเลขเลย
ดังนั้นจึงมีชื่อเรียกอีกอย่างว่า optical telegraph)

ลองดูตัวอย่าง sign บ้าง
sign แบบแรกที่เราคุ้นเคย ก็คือสัญญาณธง


ส่วนพวก รถไฟ,
ในยุครถจักรไอน้ำ
เมื่อจำนวนรางที่เชื่อมต่อกันมีมากขึ้น
ปริมาณขบวนรถไฟที่วิ่งมีมากขึ้น
ก็เลยต้องหาวิธีการสื่อสารระหว่างคนขับรถไฟ กับนายสถานีขึ้นมา
เพื่อให้สามารถควบคุมการวิ่งของรถไฟให้มีประสิทธิภาพสูงสุด
รวมทั้งป้องกันไม่ให้เกิดอุบัติเหตุ
ก็เลยมีการ apply นำ semaphore มาใช้


Note: รูปเพิ่มเติมใน flickr

ดูแล้ว semaphore ที่ทาง computer นำมาใช้
จะมาจากแนวพวก semaphore ที่ใช้ในวงการรถไฟ
เนื่องจากเป็นเรื่องของการจัดสรรทรัพยากรที่มีอยู่จำกัด
(ทรัพยากรที่จำกัดของรถไฟ ก็คือราง)
ซึ่งเป็นปัญหาพวก concurrency แบบเดียวกัน

Related link from Roti

Monday, May 21, 2007

unit_of_work

ใน rails เวลาเราเขียน action ส่วนที่เกี่ยวกับการ save, update
หน้าตาของ code ตรงนั้นส่วนใหญ่จะยึดตามแบบนี้
  def create
@role = Role.new(params[:role])
if @role.save
flash[:notice] = 'Role was successfully created.'
redirect_to :action => 'list'
else
render :action => 'new'
end
end

ซึ่งดูแล้วก็รู้เรื่องดี, แต่สำหรับผมเวลาเขียน code ตรงส่วนนี้ทีไรมันรู้สึกขัดๆ

Obie Fernandez ก็คงรู้สึกแบบเดียวกัน
แต่เขาหาวิธีแก้ไขได้เนียนดี
โดยเขาเปลี่ยนวิธี save เป็นแบบนี้แทน
  def create
case unit_of_work do
@role = Role.new(params[:role])
end

when Success
redirect_to :action => 'list'

when ValidationError
render :action => 'new'
end
end

เป็นการเอา block + case statement มา combine กันได้สวยดี
โดยตัว unit_of_work เป็น method ที่รับ block เข้าไป

Related link from Roti

Wednesday, May 16, 2007

งัวเงีย

เมื่อคืนขณะกำลังหลับลึก ก็ได้ยินเสียงหวานดังแว่วๆมา "พลวัฒน์คะ"
ก่อนที่จะเริ่มเคลิ้มไปตามเสียงหวานๆ ก็ได้ยินเสียงที่ตามมาว่า "ชงนมให้ลูกหน่อยค่ะ"
เท่านั้นแหล่ะ, กล้ามเนื้อท้องก็ทำงาน ดีดตัวขึ้นมานั่งโดยอัตโนมัติ

ขั้นแรกสุดในการชงนม ก็คือต้องหาขวดนมให้ได้ก่อน
เนื่องจากอุปสรรคเรื่องความมืด + สายตายังไม่ปรับ focus + สมองที่ยังงัวเงียอยู่
algorithm ที่ดีที่สุดในการหาขวดและจุกนมในความมืด ก็คือ brutal force
หลังจากคุ้ยและคลำกองขวดนมอยู่นานมาก (นานในความรู้สึก เพราะมันอยากกลับนอนเต็มที่แล้ว) ก็ได้ขวดนมขนาดที่เหมาะสำหรับเด็กอ่อนออกมา
ชั้นที่สอง ก็คือ ตวงน้ำให้ได้ 2 Oz. อันนี้ยากขึ้นอีก
เพราะว่า ต้องกัดฟันบังคับให้สมองส่วนที่เกี่ยวกับ "มิติ" มันยอมทำงาน
เมื่อได้ขวดนมพร้อมน้ำแล้ว ก็จัดการตักนมผงขึ้นมาหนึ่งช้อน
(ไม่ยากแล้ว เพราะ function "มิติ" มันทำงานแล้ว)
ผสมเข้าด้วยกันแล้วก็เขย่าๆ
ส่งต่อให้ภรรยา จากนั้นก็กระโดดกลับขึ้นเตียง
(หลังจากที่เขียนเรื่องนี้แล้ว คำถามที่ตามมาอีก ก็คือ
เอ๊ะ แล้วทำไม แฟนเราถึงไม่ชงนมเองหว่า)

ก่อนหลับไป ก็นึกว่า
เมื่อรู้แล้วว่าต้องชงนม
แล้วทำไมก่อนนอนตูไม่หยิบขวดนม + จุกนม
ใส่น้ำ,วางเตรียมไว้ให้เรียบร้อยก่อน
จะได้ไม่ต้องมาทุกข์ทรมานอย่างนี้วะ

ปล. เรื่องนี้สามารถเปลี่ยนบริบท จากชงนม
ไปเป็น software development ได้โดยไม่เสียความหมาย

Related link from Roti

Monday, May 14, 2007

น้องใหม่มาแล้ว

เหนื่อยสุดๆเลยช่วงอาทิตย์นี้ (และคงจะอีก 12 อาทิตย์ข้างหน้า)
ลูกชายคนเล็กเข้าบ้านวันแรก ก็เล่นเอาพ่อกับแม่บักโกรกไปตามๆกัน

ลาคลอดครั้งนี้ (ฝ่ายชายก็ลาคลอดได้ คราวก่อนลาไปเก้าเดือน, คราวนี้คงลาแค่ 3 เดือน)
ผมรับหน้าที่หลักคือดูแลคนโต
จับกินข้าว,อาบน้ำ, ล้างก้น, เล่นทุกรูปแบบ, เล่านิทาน, พาไปเที่ยว
ส่วนลูกคนเล็ก ผมรับหน้าที่ อาบน้ำ กับ หมั่นตื่นมาดูในช่วงตีสองเป็นต้นไป

Update; เมื่อคืนลูกคนเล็กมันรับน้องพ่ออีกแล้ว
ต้องอุ้มเดินหกทุ่มถึงตีหนึ่ง
แล้วก็ตีห้าถึงแปดโมงเช้า
ส่วนตีสอง เป็นเวรของลูกคนโต ตื่นขึ้นมาอิจฉาน้อง
ต้องนอนปลอบกันเกือบครึ่งชั่วโมง

Related link from Roti

Tuesday, May 08, 2007

สกีน้ำ กับ J2EE

เห็นเจ้า rerngrit เขาไป เล่นสกีน้ำ แล้ว
ประเด็นที่ผมประทับใจสำหรับกระบวนการเล่นสกีน้ำ ก็คือวิธีการหัดเล่น
ผมชอบความรู้สึกตอนหัด ที่ค่อยๆมีแบบฝึก จากง่ายไปหายาก
ในแต่ละ step จะมีโจทย์ให้เราแก้แค่ไม่กี่เรื่อง
ทำให้เราสามารถค่อยๆ step ขึ้นไป,
รู้สึกถึง achievement
ซึ่งกลายมาเป็นตัวล่อหลอกที่ทำให้เราอยากเล่นแบบที่ยากขึ้นไปอีก

สมัยผมหัด ขั้นตอนการหัดโดยคร่าวๆ จะเป็นแบบนี้
  • นั่งเล่น
    เป็นการเริ่มต้นเรียนวิธีที่เชือกดึงตัวเรา, เรียนรู้ว่าเส้นทางที่วิ่งเวลาเข้าโค้ง
    ควรจะรักษาเส้นทางอย่างไรให้เชือกตึงตลอดเวลา ไม่เช่นนั้นจะเกิดอาการกระชากอย่างแรงได้
  • สกีสองขา
    การหัดจะเริ่มจากการ start โดยการนั่งออกไป (เนื่องจากเรานั่งเป็นแล้ว ขั้นนี้เลยไม่ยากแล้ว)
    จากนั้นระหว่างทางก็ให้พยายามยืนขึ้นให้ได้
    ความยากก็คือ ตอนยืนขึ้น กับการเข้าโค้งในลักษณะยืนครั้งแรก
  • สกีสองขาแต่ถอดทิ้งระหว่างทาง 1 อัน
    ลดความยากของการออก start ด้วยขาเดียว โดยการออก start สองขา
    จากนั้นพอยืนได้ ก็ให้ค่อยๆทอดทิ้งขาที่ไม่ถนัดทิ้งไป แล้วก็นำเท้าไปวางไว้หลังเท้าที่ใส่สกีไว้
    ความยาก จะอยู่ตรงอาการเก้ๆกัง ของการถอดกับการการวางเท้า
    เพราะพอถอดเสร็จแล้ว ระหว่างที่ขาที่ลอยยังไม่ได้วางลงไป, balance มันจะเสียได้ง่าย
  • สกีขาเดียว
    ยากตอนออก start, เพราะพวกนั่งกับพวกสองขา เราสามารถออก start โดยการนั่งรอบนทางวิ่งบนบกได้
    แต่พวกขาเดียว ต้องนั่งบนระเบียงไม้ จากนั้น เมื่อเชือกเริ่มดึงก็ต้องทิ้งตัวลงจากระเบียงไม้ในจังหวะที่เหมาะสม
  • wakeboard อันนี้ผมไปไม่ได้เล่น wakeboard ของแท้นะ ของมันแพง สมัยก่อนไม่มีของส่วนกลางให้เล่น
    แต่เคยเล่น อันที่แบนๆ เล็กๆ , ความยากก็คือ มันลื่นชิบเป๋งเลย
    หัดใหม่, เจอทางที่มีพริ้วคลื่นๆเล็กๆ ก็ตกน้ำป๋อมแป๋มแล้ว


นึกเปรียบเทียบกับสายอาชีพเราแล้ว
process การหัดสกีน้ำ จากขั้น novice -> intermediate
มันมีแต่ความสนุก, ความท้าทาย กับ เอาอีกๆ
ส่วน process การหัด J2EE จาก novice -> intermediate
มันมีแต่ความมึนงง, สับสน, อุปสรรคที่ทำให้ท้อถอย, กับคำพูดในหัว "เอ๊ะเรามาถูกสายอาชีพหรือเปล่า"

Related link from Roti

Friday, May 04, 2007

Rock Climbing

เห็น roofimon comment มาชวนปีนเขา แถมยังมี climbing gym ส่วนตัวที่บริษัทฯด้วย
ก็เลยนึกครึ้มใจ เขียนโฆษณาชักชวนให้เหล่าโปรแกรมเมอร์ทั้งหลายมาปีนหน้าผากันดีกว่า

ผมเลยรู้สึกจักกีฬาปีนเขา เมื่อสักราวๆปี 2537 ได้มั้ง
จำได้ว่า ไปเที่ยวหาดไร่เลย์ กระบี่ (ซึ่งสมัยนั้นติด 1 ใน 10 ของสถานที่ปีนที่ดีที่สุดในโลก)
แล้วเห็นฝรั่งปีนกันขวักไขว่ ก็เลยทดลองซื้อบริการไกด์ปีนเขาดูบ้าง

จำบรรยากาศการปีนครั้งแรกได้แม่นเลย
ความยากระดับ 5 (ง่ายสุด)
เหงื่อนี้ไหลออกมาเป็นปี๊ปเลย (ผมกลัวความสูง)
รำพึ่ง, ไกด์ + อาจารย์ ซึ่งเป็นสาวสวยผมยาวหุ่นดี ในชุดสายเดี่ยวและกางเกงขาสั้นจู๋
ก็คอยให้กำลังใจและบอกบทอยู่ข้างล่าง
left foot in a right hole, ...
(ใครที่ไปกระบี่ แล้วไปนั่งดูเขาหัดปีนกัน จะรู้จักรูปประโยคแบบนี้ดี)
กว่าจะปีนขึ้นไปถึง ก็พาลจะเป็นลม ด้วยอาการขาดน้ำ
ต่อเมื่อปีนเป็นแล้ว ถึงรู้ว่า ถ้ารู้หลัก + ไม่เกร็งและกลัวจนเกินไป
การปีนขึ้นเส้นนั้นมันจะง่ายพอๆกับการเดินเล่น

หลังจากกลับมากรุงเทพฯ ก็มีเหตุบังเอิญอีก ก็คือ
ที่ตึกที่ทำงาน(Timesquare) เกิดมีการติดตั้งหน้าผาจำลอง อันแรกของประเทศไทยขึ้นมา
(ออกแบบและทำจากฝรั่งเศษ เจ๋งมาก)
ก็เลยเกิดกลุ่มปีนหน้าผาในกรุงเทพขึ้นมา
เสียดายที่หน้าผานี้ ถูกตั้งอยู่ไม่นานนัก (แต่ก็นานพอ ให้เกิดการแข่งขันได้ 1 รายการ)
ยังจำคำชมของน้องคนหนึ่งได้
"พี่นี่ท่าปีนเท่ห์ดีนะ เวลาอยู่เตี้ยๆ แต่ทำไมพอขึ้นไปสูงๆ แล้วหมดท่าได้"
ผมก็ตอบไปสั้นๆว่า
"ผมกลัวความสูงโว้ย"
สำหรับผม แค่การขึ้นไปสูงเกิน 3 เมตร ระบบหายใจผมก็จะเริ่มผิดปกติแล้ว

ตอนนั้นแม้จะไม่ค่อยมีที่ปีน
แต่ก็เริ่มสะสมอุปกรณ์ปีนกันบ้างแล้ว
ที่ภูมิใจสุดก็คือเชือก เส้นละ 500 บาท
ที่แฟนของอาจารย์+ไกด์สาว ช่วยหักคอซื้อจากฝรั่งที่ไร่เลย์มาให้
(เพิ่มเติมสำหรับผู้ไม่สัดทัดเรื่องอุปกรณ์ปีนเขา, เชือกสำหรับปีน
มันจะมีอายุการใช้งาน ซึ่งนอกจะเกี่ยวกับเวลาแล้ว ยังเกี่ยวกับ
จำนวนครั้งที่เกิดการตกกระชากของคนปีนด้วย, ซึ่งเชือกสมัยนั้น
มันกำหนด hard fall ไว้แค่ 1 ครั้ง
การใช้เชือกเส้นที่ไม่รู้ประวัติว่าตกมากี่ครั้งแล้ว
ก็ดูจะเป็นการฆ่าตัวตาย ในสายตาของฝรั่งได้)

สมัยนั้นที่ปีนไม่ค่อยมีมากนัก
อยากปีนก็ต้องไปกระบี่
ก็เลยกลายเป็นกิจวัตรว่า
ทุกๆหยุดยาว เราจะลงไปปีนที่กระบี่กัน

มีอยู่ครั้งหนึ่ง ขณะที่กำลังฝึกซ้อม bouldering (ไม่ใช้เชือก และไม่ได้ปีนขึ้นสูง)

ผมกำลังเกร็งตัวปีน ข้ามไปมาระหว่างผนังด้านซ้าย กับด้านขวา
ในจังหวะที่กำลังค้างหยุดพัก ให้มือมันหายชา พร้อมกับไล่ลมหายใจที่เกร็งค้างไว้ในช่องท้อง
ผมก็ได้ยินเสียงพ่อกับลูกคุยกันมาจากข้างหลัง
"พ่อๆ เขากำลังทำอะไรอยู่"
เสียงพ่อตอบ
"เขากำลังเก็บรังนกอยู่ลูก"

Related link from Roti

Friday, April 27, 2007

Flex Ecosystem

หลังจากที่ได้ข่าว Opensource Flex
ก็เลยต้องไปขวนขวายหามาว่า อะไรที่มัน opensource บ้าง
อันนี้คือภาพ sketch ที่ได้จากการฟัง podcast Architectore of flash
สีเขียว ก็คือ opensource (เฉพาะของ Adobe)

Related link from Roti

Thursday, April 26, 2007

ZX81 - 25 ปี

เผลอหน่อยเดียว คอมพิวเตอร์เครื่องแรกของผม ZX81
ก็มีอายุเกิน 25 ปีไปแล้ว
สำหรับคนที่เกิดไม่ทัน
spec ของเครื่องก็คือ
  • CPU Z80A 3.25MHz.
  • RAM 1K
  • LANGUAGE BASIC
  • UHF output for display on TV set.

จำตอนแกะกล่องได้แม่นเลย
ตื่นเต้นมาก ตอนต่อสายกับ Tv.
คาดหวังว่าจะเจอคอมพิวเตอร์พูดได้แบบ 2001 Space Odyssey
กลับกลายเป็นเจอ สี่เหลี่ยมดำๆ กระพริบได้บนหน้าจอแทน
แถมพอกดปุ่ม keyboard
กดเท่าไร ก็ได้แต่คำว่า Syntax Error กลับมา
สุดท้ายก็ต้องปิดเครื่อง แล้วก็สมัครไปเรียนเขียนโปรแกรม
จำได้ว่า เขาสอนให้ฟรีที่ siam center.

จะว่าไป ชีวิตผมเปลี่ยน ก็เพราะไอ้เจ้าเครื่องนี้แหล่ะ

Related link from Roti

Sunday, April 22, 2007

น่าอ่าน

บิณฑบาตเรือและเรื่องที่เกี่ยวข้อง
วงศาวิทยาสังเขปว่าด้วยสถาปัตยกรรมภูมินิทัศน์ถิ่น

เกริ่นนำด้วย
คุณยายแนะนำชื่อตัวเองอย่างนี้ "พ่อฉันไปอำเภอใส่ชื่อฉันว่าช้อยให้คล้องกับชื่อแกเฉื่อย แต่ย่าเรียกฉันว่า เหรย "และได้กล่าวถึงยายเขียวซึ่งเคยมาช่วยแกเลี้ยงลูกคนที่สองว่า "แกอ่านหนังสือไม่ออก แกมีหีบไม้สักใส่หนังสือที่แกเก็บสะสมในจำนวนนั้น เล่มหนึ่งคือหนังสือพุทธทำนายแกเอามาให้ฉันอ่านให้ฟัง" และตอนที่คุณยายชอบจำได้แม่น

"..แผ่นดินจะเป็นริ้วปลา ท้องฟ้าจะเป็นใยแมงมุม มีทางแต่จะไม่มีคนเดิน
บ้านเมืองจะเป็นกล่องไม้ขีด พ่อแม่ลูกจะพลัดพรากกัน....."

ยายเขียวยังย้ำกับยายว่า " ….อายุฉันจะไม่ได้เห็นหรอก แต่แม่เหรยจะได้เห็น"

ใครอยากรู้ว่า แผ่นดินเป็นริ้วปลา, ท้องฟ้าเป็นใยแมงมุม, ทางที่ไม่มีคนเดิน
ที่เป็นจริงแล้วในปัจจุบันเป็นอย่างไร ตามไปอ่านได้เลย

Related link from Roti

Friday, April 20, 2007

Vuze กับเขาด้วย

ใน blognone มี Review Vuze Beta
เห็นแล้วก็ตื่นเต้นไปกับเขาด้วย
แต่ที่ตื่นเต้นไม่ใช่ความสวย หรือเรื่อง load bit.
แต่เป็นเรื่องการ integrate browser เข้ากับ SWT Application

จริงๆแล้ว SWT มี browser widget มานานมากแล้ว
แต่มันก็มี feature ที่จำกัดอยู่ เช่น ไม่สามารถสื่อสารระหว่าง javascript กลับมายัง Java Application
ที่ห่ออยู่ข้างนอกได้

ในหน้าจอของ Vuze ที่ mk capture มา
ผมแอบเห็นว่า มันน่าจะส่ง message คุยกันระหว่าง browser กับ Java ได้
ก็เลยไปไล่ source code ของ Vuze ดู ว่าเขา implement มันอย่างไร

จากการไล่ source code ดู ก็พบว่า
เขาพยายาม implement Message Architecture ที่คุยได้ทั้งสองทาง
คือจาก java ไป Browser และจาก Browser ไป Java

โดยจากฝั่ง Java ไป Browser เขาใช้กลไกปกติ ก็คือ
method execute ใน org.eclipse.swt.browser.Browser

ส่วนขากลับ นี่สิ เส้นผมบังภูเขา
(ผมเคยอยากได้ feature นี้เมื่อ 4 ปีก่อน แต่สุดท้ายก็หาทางทำไม่ได้)
เขาใช้วิธีส่ง message ผ่านทาง status bar ของ browser
อันนี้เป็น javascript ของเขา
function sendRealMessage ( message ) {
sequence++;
window.status = 'AZMSG' + ';' + sequence + ';' + message;
window.status = '';
}

ส่วนในฝั่ง java ก็แค่ add StatusTextListener เข้ากับ browser widget
แล้วก็เช็คดูว่า มี message ที่มี prefix 'AZMSG' หรือเปล่า
ถ้าใช่ก็ feed เข้า Message channel ต่อไป

Related link from Roti

Wednesday, April 18, 2007

Highlight Current Symbol

เวลาใช้ emacs ไล่ source code ของคนอื่น หรือของตัวเองก็เถอะ
สิ่งที่ผมขัดใจ ก็คือ feature ที่จะช่วย highlight symbol ที่เรากำลังสนใจ
มันก็มีคล้ายๆกัน แต่ยังไม่ตรงกับที่อยากได้นัก
คืออยากได้แบบ แค่วาง cursor ลงไป มันก็ highlight ให้เลย

ถึงวันนี้ก็สมหวังแล้ว เพราะมีคน release library ที่ทำแบบนี้ออกมาแล้ว
ดูได้ที่ light-symbol 0.1

Related link from Roti

Thursday, April 12, 2007

หน้าจอทำงาน

คงผ่านตา ข่าวใน blognone เรื่อง "การเพิ่มพื้นที่แสดงผลช่วยเพิ่มประสิทธิภาพการทำงาน"
ผมเป็นคนหนึ่งหล่ะที่ visualize ภาพในใจไม่เก่ง
ต้องอาศัยการมองเห็น มาเป็นตัวช่วยในการคิดและเรียนรู้

เมื่อก่อน สมัยจอยังเล็กๆธรรมดาอยู่
เวลา design ก็ต้องพึ่งกระดาษแผ่นใหญ่ๆ วางให้เต็มโต๊ะ แล้วก็ sketch ขีดๆเขียนๆไป

มาปัจจุบันจอมันใหญ่มากแล้ว
ก็เลยกัดฟันลงทุนซื้อจอใหญ่ (นอนก่ายหน้าผาก คิดอยู่นานมาก)
แต่ผลของมันก็คุ้มค่านะ
ที่เห็นผมเรียนรู้เรื่อง rails, erlang, haskell, ... จนไปพูดใน blognone techday ได้
ปัจจัยหนึ่งที่ช่วยสนับสนุน ก็คือ เจ้าจอตัวนี้นั่นแหล่ะ

ว่าแล้วก็ขอ show หน้าจอทำงาน (เมื่อก่อนจะรู้สึกอาย ว่าจอมันไฮโซไปหน่อย)
ปกติ ผมจะเปิด desktop ไว้ 4 workspace
อันนี้ capture งานที่ทำวันนี้

workspace แรก เปิด eclipse



workspace สอง เป็นพวก design



workspace สาม เป็นพวก document



workspace สี่ เป็น พวก console



Note: จะสังเกตุว่าไม่มีพวก IM ใช้ เพราะแก่แล้ว (ไม่ถนัดโต้ตอบแบบ online)

Related link from Roti

Wednesday, April 11, 2007

MandelBrot Set ด้วย Haskell

จากโจทย์ใน codenone ที่ Implement ด้วย Ruby ไปแล้ว
คราวนี้มาลอง implement ด้วย haskell บ้าง

เริ่มด้วย algorithm การคำนวณค่า
ถ้าเป็น ruby เขียนแบบนี้
def is_mandelbrot(x,y) 
x0 = x
y0 = y
x2 = x*x
y2 = y*y

iter = 0
maxiter = 30

while ( x2 + y2 < 4 && iter < maxiter )
y = 2*x*y + y0
x = x2 - y2 + x0
x2 = x*x
y2 = y*y
iter += 1
end

if iter == maxiter || iter == 0
0.0
else
iter.to_f * 100 / maxiter
end

end


ใน imperative language จะมีพวก while loop ให้ใช้
แต่ใน haskell ไม่มี loop แบบนี้
ดังนั้นเราต้องแปลงให้เป็น tail-recursive แทน

max_iter = 30
is_mandel x y = is_mandel' x y max_iter x y
is_mandel' x y cnt x0 y0
| cnt == 0 = 0.0
| exceed = (max_iter - cnt) / max_iter
| otherwise = is_mandel' (x2-y2+x0) (2*x*y+y0) (cnt-1) x0 y0
where x2 = x * x
y2 = y * y
exceed = x2 * y2 > 4


ขั้นถัดไปก็คือคำนวณว่าจะ render ภาพอย่างไร
โดยในโจทย์ เขาจะให้เรากำหนด coordinate ของมุมบนซ้าย กับมุมล่างขวา
จากนั้นก็ให้เราคำนวณรูป โดยกำหนดว่า resolution ของการ plot ต้องมีอย่างน้อย 200 pixel,
ถ้าเขียนด้วย python จะเขียนดังนี้ (code นี้คุณสุกรีเป็นคนเขียน)
def mandelbrot(ul,lr,callback,step=(200,200),maxiter=255,limit=2):
xs,ys = step
x0,y0 = ul.real,ul.imag
x1,y1 = lr.real,lr.imag
xd,yd = (x1-x0)/xs,(y1-y0)/ys
i = 0
y = y0
for i in range(ys):
x = x0
for j in range(xs):
color = mandelbrot_point(x,y,maxiter,limit)
callback(i,j,x,y,color)
y += yd

จะเห็นว่ามี for loop ซ้อนกัน 2 ชั้น
haskell ก็ไม่มี for loop ให้ใช้เหมือนเดิม
ดังนั้น เราจะเปลียนแนวคิดไปเป็น List แทน
เริ่มโดยหาจุดที่เป็นไปได้บน แกน X ก่อน
โดยต้องการ list หน้าตาแบบนี้
[(-2,0),(-1.98,1),(-1.96,2),....]
ตัวแรกใน tuple คือ ค่าที่จะนำไปคำนวณสมการ
ส่วนตัวที่สองใน tuple ก็คือค่าที่ใช้ plot จุดบนแกน x
loopX startX endX = take w $ iterate (\(x,x') -> (x + stepX, x' + 1)) (startX,0)
where stepX = (endX - startX) / width
w = floor width

take คือการดึงค่าออกจาก list ตามจำนวนที่ระบุ เช่น
take 3 [1,2,3,4,5] ก็จะได้ [1,2,3]
ส่วน iterate มันอธิบายยาก ลองดูตัวอย่าง
iterate (\x -> x + 1) 1 ได้ผลลัพท์คือ [1,2,3,4,... จน infinity]
iterate (\x -> x + 2) 1 ได้ผลลัพท์คือ [1,3,5,7,...]
ดังนั้น code ข้างบน ก็อ่านได้ว่า
"ดึง element ออกมาจาก list ตามจำนวน pixel ที่เราจะ plot"
ซึ่งก็คือ 200 element

จากนั้นก็หาจุดที่เป็นไปได้บน แกน Y
loopY startY endY = take h $ iterate (\(y,y') -> (y - stepY, y' + 1)) (startY,0)
where stepY = (startY - endY) / height
h = floor height


นำ list ของแกน X และ แกน Y
มาทำ cartesial product โดยใช้ List Comprehension
โดย sx คือ x ที่มุมบนซ้าย
ex คือ x ที่มุมล่างขวา
sy คือ y ที่มุมบนซ้าย
ey คือ y ที่มุมล่างขวา
mandelset sx sy ex ey = [(x',y',is_mandel x y) | (x,x') <- (loopX sx ex), (y,y') <- (loopY sy ey)]

ถึงขั้นนี้ เราก็ได้ผลลัพท์ออกมาเป็น list ของ tuple แบบนี้
[(x coordinate,y coordinate, ค่าที่ได้จากการคำนวณว่าอยู่ใน set ของ mandelbrot )]
ซึ่งสามารถนำไป plot ได้แล้ว

ขั้นตอนที่ยากอีกขั้นของ Haskell ก็คือ จะเลือกใช้ Graphic library อันไหนดี
เพราะมันมีให้เลือกเยอะ,
แต่ที่เหมือนกันหมดทุกเจ้าก็คือ
ถ้าอยากรู้ว่าทำงานอย่างไร ก็ให้อ่าน source code ของ example เอา

ผมตกลงเลือกใช้ Graphics.UI.GLUT
(หลังจากเลือกไปตัวหนึ่งแล้ว แต่มัน run ช้ามาก,
กับอีกตัวหนึ่งที่ install ไม่ผ่าน)

ปัญหาของโปรแกรมในส่วนของ UI ก็คือ State
เนื่องจาก Haskell มันเป็น pure functional
การจัดการกับ state ก็เลยต้องใช้ Monad เข้ามาช่วย
ลองดูการ define state
โดยผมจะเก็บค่า มุมบนซ้ายกับมุมล่างขวา
ซึ่งค่านี้จะเปลี่ยนไปเรื่อยๆ เมื่อ user กด zoom

data State = State { sx, sy, ex, ey :: IORef Float }

makeState :: IO State
makeState = do
sx <- newIORef (-2.0)
sy <- newIORef 2.0
ex <- newIORef 2.0
ey <- newIORef (-2.0)
return $ State { sx = sx, sy = sy, ex = ex, ey = ey }


เวลาจะ get ค่า ก็ทำแบบนี้
sx <- get (sx state)
ส่วนเวลาจะ set ค่า ก็ต้องทำแบบนี้
writeIORef (sx state) val

code ที่เหลือเป็น code ที่เกี่ยวกับ open-gl แล้ว
มีหน้าตาประมาณนี้ (ไม่ได้แสดงส่วนที่เกี่ยวกับการ zoom)
display :: State -> DisplayCallback
display state = do
clear [ ColorBuffer ]
sx <- get (sx state)
sy <- get (sy state)
ex <- get (ex state)
ey <- get (ey state)
renderPrimitive Points $ do
mapM_ plot $ mandelset sx sy ex ey
swapBuffers

reshape :: ReshapeCallback
reshape size@(Size w h) = do
viewport $= (Position 0 0, size)
matrixMode $= Projection
loadIdentity
ortho2D 0 (fromIntegral w) 0 (fromIntegral h)
scale 1 (-1) (1::GLfloat)
translate (Vector3 0 (-400) (0 :: GLfloat))


keyboardMouse :: State -> KeyboardMouseCallback
keyboardMouse state (MouseButton b) Down _ (Position x y) = case b of
LeftButton -> zoom state x y
_ -> return ()
keyboardMouse _ _ _ _ _ = return ()

main :: IO ()
main = do
(progName, _args) <- getArgsAndInitialize
initialDisplayMode $= [ DoubleBuffered, RGBMode ]
initialWindowSize $= Size (floor width) (floor height)
createWindow progName
myInit
state <- makeState
displayCallback $= display state
reshapeCallback $= Just reshape
keyboardMouseCallback $= Just (keyboardMouse state)
mainLoop

Related link from Roti

Tuesday, April 10, 2007

สะพานลอยหน้า Major

7 เดือนก่อน :
ผม: วันก่อนผมแบบจักรยานข้ามสะพานลอย Major,
เห็นพวก california fitness ~50 คน
กำลังปั่นจักรยานกันใหญ่เลย, ได้ feeling มากเลย
(ประมาณว่า เราขี่จักรยานจริงๆ พวกนั้นทำอะไรกันอยู่, โลกอยู่ข้างนอกนี่)
revolution: (เจ้าของร้านจักรยาน และเจ้าของ host server ที่ support rails) ยิ้ม...

5 เดือนก่อน :
roofimon: พี่ ผมเห็นพี่บนสะพานลอยคนข้ามหน้า Major ด้วย
ผม: เออ ผมกำลังแบบจักรยานกลับบ้านน่ะ

เมื่อวาน :
ขณะกำลังแบกจักรยานข้ามสะพานลอยหน้า Major
RerngIt: พี่ป๊อก พี่ป๊อก
ผม: เฮ้ยมาได้ยังไง
RerngIt: ผมย้ายมาอยู่แถวนี้แล้วพี่

เมื่อเช้า :
ไม่ห่างจากสะพานลอยหน้า Major
ตำรวจ 2 คนยืนเฝ้าตู้โทรศัพท์ที่โดนระเบิด
ทหารอีกคนกำลังกึ่งเดินกึ่งวิ่ง พร้อมกับพูด ว. ไปด้วย
เศษกระจกแตกเกลื่อนพื้น
ผมประคองจักรยานผ่านหน้าตู้ด้วยความระมัดระวัง ด้วยความกลัวยางแตก
จากนั้นก็ลงเดินแล้วแบกจักรยานข้ามสะพานลอย

Related link from Roti

Monday, April 09, 2007

Thrift

Note:
ไปเจอจาก bookmark ของ bact' บน del.icio.us
น่าสนใจดี ตรงตามที่อยากได้
เลยชิงเขียนถึงก่อน

Thrift มีแนวคิดคล้ายๆ COBRA นั่นคือ เข้ามาช่วยเราในเรื่อง Distributed Application
แต่ไม่ over spec เหมือน COBRA
ตัวมันจะเล็กๆ เบาๆ กว่า

ที่ผมชอบก็คือ มันเปิดโอกาสให้ผม เชื่อม ruby, python, java, php
เข้าหากันในลักษณะ remote procedure call ได้

วิธีการใช้ ก็คือ เราต้องเขียน definition ของ interface, parameter ที่เราต้องการใช้ ด้วย syntax ของ Thrift เองก่อน
(แบบเดียวกับที่เราต้องเขียน IDL file ใน COBRA)
จากนั้นก็สามารถจะ generate stub สำหรับฝั่ง server หรือฝั่ง client
บนภาษาที่เราต้องการได้
โดยขณะนี้ Thrift support การ generate java,cpp,php,python,ruby

ลองดูตัวอย่าง
เริ่มด้วยการเขียน definition file
#!/usr/local/bin/thrift -cpp -java -py -php -rb -r

struct Address {
1: string province
2: string zipCode
}

struct Person {
1: i32 id,
2: string name,
3: Address address
}


service PersonService {
Person getById(1:i32 id),
void create(1:Person person)
}


ลอง implement server ด้วย java
public class MyServer implements PersonService.Iface {

static Hashtable<Integer, Person> map = new Hashtable<Integer, Person>();

public void create(Person person) throws TException {
map.put(person.id, person);
}

public Person getById(int id) throws TException {
return map.get(id);
}

public static void main(String[] args) throws TTransportException {
PersonService.Processor proc = new PersonService.Processor(new MyServer());
TServerSocket tss = new TServerSocket(9090);
TThreadPoolServer server = new TThreadPoolServer(proc, tss);
server.serve();
}
}


ลอง implement client ด้วย ruby
#! /usr/bin/env ruby

require 'thrift/transport/tsocket'
require 'thrift/protocol/tbinaryprotocol'

require 'PersonService'

transport = TBufferedTransport.new(TSocket.new('localhost',9090))
protocol = TBinaryProtocol.new(transport)
client = PersonService::Client.new(protocol)

transport.open()

p = Person.new
a = Address.new
a.zipCode = '10900'
a.province = 'Bangkok'
p.address = a
p.name = 'pok'
p.id = 1

client.create(p)

np = client.getById(1)
puts np.name
puts np.address.zipCode


สุดท้ายลอง implement client ด้วย python
#!/usr/bin/env python

import sys
sys.path.append('..')

from person import PersonService
from person.ttypes import *

from thrift import Thrift
from thrift.transport import TSocket
from thrift.transport import TTransport
from thrift.protocol import TBinaryProtocol


transport = TSocket.TSocket('localhost',9090)
transport = TTransport.TBufferedTransport(transport)
protocol = TBinaryProtocol.TBinaryProtocol(transport)

client = PersonService.Client(protocol)

transport.open()

p = client.getById(1)
print p.name, p.address.zipCode

Related link from Roti

Thursday, April 05, 2007

ruby ช้า

ใน codenone คุณสุกรีตั้งโจทย์ Mandelbrot Set ไว้
แน่นอนเมื่อตั้งตัวเป็นผู้สนับสนุน ruby แล้ว
ก็ต้องทดลอง implement ด้วย ruby ดู (haskell เข้าคิวอยู่)

เริ่มแรกสุด ลอง implement algorithm โดยใช้ Datatype ที่มากับ ruby
require 'complex'
def is_mandel(z)
max = 30
cnt = 0
c = z
while (z.abs < 2 && cnt < max)
z = z ** 2 + c
cnt += 1
end
cnt == max ? 0 : cnt
end

สั้นดี ได้ใจความ
ทดลอง run ด้วย loop นี้
def bm()
step = 0.005
x = 0.0
while x < 1.0
y = 0.0
while y < 1.0
is_mandel(Complex.new(x,y))
y += step
end
x += step
end
end

ผลออกมาเกินห้ามใจ
นี่ขนาดคำนวณ แค่ 40000 จุด (200x200 pixel)

user system total real
complex 23.180000 0.260000 23.440000 ( 26.981269)


ปรับไปใช้ algorithm ใน wikipedia
def is_mandel2(x,y)
x0 = x
y0 = y
x2 = x*x
y2 = y*y

cnt = 0
max = 30

while (x2 + y2 < 4 && cnt < max)
y = 2*x*y + y0
x = x2 - y2 + x0

x2 = x*x
y2 = y*y

cnt += 1
end
cnt == max ? 0 : cnt
end


ผลลัพท์ ดีขึ้นหน่อย ดีกว่าเกือบ 4 เท่า
แต่ก็ยังช้าอยู่ดี

user system total real
wikipedia 5.440000 0.080000 5.520000 ( 6.913239)


สุดท้ายก็เปลี่ยนไปใช้ rubyinline
เพื่อจะเห็นแสงสว่างที่ปลายอุโมงค์บ้าง
inline do |builder|
builder.c "
int is_mandel3(double x, double y) {
double x0 = x;
double y0 = y;
double x2 = x * x;
double y2 = y * y;
int cnt = 0;
int max = 30;
while (x2 + y2 < 4 && cnt < max) {
y = 2*x*y + y0;
x = x2 - y2 + x0;
x2 = x*x;
y2 = y*y;
cnt++;
}
return cnt == max ? 0 : cnt;
}"

end

ผลลัพท์ที่ได้ ก็อย่างที่ทุกคนคงคาดไว้

user system total real
inline 0.220000 0.000000 0.220000 ( 0.257189)


Put the right man on the right job.
ภาษาก็เหมือนกัน

Related link from Roti

อำนาจ

รัฐมนตรี ICT คนนี้ใช้ได้เหมือนกันนะ
ตรงที่ทำให้คน IT หลายคน ไดัสัมผัสกับ "อำนาจนิยม"

สิ่งที่พวกเราซึ่งเป็นชนชั้นกลางเจอ ก็อยู่ในขั้นแค่ "รำคาญ" ที่ไม่ได้ดู "ความบันเทิง"
ซึ่งเทียบไม่ได้กับ สิ่งที่กลุ่มคนชั้นล่างได้พบเจอ
ไม่ว่า จะเป็น คนมุสลิมในสามจังหวัดชายแดนภาคใต้
ที่เจอประเด็นเรื่อง รัฐหรือผู้เป็นตัวแทนของรัฐ ไม่มีความเคารพในความแตกต่างของ ความเชื่อ,ศาสนา
หรือ ชาวบ้านที่อาศัยแม่น้ำมูล เป็นแหล่งจับปลา ที่โดนคุกคาม โดยเชื่อนปากมูล ในแบบที่ต้องเปลี่ยนวิถีชีวิตไปเลย
หรือ ชนกลุ่มน้อยที่โดนเพิกถอนบัตรประชาชนแบบเหวี่ยงแห

พูดอย่างนี้ไม่ได้แปลว่าต้อง ปลง กับ สถานะการณ์ตัวเอง (พวก IT)
เพราะเมื่อเจอการใช้อำนาจ ก็ควรจะต้องรวมตัวต่อต้านอำนาจ

Related link from Roti

Wednesday, April 04, 2007

Autism -> Engineering

Autism ใน longdo แปลว่า ความผิดปกติทางการสื่อสารและอารมณ์

มีลูกแล้ว ความสนใจในเรื่องของ Gene เรื่องของ เด็ก ก็เพิ่มสูงขึ้น
วันนี้อ่านเจอ สมมติฐานของ Baron-Conhen เรื่อง Autism-Engineer Hypothesis.
สมมติฐานนี้มีประมาณว่า
พ่อที่เป็นเป็น engineering ถ้าเลือกแต่งงานกับผู้หญิงในแบบ assortative mating
(assortative -> แต่งงานกับคนที่มีคุณสมบัติเหมือนตัวเอง)
มีโอกาสที่ลูกจะได้ gene ที่มีคุณสมบัติเด่นใน systemisers เบิ้ลจากพ่อและแม่
จนก้าวไปสู่บริเวณที่เรียกว่า Autism

ดูตาราง


ไม่ต้องตกใจ สมมติฐานก็คือสมมติฐาน
(โชคดีที่เราไม่ได้แต่งงานกับสายอาชีพ Engineer)

Link
Two new theories of autism: hyper-systemising and assortative mating
(หน้า 4)

Related link from Roti

Tuesday, April 03, 2007

SVN Branching and Merging

ใช้ version control มานานแล้ว
แต่ยังไม่เคยได้ทำ branch & merge operation สักที
หลักๆก็ใช้อยู่แค่ trunk เส้นเดียว
ไม่เคยสร้าง branches ให้ปวดหัว
(ไม่สร้าง branches ก็ปวดหัวไปอีกแบบ)

ปกติในการใช้ version control,
เราสามารถแบ่งลักษณะการใช้ออกเป็น 3 กลุ่ม ตามลักษณะการใช้ branches ได้ดังนี้

พวกที่ 1 ไม่ใช้ branches เลย มีแต่ trunk อย่างเดียว
(บริษัทผม จัดอยู่ในกลุ่มนี้)

พวกที่ 2 พวกนี้สุดโต่งไปอีกด้านหนึ่งก็คือ ใช้แต่ branches เป็นหลัก
trunk เก็บเฉพาะ code ที่ verify ว่า stable แล้วเท่านั้น
ในพวกนี้ เวลาใครจะทำอะไรกับ code ไม่ว่าจะ bug ตัวจิ๊บจ๊อย ก็ต้องสร้าง branch ออกไป
เมื่อเสร็จแล้วจึงจะ merge เข้า trunk

พวกที่ 3 เป็นพวกผสมระหว่าง 1 และ 2 นั่นคือ
การเปลี่ยนแปลงเล็กๆน้อยๆ ก็ใช้ trunk ไป
แต่ถ้าเปลี่ยนเยอะหน่อยก็ใช้ branches

ปัจจุบัน ปัญหาที่ผมเจอก็คือ
เนื่องจาก methodology ที่ผมใช้พัฒนา software ในปัจจุบัน
ใช้เทคนิคแบบ Agile ในการพัฒนา (ทำไปคิดไป)
ซึ่งพอถึงจุดหนึ่ง มันจะเกิด refactor ขนานใหญ่ขึ้น
ก็เลยหลีกเลี่ยงไม่ได้ที่จะต้องทำ branches เพื่อแยกไป refactor ต่างหาก
(แยกไปลองผิดลองถูกต่างหาก จะได้ไม่กระทบคนอื่นเขา)

ใน svn, เวลาเราจะทำ branches
เราสามารถทำได้ง่ายๆ ดังนี้
  • สร้าง directory ที่จะใช้เก็บ branches ของเรา
    มาตรฐานแบบไม่เป็นทางการของ svn กำหนดว่า directory branches ให้เราอยู่แล้ว
    ดังนี้เราก็เลือกสร้าง folder ใต้ directory นี้
    ความยากในขั้นนี้ก็คือการตั้งชื่อ folder

    สมมติเราต้องการสร้าง folder mybranche-1 ก็ให้ใช้คำสั่งนี้

    svn mkdir http://hostname/svn/yourproject/branches/mybranche-1

  • ใช้คำสั่ง copy เพื่อ copy จาก trunk ไปยัง folder ที่เราสร้าง

    svn copy http://hostname/svn/yourproject/trunk \
    http://hostname/svn/yourproject/branches/mybranche-1


แค่นี้ ก็ได้ branche เส้นใหม่ ให้เราได้ลองยำเล่นสมใจแล้ว

ความยากขั้นถัดไป ก็คือการ merge
โดยการ merge จะเกิดได้ 2 กรณี
  • กรณีแรก เราทำ code เสร็จไว, ต้องการ commit change กลับเข้าไปยัง trunk
    การ merge ก็จะเกิดใน ทิศทางจาก branche มายัง trunk
  • กรณีที่สอง เราแก้ไข branch นานมาก จน trunk เปลี่ยนไปเยอะแล้ว,
    ถ้าเกิดเหตุการณ์แบบนี้ เราก็ควรจะ merge การเปลี่ยนแปลงจาก trunk ไปยัง branche เป็นระยะๆ
    เพื่อลดความแตกต่างระหว่าง code ลง


ข้อด้อยของ svn ก็คือ มันไม่มี feature ที่ช่วยจำว่า เรา merge อะไรไแล้วบ้าง
ดังนั้นจึงมีโอกาสที่เราจะ merge ซ้ำซ้อนลงไปได้
best practice ก็คือ ในการ commit การ merge แต่ละครั้ง
ให้เขียน log message ให้ชัดเจนว่า เป็นการ merge จาก revision ไหนถึงไหน
เพื่อจะได้ใช้เป็นข้อมูลเตือนความจำ

ลองดูตัวอย่างการใช้คำสั่ง merge จาก branch ไปยัง trunk
สมมติเรามีเส้นทางของ version เราดังนี้

#trunk# --- revision 149 ------- .... ------- >
\
\ branches
\
+ revision 150 ----- .... ----- revision 159 >


ขั้นตอนการ merge กลับ, เราจะใช้คำสั่ง svn merge เข้ามาช่วย
โดยเราจะสั่งคำสั่งดังนี้

ย้าย current directory ไปยัง working copy ของ trunk
แล้วทำการ update code ให้เป็น code ล่าสุดก่อน

$ cd $WORKING_COPY_OF_MAIN_TRUNK
$ svn update


trick ของการใช้ merge ก็คือ ประเด็นว่าเราจะต้องหาเลข revision เริ่มต้นให้ได้ก่อน (ในที่นี้คือ 150)
เพราะส่วนใหญ่มักจะลืมไม่ได้จดไว้
คำสั่งที่ช่วยหาเลขนี้ก็คือ

svn log -stop-on-copy

โดยมันจะ report log ไปจนถึง จุดที่เกิดการทำ branches (ใน svn การทำ branches คือการ copy)

เมื่อได้เลข revision มาแล้ว ก็สั่ง merge ส่วนเปลี่ยนแปลงที่เกิดขึ้นระหว่าง revision 150 ถึง 159 ใน branches
กับ working copy ของ trunk
โดยเริ่มแรกให้ใส่ parameter --dry-run เพื่อตรวจสอบความถูกต้องก่อน

$ svn merge --dry-run -r 150:159 http://domain/svn/branches/your_branches_name


ซึ่งหลังจากเกิดการ merge แล้ว,
เราก็ต้องตรวจ working copy ของเรา
ว่ามีอะไรผิดผลาดไหม เช่นพวก conflict
พอมั่นใจว่าถูกหมด ก็สั่ง commit ตามปกติ

ส่วนการ merge จาก trunk ไปยัง branch ก็ทำแบบเดียวกัน
เพียงแต่กลับทิศทางกัน

Related link from Roti

Monday, April 02, 2007

Expertise


Research on the acquisition of expertise has shown that
(a) it takes a long time,
(b) it takes intense, focused effort
(c) it can not be taught by direct tuition.

One of the most widely replicated findings in all domains studied is that it
takes at least 10 years to reach expert level.


ที่มา
The Acquisition of Expertise in Software Engineering Education

Related link from Roti