面向 Java 开发人员的 db4o 指南: 结构化对象和集合,db4o 中的继承和多态

在本系列文章中,我使用 Person 类型来演示 db4o 的所有基本原理。您已经学会了如何创建完整的 Person 对象图,以细粒度方式(使用 db4o 本身的查询功能来限制返回的实际对象图)对其进行检索,以及更新和删除全部的对象图(设定一些限制条件)等等。实际上,在面向对象的所有特性中,我们只漏掉了其中一个,那就是继承。

关于本系列
信息存储和检索作为同义语伴随 RDBMS 已经有 10 余年了,但现在情况有所改变。Java 开发人员为所谓的对象关系型阻抗失配而沮丧,也不再有耐心去尝试解决这个问题。加上可行替代方案的出现,就导致了人们对对象持久性和检索的兴趣的复苏。 面向 Java 开发人员的 db4o 指南 对开放源码数据库 db4o 进行了介绍,db4o 可以充分利用当前的面向对象的语言、系统和理念。请访问 db4o 主页 并下载 db4o;您需要用它来完成本文中的例子。

我将演示的这个例子的最终目标是一个用于存储雇员数据的数据管理系统,我一直致力于开发我的 Person 类型。我需要这样一个系统:存储某个公司的员工及其配偶和子女的信息,但是此时他们仅仅是该系统的 Person(或者,可以说 Employees 是一个 Person,但是 Persons 不是一个 Employee)。而且,我不希望 Employee 的行为属于 Person API 的一部分。从对象建模程序的角度公平地讲,按照 is-a 模拟类型的能力就是面向对象的本质。

我会用 Person 类型中的一个字段来模拟雇佣 的概念。这是一种关系方法,而且不太适合用于对象设计。幸运的是,与大多数 OODBMS 系统一样,db4o 系统对继承有一个完整的理解。在存储系统的核心使用继承可以轻松地 “重构” 现有系统,可以在设计系统时更多地使用继承,而不会使查询工具变得复杂。您将会看到,这也使查询特定类型的对象变得更加容易。

高度改进的 Person

清单 1 回顾了 Person 类型,该类型在本系列文章中一直作为示例使用:


清单 1. 改进之前的示例……

package  com.tedneward.model;

import  java.util.List;
import  java.util.ArrayList;
import  java.util.Iterator;

public   class  Person
{
    
public Person()
    
{ }
    
public Person(String firstName, String lastName, Gender gender, int age, Mood mood)
    
{
        
this.firstName = firstName;
        
this.lastName = lastName;
        
this.gender = gender;
        
this.age = age;
        
this.mood = mood;
    }

    
    
public String getFirstName() return firstName; }
    
public void setFirstName(String value) { firstName = value; }
    
    
public String getLastName() return lastName; }
    
public void setLastName(String value) { lastName = value; }

    
public Gender getGender() return gender; }
    
    
public int getAge() return age; }
    
public void setAge(int value) { age = value; }
    
    
public Mood getMood() return mood; }
    
public void setMood(Mood value) { mood = value; }

    
public Person getSpouse() return spouse; }
    
public void setSpouse(Person value) 
        
// A few business rules
        if (spouse != null)
            
throw new IllegalArgumentException("Already married!");
        
        
if (value.getSpouse() != null && value.getSpouse() != this)
            
throw new IllegalArgumentException("Already married!");
            
        spouse 
= value; 
        
        
// Highly sexist business rule
        if (gender == Gender.FEMALE)
            
this.setLastName(value.getLastName());

        
// Make marriage reflexive, if it's not already set that way
        if (value.getSpouse() != this)
            value.setSpouse(
this);
    }


    
public Address getHomeAddress() return addresses[0]; }
    
public void setHomeAddress(Address value) { addresses[0= value; }

    
public Address getWorkAddress() return addresses[1]; }
    
public void setWorkAddress(Address value) { addresses[1= value; }

    
public Address getVacationAddress() return addresses[2]; }
    
public void setVacationAddress(Address value) { addresses[2= value; }

    
public Iterator<Person> getChildren() return children.iterator(); }
    
public Person haveBaby(String name, Gender gender) {
        
// Business rule
        if (this.gender.equals(Gender.MALE))
            
throw new UnsupportedOperationException("Biological impossibility!");
        
        
// Another highly objectionable business rule
        if (getSpouse() == null)
            
throw new UnsupportedOperationException("Ethical impossibility!");

        
// Welcome to the world, little one!
        Person child = new Person(name, this.lastName, gender, 0, Mood.CRANKY);
            
// Well, wouldn't YOU be cranky if you'd just been pushed out of
            
// a nice warm place?!?

        
// These are your parents...            
        child.father = this.getSpouse();
        child.mother 
= this;
        
        
// ... and you're their new baby.
        
// (Everybody say "Awwww....")
        children.add(child);
        
this.getSpouse().children.add(child);

        
return child;
    }

    
    
