jim yeh on 六月 7th, 2009

程式碼的重覆性使程式不容易維護、以及增加系統出錯的機率,同時使得程式的再用性難以提昇。因此降低程式碼的重覆性對程式碼的品質與彈性有很大的助益。然而,在資料存取與與物件型別的無法分離的情況下,卻會使得開發者難以對治程式碼的重覆性;相同處理資料的邏輯,受限於處理資料的不同型態,使得開發者必須依據資料型態不同,而改寫同一段程式碼,使得程式碼很難共用。

就像一個比較常見的例子是資料永續存取的實作,常會因為存取資料型態的不同,而影響到資料存取的界面,因此對每一個需要持久化的商業領域物件,都必須重覆地實作它們的 CRUD 存取功能。

雖然透過永續層的概念,我們可以將這種重覆性隔離開來,加上有些 ORM 永續框架的協助,開發資料存取物件的負擔並不太大。然而即使如此,我們有沒有可能避免重覆開發資料存取的元件,不用針對每一種物件型別都要寫一支相對的資料存取物件呢?要達到這樣的目的,我們必須分離資料存取的處理程序與物件型別,但要如何才能做到呢?

假設我們要開發一個會員註冊的功能,如果考慮以測試來驅動開發過程,我們可能會先撰寫下面的測試程式:

public class MemberTester
    extends TestCase {
    private static final String MEMBER_SERVICE =
        "memberSvc";
    private ApplicationContext context = null;

    MemberTester() {
	context = new ClassPathXmlApplicationContext(
            "conf/appContext.xml");
    }

    void testRegisterMember() {
        MemberService memberSvc = (MemberService)
            context.getBean(MEMBER_SERVICE);
        User member = new User();

        member.setId("myId");
        member.setEncodedPassword("abc");
        member.setEmailAddress("myId@domain");

        memberSvc.RegisterMember(member);

        CriteriaExpression criteria =
            Comparison.eq("id",
            ConstantExpression.getString("myId"));
        ParameterContext parameterContext =
            new HashtableParameterContext();
        User members[] =
            memberSvc.getMembers(criteria,
            parameterContext, 0);

        assertEquals(members.size(), 1);

        for (int i = 0; i < members.length();
            i ++) {
            assertEquals(member.getId(),
                members[i].getId());
            assertEquals(
                member.getEncodedPassword(),
                members[i].getEncodedPassword());
            assertEquals(
                member.getEmailAddress(),
                members[i].getEmailAddress());
        }

        memberSvc.removeMember(member);
    }
}

從測試程式找到應用服務的界面 MemberService ,並進一步實現符合此界面的服務元件:

public interface MemberService {
    public User [] GetMembers(
        CriteriaExpression criteria,
        ParameterContext context, int maxCount);
    public void RegisterMember(User member);
    public void UpdateMemberInfo(User member);
    public void removeMember(User member);
}
public class MemberServiceImpl
    implements MemberService {
    private UserDataAccessor
        memberDataAccessor = null;

    public User [] GetMembers(
        CriteriaExpression criteria,
        ParameterContext context,
        int maxCount) {
        return memberDataAccessor.find(criteria,
            context, maxCount);
    }

    public void RegisterMember(User member) {
        memberDataAccessor.insert(member);
    }

    public void UpdateMemberInfo(User member) {
        memberDataAccessor.update(member,
            LockMode.UPGRADE);
    }

    public void removeMember(User member) {
        memberDataAccessor.remove(member,
            LockMode.UPGRADE);
    }

    public void setMemberDataAccessor(
        UserDataAccessor memberDataAccessor) {
        this.memberDataAccessor =
            memberDataAccessor;
    }

    public UserDataAccessor
        getMemberDataAccessor() {
        return memberDataAccessor;
    }

}

實作服務元件會使用到界面 UserDataAccessor 來存取會員資料,利用永續框架如 Hibernate,可以減輕我們開發資料存取功能的實作負擔。

