题目:

要完成一个学校的管理系统,需要管理学生,老师,工作人员,行政人员,同时包括一个管理员,要求:

  1. 分析根据自己的理解,分析如何设计这几个类,要用继承强行继承
  2. 用方法覆盖实现对功能的扩展,例如,可以说话功能的复写。 谁能告诉我这有什么意义
  3. 用一个类来管理这五类人员,就是用管理类对这几类人员进行添加,删除,修改
  4. 写一个测试类测试正确性
    关系如下图

nothing

思路

定义数据存储

我们需要使用 Manager 类来管理其它类像 Student, Teacher…, 这个管理也就是增删查改, 相当于和数据库
通信, 考虑到这只是一个简单的作业,数据库直接简化为内存就行了,因此我们定义数据库db到 Manager 类

1
private static Map<Class<?>, Map<Long, Object>> db = new HashMap<Class<?>, Map<Long, Object>>();

细节后面再说

设计 API

考虑 client 端, 期望的得到的 API 可能是各种类的实例直接插入, 然后根据(类, id)来查询到指定已经存入的数据,像这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 增
long id = 111;
Student s = new Student(id);
s.name = "kanai";
Manager.save(s);

// 查
Student ns = Manager.query(Student.class, id);
assert ns.equals(s);

// 改
ns.name = "new name";
Manager.update(ns);

// 删
Manager.delete(Student.class, id);
assert Manager.query(Student.class, id) == null;

我们总是需要一个 id(identifier)去查找同一个类的唯一示例(无论对与调用者和 manager), 因此至少在插入操作, 必须限制数据类型至少拥有只读 id 的属性, 为了方便使用我们定义接口(注意不是父类, 使用接口规范数据而不是继承)。

1
2
3
public interface Model<T> {
long getId();
}

详细解释下

因为存入的数据后面需要根据一些属性进行查询,删除,而且需要唯一标识的效果, 就像数据库里面的主键primary key, 虽说 Manager 类我们可以直接设定成任意类都可以直接插入,但是后面查询和删除没有这个主键就没办法进行, 所以要定义一个规范, 一般称为接口interface, 就是上面那个 Model

内容很简单, 就一个 getId 方法, 只要实现这个接口不管什么类都可以直接保存进来

这样, 然后 Manager 只需要设置saveupdate的方法接受的数据类型为Model<T>即可。例如

1
2
3
4
5
6
7
public class Manager {
private static Map<Class<?>, Map<Long, Object>> db = new HashMap<Class<?>, Map<Long, Object>>();

public static <T> void save(Model<T> modelInstance) {
// save to db...
}
}

现在规范初步约定好了,我们可以尝试定义Model, 这里拿Student示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Student extends Person implements Model<Student> {
private long id;
public String name;

public Student(long id) {
this.id = id;
}

public long getId() {
return id;
}

public String toString() {
return String.format("<Student id:'%d' name:'%s'>", id, name);
}
}

这里 Student 类继承了 Person 类(实际上 Person 就是个空类,为了满足作业要求才强行继承的), 实现了Model<Student>接口,我们这里为了方便除了id只定义了一个name属性。

实现 API

实现Manager的各个静态方法。 等等, 为什么偏要是静态方法而不是方法? 理由简单地说就是没必要