public Person getFather() return this.father; }
    
public Person getMother() return this.mother; }
    
    
public String toString()
    
{
        
return 
            
"[Person: " +
            
"firstName = " + firstName + " " +
            
"lastName = " + lastName + " " +
            
"gender = " + gender + " " +
            
"age = " + age + " " + 
            
"mood = " + mood + " " +
            (spouse 
!= null ? "spouse = " + spouse.getFirstName() + " " : ""+
            
"]";
    }

    
    
public boolean equals(Object rhs)
    
{
        
if (rhs == this)
            
return true;
        
        
if (!(rhs instanceof Person))
            
return false;
        
        Person other 
= (Person)rhs;
        
return (this.firstName.equals(other.firstName) &&
                
this.lastName.equals(other.lastName) &&
                
this.gender.equals(other.gender) &&
                
this.age == other.age);
    }

    
    
private String firstName;
    
private String lastName;
    
private Gender gender;
    
private int age;
    
private Mood mood;
    
private Person spouse;
    
private Address[] addresses = new Address[3];
    
private List<Person> children = new ArrayList<Person>();
    
private Person mother;
    
private Person father;
}

跟本系列的其他文章一样,我不会在每次更改时都展示完整的 Person 类,只逐步展示每次更改。在这个例子中,我实际上并没有更改 Person,因为我将要扩展 Person,而不是修改它。

区别雇员

需要做的第一件事是使我的雇员管理系统能够区别普通的 Person(例如雇员的配偶和/或子女)和 Employee。从纯粹建模的立场来说,这个更改很简单。我只是向 Person 引入了一个新的派生类,这个类和目前涉及到的其他类都在同一个包中。毫无疑问,我将会调用这个类 Employee,如清单 2 所示:


Listing 2. Employee 扩展 Person

package  com.tedneward.model;

public   class  Employee  extends  Person
{
    
public Employee()
    
{ }
    
public Employee(String firstName, String lastName, String title,
                     Gender gender, 
int age, Mood mood)
    
{
        
super(firstName, lastName, gender, age, mood);
        
        
this.title = title;
    }


    
public String getTitle() return title; }
    
public void setTitle(String value) { title = value; }
    
    
public String toString()
    
{
        
return "[Employee: " + getFirstName() + " " + getLastName() + " " +
            
"(" + getTitle() + ")]";
    }

    
    
private String title;
}

 

Employee 类的全部代码都在清单 2 中。从 OODBMS 的角度看, Employee 中的其他方法意义不大。在本讨论中需要记住的是 EmployeePerson 的一个子类(如果更加关心系统的建模过程,可以设想 Employee 中的其他方法,例如 promote()demote()getSalary()setSalary()workLikeADog())。

测试新模型

对新模型的探察测试简单明了。我创建一个叫做 InheritanceTest 的 JUnit 类,目前为止,它是第一个较为复杂的对象集,充当 OODBMS 最初的工作内容。为了使输出(将会在清单 6 中见到)更加清晰,我在清单 3 中展示了带有 @Before 注释的 prepareDatabase() 调用:


清单 3. 欢迎加入本公司(您现在为我服务)

@Before  public   void  prepareDatabase()
    
