抽象文档模式 abstract-document
-
为什么叫抽象文档模式?
在文件系统中,我们一个文件夹内可以有多个文件,也可以有多个文件夹,子文件夹内又可以有文件和文件夹,是树形的结构. 抽象文档模式就是对这种情况的一种抽象,可以动态的给对象添加属性来支持这种数据结构.
-
能干什么?
我们通常编程的过程都是先声明一个对象,对象内包含事先定义好的属性,类似于以下的这种情况↓↓↓
public class Head {
private String head;
}
public class Arm {
private String arm;
}
import lombok.Data;
@Data
public class Person {
private Head head;
private Arm leftArm;
private Arm rightArm;
}
public static void main(String[] args) {
Person person = new Person();
person.setHead(new Head("头"));
person.setLeftArm(new Arm("左胳膊"));
person.setRightArm(new Arm("右胳膊"));
System.out.println(person);
}
可是有时后台的属性是动态的,能添加,能减少.这时候我们通常选择在数据库保存一个json格式的字符串, 而属性的key值基本上是写死的, 前后端对应上就ok,高级点的保存到字典表里.这种方式在实际开发中经常采用,也没什么问题,唯一缺点就是太随意,不好维护.
而抽象模式就是处理这种情况的方法之一
- 怎么用?
我们要把所有对象都抽象为文档. 每个文档都有自己的属性, 还能获取自己的子文档, 那么这个对象得拥有一个map来保存自己的所有属性和子文档,还需要put,get方法来获取属性,最后还应该有一个children方法,能够获取自己所有的子文档.
- 首先定义一个Document接口
public interface Document {
Object get(String key);
void set(String key, Object value);
//获取所有子文档
<T> Stream<T> children(String key, Function<Map<String, Object>, T> function);
}
- 然后定义抽象类,把接口实现
public abstract class AbstractDocument implements Document {
private Map<String, Object> properties;
protected AbstractDocument(Map<String, Object> properties) {
this.properties = properties;
}
@Override
public Object get(String key) {
return properties.get(key);
}
@Override
public void set(String key, Object value) {
properties.put(key,value);
}
@Override
public <T> Stream<T> children(String key, Function<Map<String, Object>, T> function) {
return Stream.of(properties.get(key))
.filter(Objects::nonNull)
.map(el -> (List<Map<String,Object>>) el)
.findAny()
.stream()
.flatMap(Collection::stream)
.map(function);
}
}
此时我们的抽象文档已经定义好了, 接下来的对象只要继承AbstractDocument类就会拥有一个map来保存属性,set,get,children方法来操作map.
- 下面就是将我们实际的对象都抽象成 抽象文档 的时候了,这里以构造一个电脑为例,简单的设定电脑有CPU,Model.Price三个属性
public enum Property {
MODEL, PRICE, CPU;
}
4.然后定义三个接口,分别表示我们拥有某一种属性 实质上就是为Docunment类提供了默认的实现, 例如: 调用HasModel对象的getModel()方法,就避免了使用document.get(“key值字符串”); 注意HasCpu接口的getCpus()方法提供了获取子类的实现方法
public interface HasModel extends Document {
default Object getModel() {
return get(Property.MODEL.toString());
}
}
public interface HasPrice extends Document {
default Object getPrice() {
return get(Property.PRICE.toString());
}
}
public interface HasCpu extends Document {
default Stream<Cpu> getCpus() {
return children(Property.CPU.toString(), Cpu::new);
}
}
5.然后是实现Cpu和Computer两个类
public class Cpu extends AbstractDocument implements HasModel, HasPrice {
public Cpu(Map<String, Object> properties) {
super(properties);
}
}
public class Computer extends AbstractDocument implements HasModel,HasPrice, HasCpu {
protected Computer(Map<String, Object> properties) {
super(properties);
}
}
- 此时电脑已经可以组装了
public static void main(String[] args) {
var cpu1 = Map.of(
Property.MODEL.toString(), "cpu1",
Property.PRICE.toString(), "100$");
var cpu2 = Map.of(
Property.MODEL.toString(), "cpu2",
Property.PRICE.toString(), "200$");
var computerMap = Map.of(
Property.MODEL.toString(), "联想电脑s1",
Property.PRICE.toString(), "3000$",
Property.CPU.toString(), List.of(cpu1, cpu2)
);
Computer computer = new Computer(computerMap);
System.out.println(computer.getPrice());
System.out.println(computer.getModel());
computer.getCpus().forEach(el -> {
System.out.println(el.getModel());
System.out.println(el.getPrice());
}
);
}
输出如下:
3000$
联想电脑s1
cpu1
100$
cpu2
200$
-
如果我们再为电脑加上两个硬盘,那岂不是要先在Property类中加个DISK属性,然后再加一个HasDisk接口,在让Computer实现HasDisk接口?
如果不嫌麻烦可以这么做,但是抽象文档模式的初衷不是要做这种事的, 他对于属性固定,拥有树形结构的对象有较好的表现 就像他的名字,抽象文档
所以如果你有需求实现类似文件系统的对象,又希望能够动态的添加属性,那么抽象文档模式是一个很好的选择