所谓的静态方法,在其它语言里就是普通的函数,我们这里只需要用到函数没必要对Manager类实例化。面向对象编程中将数据和它的行为(behavior)绑定在一起,使方法的行为可以很方便的依赖于不断变化的属性, 对这里Manager类来说, 并没有相应的属性需要被依赖例如database dialect来和数据库通信, 因此我们这里只是使用简单的函数, Java 里所谓简单的函数就是静态方法(虽说反而看着更复杂

回到正题, save方法方法接收一个实现Model接口的任意实例,我们必须根据不同的类来把这些数据分类, 这也是db最外层定义为Map<Class<?>, ...>的理由。而对于每一种类型, 我们只需简单的使用Map<Long, Object>映射 id 到实体即可,db的定义就这么来了, 那么实现这几个方法,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public static <T> void save(Model<T> modelInstance) {
// get table or initialize a new table
Map<Long, Object> table = db.get(modelInstance.getClass());
if (table == null) {
table = new TreeMap<>();
db.put(modelInstance.getClass(), table);
}

table.put(modelInstance.getId(), modelInstance);
}

public static <T> T delete(Class<T> c, long id) {
// get table
Map<Long, Object> table = db.get(c);

if (table == null)
return null;

return (T) table.remove(id);
}

public static <T> T query(Class<T> c, long id) {
// get table
Map<Long, Object> table = db.get(c);

if (table == null)
return null;

Model<T> model = (Model<T>) table.get(id);
if (model == null)
return null;

return model; // Ignore the nasty batch operation here, we will do more here
}

saveudpate写法几乎一致, 甚至可以合并在一起,这里忽略掉。

保护数据

目前为止的 API 存在很严重的安全问题: 数据可以直接被修改。

实际上,保存在数据库中的数据和在内存中的数据并不同(指并没有指向同一块地址)或者说数据库中的数据被处理器 copy 过去的, 而在这里我们用内存作为数据库并没有做出这样的行为, 数据依旧指向同一块内存, client 在执行save操作后对实例的修改同时会影响到db中存储的数据,反过来也是一样,我们对从db中查询到数据直接修改也会影响到db里的原始数据, 为了解决这个问题, 我们必须在save, updatequery里增加 copy 行为。 在 Java 里我们称之为clone

为了方便, 我们在Model里新增方法clone

1
2
3
4
5
public interface Model<T> {
long getId();

T clone();
}

这个时候 Student 类报错, 我们需要在 Student 类里实现clone方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class Student extends Person implements Model<Student>, Cloneable {
private long id;
public String name;

public Student(long id) {
this.id = id;
}

public long getId() {
return id;
}

public Student clone() {
try {
return (Student) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
return null;
}
}

public String toString() {
return String.format("<Student id:'%d' name:'%s'>", id, name);
}
}

注意我们类的声明新增了对Cloneable接口的实现, Cloneable接口时 Java 内置的接口, 这个接口什么都没做, 只是用来告知 Java 此类可以被 Clone(否则一定会出现 CloneNotSupportedException)。

save, update, query操作时稍作修改即可, 例如

1
2
3
4
5
// save, update
table.put(modelInstance.getId(), modelInstance.clone());

// query
return model.clone();

很好, 来测试一下:

1
2
3
4
5
6
7
8
9
10
11
12
// add
Student s = new Student(111);
s.name = "kanai";
Manager.save(s);

// query
Student qs = Manager.query(Student.class, 111);
System.out.println(qs);
System.out.println(s);

// not reference to the same address
assert s != qs;

注意最后一行断言, Java 里当两个实例指向不同地址时,尽管数据相同,然而==运算会得出false, 如果想要比较数据, 可以使用equals方法。

稍作总结

  1. 我们需要数据存储到相应的位置, 一般是数据库,但是这里只是作业,简化成内存,也就是db的定义
  2. Manager类想要达成增删查改任意数据类型到db的任务, 但是查删之类的操作必须根据identifier来确定唯一实例, 因此至少要限制插入进来的数据行为(或者属性, 因此定义了 Model 接口, 凡是实现这个接口的(拥有可读 id(long))的实例, 都可以保存入数据库
  3. 分离db和 client 端数据, 我们在保存,更新和查找操作时都会进行clone将存储到数据库db中的实例和 client 端的实例的内存分离开, 防止数据被随意更改

测试

至于测试类, 随便写下就 OK, 示例代码和执行结果如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class Main {
public static void main(String[] args) {
long id = 111;

// add
Student s = new Student(id);
s.name = "kanai";
Manager.saveOrUpdate(s);

// query, assert no equals
Student qs = Manager.query(Student.class, id);
System.out.println(qs);
System.out.println(s);

// not reference to the same address
assert qs != s;

// add for another class
Teacher t = new Teacher(id);
t.name = "kohaku";
Manager.saveOrUpdate(t);
System.out.println(Manager.query(Teacher.class, id));

// update
System.out.println("qs's name is:" + qs.name);
qs.name = "TEST";
Manager.saveOrUpdate(qs);
System.out.println("then qs's name be:" + Manager.query(Student.class, id).name);

// add
Student a = new Student(222);
a.name = "nobe";
Manager.saveOrUpdate(a);

// query all
System.out.println(Manager.queryAll(Student.class));

// remove
Manager.delete(Student.class, id);
System.out.println(Manager.query(Student.class, id));
}
}
1
2
3
4
5
6
7
<Student id:'111' name:'kanai'>
<Student id:'111' name:'kanai'>
<Teacher id:'111' name:'kohaku'>
qs's name is:kanai
then qs's name be:TEST
[<Student id:'111' name:'TEST'>, <Student id:'222' name:'nobe'>]
null