{
        db 
= Db4o.openFile("persons.data");

        
// The Newards
        Employee ted = new Employee("Ted""Neward""President and CEO",
            Gender.MALE, 
36, Mood.HAPPY);
        Person charlotte 
= new Person("Charlotte""Neward",
            Gender.FEMALE, 
35, Mood.HAPPY);
        ted.setSpouse(charlotte);
        Person michael 
= charlotte.haveBaby("Michael", Gender.MALE);
        michael.setAge(
14);
        Person matthew 
= charlotte.haveBaby("Matthew", Gender.MALE);
        matthew.setAge(
8);
        Address tedsHomeOffice 
= 
            
new Address("12 Redmond Rd""Redmond""WA""98053");
        ted.setHomeAddress(tedsHomeOffice);
        ted.setWorkAddress(tedsHomeOffice);
        ted.setVacationAddress(
            
new Address("10 Wannahokalugi Way""Oahu""HA""11223"));
        db.set(ted);
        
        
// The Tates
        Employee bruce = new Employee("Bruce""Tate""Chief Technical Officer"
            Gender.MALE, 
29, Mood.HAPPY);
        Person maggie 
= new Person("Maggie""Tate"
            Gender.FEMALE, 
29, Mood.HAPPY);
        bruce.setSpouse(maggie);
        Person kayla 
= maggie.haveBaby("Kayla", Gender.FEMALE);
        Person julia 
= maggie.haveBaby("Julia", Gender.FEMALE);
        bruce.setHomeAddress(
            
new Address("5 Maple Drive""Austin",
                
"TX""12345"));
        bruce.setWorkAddress(
            
new Address("5701 Downtown St""Austin",
                
"TX""12345"));
        
// Ted and Bruce both use the same timeshare, apparently
        bruce.setVacationAddress(
            
new Address("10 Wanahokalugi Way""Oahu",
                
"HA""11223"));
        db.set(bruce);
        
        
// The Fords
        Employee neal = new Employee("Neal""Ford""Meme Wrangler",
            Gender.MALE, 
29, Mood.HAPPY);
        Person candi 
= new Person("Candi""Ford",
            Gender.FEMALE, 
29, Mood.HAPPY);
        neal.setSpouse(candi);
        neal.setHomeAddress(
            
new Address("22 Gritsngravy Way""Atlanta""GA""32145"));
        
// Neal is the roving architect
        neal.setWorkAddress(null);
        db.set(neal);
        
        
// The Slettens
        Employee brians = new Employee("Brian""Sletten""Bosatsu Master",
            Gender.MALE, 
29, Mood.HAPPY);
        Person kristen 
= new Person("Kristen""Sletten",
            Gender.FEMALE, 
29, Mood.HAPPY);
        brians.setSpouse(kristen);
        brians.setHomeAddress(
            
new Address("57 Classified Drive""Fairfax""VA""55555"));
        brians.setWorkAddress(
            
new Address("1 CIAWasNeverHere Street""Fairfax""VA""55555"));
        db.set(brians);
        
        
// The Galbraiths
        Employee ben = new Employee("Ben""Galbraith""Chief UI Director",
            Gender.MALE, 
29, Mood.HAPPY);
        Person jessica 
= new Person("Jessica""Galbraith",
            Gender.FEMALE, 
29, Mood.HAPPY);
        ben.setSpouse(jessica);
        ben.setHomeAddress(
            
new Address(
                
"5500 North 2700 East Rd""Salt Lake City"
                
"UT""12121"));
        ben.setWorkAddress(
            
new Address(
                
"5600 North 2700 East Rd""Salt Lake City",
                
"UT""12121"));
        ben.setVacationAddress(
            
new Address(
                
"2700 East 5500 North Rd""Salt Lake City",
                
"UT""12121"));
            
// Ben really needs to get out more
        db.set(ben);
        
        db.commit();
    }

跟本系列早先的探察测试示例一样,在每次测试完成后,我使用带 @After 注释的 deleteDatabase() 方法来删除数据库,以使各部分能够很好地分隔开。

让我们运行几个查询……

在实际运行这个方法之前,我将会检查在系统中使用 Employee 会有哪些效果(如果有的话)。希望从数据库获取所有 Employee 信息,这很正常 — 或许当公司破产时他们将会全部被解雇(是的,我知道,这样想很残酷,但我只是对 2001 年的 dot-bomb 事故还有点心有余悸)。最初的测试看起来很简单,正如清单 4 所示:


清单 4. Ted 说,“你被解雇了!”

@Test  public   void  testSimpleInheritanceQueries()
{
    ObjectSet employees 
= db.get(Employee.class);
    
while (employees.hasNext())
        System.out.println(
"Found " + employees.next());
}

当进行测试时,将会产生一个有趣的结果:数据库(我自己、Ben、Neal、Brian 和 Bruce)中只返回了 Employee。OODBMS 识别出查询受到子类型 Employee 的显式约束,并且只选择了符合返回条件的对象。因为其他对象(配偶或者孩子)不属于 Employee 类型,他们不符合条件,所以没有被返回。

当运行一个返回所有 Person 的查询时,将会更加有趣,如下所示:


清单 5. 找到所有人!

@Test  public   void  testSimpleNonEmployeeQuery()
{
    ObjectSet persons 
= db.get(Person.class);
    
while (persons.hasNext())
        System.out.println(
"Found " + persons.next());
}