public UserDataAccessor {
    public void insert(User obj);
    public void update(User obj);
    public BizObject get(Serializable id);
    public void remove(User obj);
    User [] find(CriteriaExpression criteria,
        ParameterContext context,
        int maxCount);
}
public class UserDataAccessorImpl
    implements UserDataAccessor
    extends HibernateDataAccessor {

    public UserDataAccessorImpl() {
        super();
    }

    public void insert(User user) {
        insertBizObj(user);
    }

    public User get(Serializable id) {
        return (User) getBizObj(id);
    }

    public void update(User user) {
    	updateBizObj(user);
    }

    public void remove(User user) {
        removeBizObject(user);
    }

    public User [] find(
        CriteriaExpression criteria,
        ParameterContext context,
        int maxCount) {
        List userList = findBizObjs(criteria,
            context, maxCount);
        User users[] = new User[userList.size()];

        for (int i = 0; i < userList.size();
            i ++) {
            users[i] = userList.get(i);
        }

        return users;
    }
}

以上的程式是假設我們已經開發好供資料存取物件繼承的 HibernateDataAccessor 的基礎類別。它被定義為 abstract class,只提供 protected methods 供衍生類別呼叫物件存取的功能,例如上面程式片段的 insertBizObjupdateBizObjgetBizObjremoveBizObj、以及 findBizObjs 等方法。這樣使得永續存取物件的實作負擔並不重,但美中不足的是仍然因應不同的物件開發他們所需要的永續存取物件,而各個永續存取物件之間最大的差異卻只是不同物件的轉型而已,而顯得有些重覆而笨拙。

所幸 Java 5.0 以後提供的泛型程式設計,可以幫我們參數化存取物件的型態,這樣就可讓資料存取的處理程序,與資料形態完全分離。因此我們可以設計適用存取各種資料型態的泛型資料存取界面如下。

public interface DataAccessor<BizObject,
    LockType>
    extends Persister<BizObject,
    LockType>,
    FinderExecutor<BizObject> {
}

public interface Persister<BizObject,
    LockType> {
    public void insert(BizObject obj);
    public void update(BizObject obj,
        LockType lock);
    public BizObject get(Serializable id,
        LockType lock);
    public void remove(BizObject obj,
        LockType lock);
}

public interface FinderExecutor<BizObject> {
    List<BizObject> find(
        CriteriaExpression criteria,
        ParameterContext context, int maxCount);
    void addFilter(String name, Map<String,
        Object> params);
    void removeFilter(String name);
}

為了讓資料存取的權責更清楚而明確,我們使 DataAccessor 組合兩種抽象概念,一個是永續物件的存取、另一個則是找出符合查詢條件的永續物件序列。依據 OOADISP 原則,將這兩種不同概念分離成兩個界面,並由實作 DataAccessor 界面的程式來分別實作這兩個界面。

public class HibernateDataAccessor<
    BizObject> implements
    DataAccessor<BizObject, LockMode> {

    private Class<BizObject> bizObjType;
    private SessionFactory sessionFactory = null;
    private HibernateTemplate hibernateTemplate
        = null;

    public static int MAX_COUNT_IS_NOT_LIMITED
        = 0;

    public HibernateDataAccessor(
        Class<BizObject> bizObjType) {
        super();
        this.bizObjType = bizObjType;
    }

    public void insert(BizObject obj) {
        if (hibernateTemplate != null) {
            hibernateTemplate.save(obj);
            hibernateTemplate.flush();
        }
    }

    public BizObject get(Serializable id,
        LockMode lock) {
    	BizObject bizObj = null;

        if (id != null &&
            hibernateTemplate != null) {
            hibernateTemplate.flush();
            hibernateTemplate.get(bizObjType, id,
                lock);
        }

        return bizObj;
    }

    public void update(BizObject obj,
        LockMode lock) {
        if (hibernateTemplate != null) {
            hibernateTemplate.update(obj, lock);
            hibernateTemplate.flush();
        }
    }

    public void remove(BizObject obj,
        LockMode lock) {
        if (hibernateTemplate != null) {
            hibernateTemplate.delete(obj, lock);
            hibernateTemplate.flush();
        }
    }

    public void setSessionFactory(
        SessionFactory sessionFactory) {
        this.sessionFactory = sessionFactory;
        this.hibernateTemplate =
            new HibernateTemplate(
                this.sessionFactory);
    }

    public SessionFactory getSessionFactory() {
        return sessionFactory;
    }

    public List<BizObject> find(
        CriteriaExpression criteria,
        ParameterContext context, int maxCount) {

        if (hibernateTemplate != null)	{
            QueryCriteria fromBizObject =
                QueryCriteria.from(bizObjType);
            HibernateHqlQueryBuilder builder =
                getHqlBuilder(
                fromBizObject.where(criteria,
                context));

            hibernateTemplate.flush();
            hibernateTemplate.setMaxResults(
                maxCount);

            if (builder.getParamValues().length
                > 0) {
                return hibernateTemplate.find(
                    builder.getHqlText(),
                    builder.getParamValues());
            } else {
                return hibernateTemplate.find(
                    builder.getHqlText());
            }
        }
        else {
            return null;
        }
    }

    private HibernateHqlQueryBuilder
        getHqlBuilder(
        QueryCriteria criteria) {
        HibernateHqlQueryBuilder builder =
            new HibernateHqlQueryBuilder();
        CriteriaQueryDirector director =
            new CriteriaQueryDirector(builder);

        director.makeQuery(criteria,
            criteria.getContext());

        return builder;
    }

    public void addFilter(String name, Map<String,
        Object> params) {
        if (hibernateTemplate != null)
        {
            Filter filter =
                hibernateTemplate.enableFilter(
                name);
            Iterator<
                Map.Entry<String,
                Object> > it
                    = params.entrySet().iterator();

            while(it.hasNext())
            {
                Map.Entry<String, Object> mapEntry
                    = it.next();
                filter.setParameter(
                    mapEntry.getKey(),
                    mapEntry.getValue());
            }
        }
    }

    public void removeFilter(String name) {
        if (hibernateTemplate != null) {
            final String filterName = name; 

            hibernateTemplate.execute(
                new HibernateCallback() {
                public Object doInHibernate(
                    Session session)
                    throws HibernateException,
                    SQLException {
                        session.disableFilter(
                            filterName);

                        return null;
                }
            });
        }
    }
}

