До этого момента я все время использовал класс Person
для показа всех основных принципов работы с db4o. Вы научились создавать графы объектов типа Person
, запрашивать и получать графы с весьма высокой степенью гибкости (используя запросы db4o для наложения условий на запрашиваемые графы), а также их модифицировать и удалять (правда, с некоторыми ограничениями). Фактически единственное, что осталось неохваченным в предыдущих выпусках - это наследование.
До сих пор я разрабатывал только класс Person
несмотря на то что конечной целью моих примеров было создание системы хранения данных о сотрудниках. Вероятно, система должна предоставлять возможности хранения информации не только о сотрудниках, но также о членах их семей, но пока что все это просто объекты типа Person
. Выражаясь технически, каждый объект типа Person
также является сотрудником (объектом типа Employee
), но не наоборот. Вдобавок, класс Employee
может предоставлять более специфические функции, чем Person
. Таким образом, с точки зрения разработки объектно-ориентированной архитектуры использование наследования является ключевой возможностью при проектировании системы типов.
Конечно, можно различать сотрудников и всех остальных, просто используя дополнительное поле класса Person
, но этот реляционный подход плохо вписывается в объектно-ориентированную систему. К счастью, как и во многих ООСУБД, наследование является встроенной возможностью 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.
Листинг 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. С точки зрения ООСУБД все дополнительные методы класса не имеют никакого значения. Все, что вам надо помнить - это то, что Employee
является наследником Person
. Разумеется, если вас очень интересует возможное дальнейшее проектирование Employee
, то можете представить методы типа promote()
, demote()
, getSalary()
и setSalary()
, а также workLikeADog()
.)
Тестирование новой модели данных
Проверить работоспособность новой модели достаточно просто. Я создал JUnit-тест под названием InheritanceTest
, а также немного запутанный набор объектов для первоначального заполнения объектной БД. Чтобы упростить понимание тестового вывода (он будет показан на листинге 6), листинг 3 раскрывает детали подготовки БД при исполнении метода prepareDatabase()
с аннотацией @Before
.
Листинг 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();
}
|
Как и ранее, я использую метод deleteDatabase()
с аннотацией @After
для очищения БД после каждого запуска, чтобы тесты оставались полностью независимыми друг от друга.
Запустим пару запросов
Для начала проверим, какие последствия повлекло добавление в систему класса Employee
. Система должна предоставлять возможность выборки всех сотрудников, например, для того чтобы сразу их всех уволить в случае банкротства (да, звучит, конечно, жестоко, но в моей памяти все еще всплывает известный кризис доткомов в 2001 г.). Проверка выглядит весьма просто, как вы можете судить из листинга 4.
Листинг 4. "Вы уволены!", заявляет Тед
@Test public void testSimpleInheritanceQueries()
{
ObjectSet employees = db.get(Employee.class);
while (employees.hasNext())
System.out.println("Found " + employees.next());
}
|
При запуске сразу же проявляется интересный момент: только объекты класса Employee
(я, Бен, Нил, Брайан и Брюс) возвращаются в качестве результата выборки. Это означает, что ООСУБД неким образом распознала условие на принадлежность подтипу Employee
и выбрала только те объекты, которые ему удовлетворяли. Так как никто из членов семей сотрудников не являлся объектом типа Employee
, они не удовлетворяли этому условию и, как следствие, не были включены в результат выборки.
Еще интереснее запустить запрос на выборку всех объектов типа Person
, например, так (листинг 5).
Листинг 5. Выведи всех людей
@Test public void testSimpleNonEmployeeQuery()
{
ObjectSet persons = db.get(Person.class);
while (persons.hasNext())
System.out.println("Found " + persons.next());
}
|
При этом выбираются абсолютно все объекты в системе, включая выбранные ранее объекты типа Employee
. В целом это совершенно логично, потому что каждый объект типа Employee
является также объектом типа Person
(Employee
is-a Person
) в силу использования наследования при объявлении Employee
в Java. Таким образом, каждый объект удовлетворяет условию запроса.
Как видите, нет ничего сложного в использовании наследования (а в дальнейшем и полиморфизма) в db4o. Не нужны ни расширения языка запросов, ни дополнительные операции над типами сверх используемых в Java. Все что нужно - это указать нужный тип в запросе. Все работает аналогично связыванию таблиц в запросах SQL, в которых указываются таблицы, из которых необходимо выбирать данные. Дополнительным преимуществом является неявное включение объектов родительских типов в результаты запроса. На листинге 6 показан результат исполнения теста, приведенного ранее в листинге 3.
Листинг 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()
, как и следовало ожидать. Но так как класс Employee
перегружает toString()
, то динамическое связывание отработает как обычно, и для объектов типа Employee
будет вызван Employee.toString()
. Иными словами, дополнительная информация о сотрудниках, содержащаяся в полях класса Employee
, не будет "отсечена" запросом. Подобная проблема часто встречается при использовании SQL, если при хранении объектов каждого подтипа в отдельной дочерней таблице забыть связать какую-то из них с родительской.
Наследование в естественных запросах
Разумеется, условия наследования можно использовать в естественных запросах (native queries) в той же степени, что и в объектных запросах в предыдущих примерах. Синтаксис запросов чуть-чуть усложняется, но, как показано на листинге 7, не претерпевает никаких серьезных изменений.
Listing 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
, чьи супруги являются объектами типа Employee
. Это делается с помощью Java-оператора instanceof
, который проверяет тип объекта, возвращаемого методом getSpouse()
класса Person
. Помните, что метод match()
должен возвращать только true или false, указывая таким образом, надо ли включать данный объект в результат выполнения запроса.
На листинге 8 показано, как изменение типа-параметра класса Predicate
, передаваемого в метод query()
, может изменить неявно заданное условие на тип выбираемых объектов:
Листинг 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), как запрос сразу вернет два объекта, а именно Теда (Ted) и Шарлотту (и при этом кто-то еще имеет что-то против служебных романов!)
Вот, в общем и целом, как работает наследование в запросах. У него нет никаких побочных эффектов при работе с запросами на изменение и удаление, а также при ограничении глубины выборки данных. Но, как вы помните, в Java существует два варианта наследования: наследование реализации (конструкция extends ) и наследование интерфейсов (конструкция implements ). Поскольку db4o поддерживает первый вариант, он также обязан поддерживать и наследование интерфейсов, что является весьма мощной возможностью, как будет показано ниже.
Интерфейсы - это главное
Наверное, один из первых уроков начинающего программиста на Java или C# - это роль интерфейсов при проектировании приложений. Интерфейсы предоставляют мощные возможности для отделения типов от их реализаций. Например, используя интерфейсы, можно объявить тип как Comparable
или Serializable
, или же, как в нашем случае - Employable
(листинг 9). Возможно, последний - это некоторый перегиб, но вполне подходит для данной учебной цели.
Листинг 9. Эй, 2001-й остался в прошлом, пора работать!
package com.tedneward.model;
public interface Employable
{
public boolean willYouWorkForUs();
}
|
|
Объекты и роли
С точки зрения определенных подходов к проектированию мое использование интерфейсов и наследования для моделирования объектов и их ролей может выглядеть неправомерным. Например, допустим, что родственник некоторого сотрудника был нанят на работу в компании. Означает ли это, что нам необходимо удалить его из списка объектов типа Person и добавить уже как объект типа Employee ? В то время как один объект может играть разные роли в разные моменты времени, ни его реализация, ни список интерфейсов не должны меняться по ходу исполнения программы.
Это утверждение вполне справедливо, и мой подход к моделированию нарушает его принципы. В то же время интерфейсы и наследование используются в данной статье исключительно в педагогических целях, для демонстрации возможностей db4o. | |
Чтобы продемонстрировать интерфейсы в деле, мне понадобится некоторая реализация Employable
. Как вы, наверное, уже догадались, ею станет класс EmployablePerson
, который также является наследником Person
. Я не буду показывать детали реализации, так как там все равно нет ничего интересного кроме добавления строки ** EMPLOYABLE **
в конец метода EmployablePerson.toString()
, перегружаюшего Person.toString()
. Я только изменю метод prepareDatabase()
, сделав Шарлотту объектом 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!");
}
|
Как и ожидалось, Шарлотта была включена в результат запроса. Это означает, что любой интерфейс, который мне придет в голову создать, может быть использован для наложения условий на выборку данных. При этом нет никакой необходимости добавлять искусственные атрибуты: та же Шарлотта удовлетворяет условию только благодаря реализации интерфейса, отличающего ее от всех остальных родственников на текущий момент времени.
Заключение
Объекты, наследование и полиморфизм - это настолько неразделимо связанные понятия, что любая система, отвечающая за хранение объектов, обязана также хранить информацию об их типах и поддерживать использование наследования при поиске и фильтрации данных. К счастью, это представляет собой тривиальную задачу с точки зрения объектно-ориентированных СУБД, которые даже не требуют поддержки специальных предикатов в запросах. В перспективе эта особенность ООСУБД значительно облегчает работу с иерархиями типов.
Ссылки по теме