当运行这个查询时,每个单一对象 — 包括以前返回的所有 Employee — 都被返回了。从某种程度上说,这是有意义的。因为 Employee 是一个 Person,由于建立在 Java 代码中的实现继承关系,因此满足返回查询的必须条件。

db4o 中的继承(以及多态)其实就是这么简单。没有用于查询语言的复杂的 IS 扩展,就不会引入不同于 Java 类型系统中现有概念的 “类型” 概念。我所指的只是期望作为查询的一部分的类型,而且这些是构成查询的主要成分。这跟在 SQL 查询中加入表格很相似,方法就是选择其数据应为查询结果一部分的表格。额外的好处是,“父类型” 也是作为查询的一部分隐式地 “加入” 的。清单 6 显示了 清单 3InheritanceTest 的输出:


清单 6. 多态发挥作用

.Found [Employee: Ted Neward (President and CEO)]
Found [Person: firstName 
=  Charlotte lastName  =  Neward gender  =  FEMALE age  =   35
mood 
=  HAPPY spouse  =  Ted ]
Found [Person: firstName 
=  Michael lastName  =  Neward gender  =  MALE age  =   14  mood
 
=  CRANKY ]
Found [Person: firstName 
=  Matthew lastName  =  Neward gender  =  MALE age  =   8  mood
=  CRANKY ]
Found [Employee: Bruce Tate (Chief Technical Officer)]
Found [Person: firstName 
=  Maggie lastName  =  Tate gender  =  FEMALE age  =   29  mood
=  HAPPY spouse  =  Bruce ]
Found [Person: firstName 
=  Kayla lastName  =  Tate gender  =  FEMALE age  =   0  mood  =
CRANKY ]
Found [Person: firstName 
=  Julia lastName  =  Tate gender  =  FEMALE age  =   0  mood  =
CRANKY ]
Found [Employee: Neal Ford (Meme Wrangler)]
Found [Person: firstName 
=  Candi lastName  =  Ford gender  =  FEMALE age  =   29  mood  =
 HAPPY spouse 
=  Neal ]
Found [Employee: Brian Sletten (Bosatsu Master)]
Found [Person: firstName 
=  Kristen lastName  =  Sletten gender  =  FEMALE age  =   29  m
ood 
=  HAPPY spouse  =  Brian ]
Found [Employee: Ben Galbraith (Chief UI Director)]
Found [Person: firstName 
=  Jessica lastName  =  Galbraith gender  =  FEMALE age  =   29
 mood 
=  HAPPY spouse  =  Ben ]

您可能会感到奇怪,不管如何查询,返回的对象仍然是适当的子类型对象。例如,跟预期的一样,在上面的查询中当 toString() 被每个返回的 Person 对象调用时,Person.toString() 也正被每个 Person 调用。然而,因为 Employee 有一个重写的 toString() 方法,因此关于动态绑定的常用规则就不适用了。存储在 Employee 中的 Person 的各部分不会被 “切掉”,而当定期 SQL 查询未能成功地将派生子类表加入到 table-per-class 模型中时,这种被 “切掉” 的现象就会发生。

原生继承

当然,当继承条件扩展到原生查询中时,其功能就跟我所做过的简单对象查询一样强大。进行调用时,查询语法将会更加复杂,但是基本上遵循我以前所使用的语法,如清单 7 所示:


清单 7. 你是单身吗?

@Test  public   void  testNativeQuery()
{
    List
<Person> spouses =
        db.query(
new Predicate<Person>() {
            
public boolean match(Person candidate) {
                
return (candidate.getSpouse() instanceof Employee);
            }

        }
);
    
for (Person spouse : spouses)
        System.out.println(spouse);
}

下面的查询与我以前做过的查询在思想上类似,考虑系统中所有的 Person,但是设置一个约束条件,只查找配偶也是一个 EmployeePerson — 调用 getSpouse(),将返回值传递给 Java instanceof 运算符,这样就完成了查询(记住 match() 调用只返回 true 或者 false,表示候选对象是否应该返回)。

请注意如何通过更改在 query() 调用中传递的 Predicate 来更改隐式选择的类型条件,如清单 8 所示:


清单 8. 哇!办公室恋情!

@Test  public   void  testEmployeeNativeQuery()
{
    List
<Employee> spouses =
        db.query(
new Predicate<Person>() {
            
public boolean match(Person candidate) {
                
return (candidate.getSpouse() instanceof Employee);
            }

        }
);
    
for (Person spouse : spouses)
        System.out.println(spouse);
}