上面這段程式用到同人過去分享的「永續物件查詢的設計」來一般化查詢商業物件的方法,讓我們不會受到 ORM 的框架而影響影響 DataAccossor 的操作界面。同人建立一個共通的表述語法稱為 QueryExpression,讓查詢商業物件的實作方式不會因為永續框架的改變而受到影響。

再加上 BizObject 與 LockMode 型態的參數化,使我們不需要為不同的物件型態或永續框架的鎖定機制而重覆開發資料存取元件。此外,考慮物件可能會有需要事先定義過濾條件的需求,我們也在 FinderExecutor 界面中加入了 AddFilter()RemoveFilter() 的操作界面。使用 Hibernate 的泛型資料存取元件,只須略為修改我們的會員服務的實作,我們根本無須額外開發會員資料的資料存取物件。

public class MemberServiceImpl
    implements MemberService {

    private DataAccessor<User,
        LockMode> memberDataAccessor
            = null;

    public List<User> getMembers(
        CriteriaExpression criteria,
        ParameterContext context,
        int maxCount) {
            return memberDataAccessor.find(
                criteria, context, maxCount);
	}

    public void registerMember(
        User member) {
        memberDataAccessor.insert(
            member);
    }

    public void updateMemberInfo(
        User member) {
        memberDataAccessor.update(
            member, LockMode.UPGRADE);
    }

    public void setMemberDataAccessor(
        DataAccessor<User, LockMode>
        memberDataAccessor) {
        this.memberDataAccessor
            = memberDataAccessor;
	}

    public DataAccessor<User, LockMode>
        getMemberDataAccessor() {
        return memberDataAccessor;
    }

    public void removeMember(User member) {
        memberDataAccessor.remove(
            member, LockMode.UPGRADE);
    }

}

我們只須在注入 memberDataAccessor 時,以 User 的資料型態傳入建構子的參數初始化資料存取物件,就不需要再為 User 開發資料存取物件了,因為資料存取的處理邏輯與資料型態已經分離,彼此不再相互耦合,這正是泛型技術帶給我們的一大優勢呀。



     

One Response to “降低資料存取的重覆性”

  1. [...] 其實解決的方法並不難,雖然我們在 Cx 仍然很難反射出參數型別的預設建構式,但如果你對同人過去發表的〈降低資料存取的複雜性〉還有印象的話,你可以從這篇文章找到解決石頭成問題的方向。我們可以在這篇文章可以看到,在還沒使用 Java 的泛型之前,UserDataAccessorImpl 有預設建構子可供初始化 UserDataAccessor 的型別,但使用 Java 的泛型重構之後,泛型版本的 HibernateDataAccessor 卻提提供必須指定物件類別的建構式。所以使用同樣的原理,我們應該也可以解決石頭成的第二個問題。 [...]

Leave a Reply

You can use these tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong> <pre class="">