当执行此查询时,不会产生什么结果,因为现在此查询只查找配偶也在公司工作的 Employee。目前,数据库中的雇员都不满足这个条件。如果公司雇佣 Charlotte,那么会返回两个 Employee:Ted 和 Charlotte(但是人家说办公司恋情永远不会发生)。

在很大程度上,就是这样。继承不会对更新、删除和激活深度产生任何影响,只会影响到对象的查询方面。但是回想起 Java 平台提供的两种形式的继承:实现继承和接口继承。前者通过各种 extends 子句来实现,而后者通过 implements 来实现。如果 db4o 支持 extends,那么它也一定支持 implements,您将会看到,这有利于实现强大的查询功能。

都是关于接口的

就像任何 Java (或 C#)编程人员使用了一段时间这种语言后认识到的,接口对于建模非常有用。尽管不会经常看到,接口具有强大的 “隔离” 交叉在传统实现继承行中的对象的能力;通过使用接口,我可以声明某些类型为 Comparable 或者 Serializable,或者在本例中,Employable(是的,从设计的角度说这是大材小用了,但是用于教学还是很不错的)。


清单 9. 嘿,不再是 2001 年了!来为我工作吧!

package  com.tedneward.model;

public   interface  Employable
{
    
public boolean willYouWorkForUs();
}

要看接口是如何工作的,我需要 Employable 接口的一个实体类继承,并且 — 或许您已经猜测到 — 这意味着创建一个 EmployablePerson 子类型来扩展 Person 和实现 Employable。我不会再次演示这些代码(没有必要演示,除了将 ** EMPLOYABLE ** 添加到 PersontoString() 末尾以外, PersonEmployablePerson.toString() 方法中)。我也会修改 prepareDatabase() 调用以返回 “Charlotte 是一个 EmployablePerson,而不只是一个 Person” 的事实。

现在,我会编写一个遍历数据库的查询,查找愿意为本公司工作的雇员的配偶或亲人,如清单 10 所示。


清单 10. 有工作了,来看看吧……

@Test  public   void  testEmployableQuery()
{
    List
<Employable> potentialEmployees =
        db.query(
new Predicate<Employable>() {
            
public boolean match(Employable candidate) {
                
return (candidate.willYouWorkForUs());
            }

        }
);
    
for (Employable e : potentialEmployees)
        System.out.println(
"Eureka! " + e + " has said they'll work for us!");
}
    



毫无疑问,Charlotte 被返回了,说明她可能为本公司工作。更好的是,这意味着我引入的任何接口都变成了一种限制查询的新方式,不需要人工添加包含此信息的字段;只有 Charlotte 符合查询条件,因为她实现了这个接口,而其他配偶都没有实现(至少到目前为止)。

结束语

如果说对象和继承就像巧克力和花生酱的话,那对象和多态就好比手和手套。这两个元素就像经理和他/她的高薪一样般配。检索数据时,任何存储对象的系统都不得不将继承的概念引入它的存储媒介和过滤器中。幸运的是,面向对象的 DBMS 使得这很容易实现,而且不必引入新的 query-predicate 术语。从长远看来,引入继承会使 OODBMS 容易使用 得多


参考资料

学习

  • 您可以参阅本文在 developerWorks 全球站点上的 英文原文

  • 面向 Java 开发人员的 db4o 指南: 超越简单对象”(Ted Neward,developerWorks,2007 年 7 月):开始在 db4o 中使用结构化对象和探察测试。

  • Role object”(Dirk Bäumer,Dirk Riehle,Wolf Siberski,and Martina Wulf;Pattern Languages of Program Design 4,Addison-Wesley,2000):解释了角色对象,在其中将角色表示成独立的对象,以将不同的上下文分开,从而简化系统配置。

  • 追求代码质量 (Andrew Glover, developerWorks 系列):学习更多有关开发人员测试技术的内容,比如探察测试。

  • 面向 Java 开发人员的 db4o 指南 (Ted Neward, developerWorks 系列): 介绍 db4o,一个使用目前的面向对象语言、系统及理念的开源数据库。

  • db4o 主页:学习更多关于 db4o 的知识。

  • ODBMS.org:其中有许多优秀的关于对象数据库技术的免费资料

  • developerWorks Java 技术专区:其中有数百篇关于 Java 编程各方面的文章。

  • IBM® Information Management 新手入门:仍然没有使用 OODBMS 吗?了解更多关于 IBM 强大的关系数据库管理系统(RDBMS)服务器家族的信息。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值