目录

system design principles by example

从todo-cli谈谈一些系统设计的原则

介绍

以实际的需求为切入点,谈谈在系统设计以及实现上哪些原则可以帮助我们更好的实现设计。

需求来自郑晔老师的代码之丑, 简单讲,就是一个简易的命令行todo应用。

原则解读

1. 分层设计

类设计图,具体代码可以下拉到参考链接

class diagram

这是目前版本的类设计图,可以看到domain包作为核心功能的承载者,不依赖外部的类,这样做的好处是,外围的一些需求变化,不会导致核心功能的变更。

但是从具体的实现上来看,还是有一点问题,比如AddCommand的实现。

@Command(name = "add", description = "Add todo item.", mixinStandardHelpOptions = true)
public class AddCommand implements Runnable {
    @Parameters(description = "Item content.")
    String content = "";

    @Inject
    ItemRepository itemRepository;

    @Inject
    SessionRepository sessionRepository;

    @Override
    public void run() {
        String currentUser = sessionRepository.getCurrentUser();

        Long nextIndex = itemRepository.nextIndex(currentUser);

        Item item = Item.create(nextIndex, currentUser, content);
        itemRepository.save(item);

        System.out.printf("\n%d. %s\n", nextIndex, content);
        System.out.printf("\nItem %d added\n", nextIndex);
    }
}

实现上,在command里直接处理了业务逻辑,如果这时候来了一个需求,需要提供rest api供网页端使用,怎么办?显然目前的这种实现是需要加以改造的。

为此,我引入了domain service,将command里处理的业务逻辑挪到此。

@Singleton
public class AddService {
    @Inject
    ItemRepository itemRepository;

    public Long add(final String username, final String content) {
        Long nextIndex = itemRepository.nextIndex(username);

        Item item = Item.create(nextIndex, username, content);
        itemRepository.save(item);

        return nextIndex;
    }
}

@Command(name = "add", description = "Add todo item.", mixinStandardHelpOptions = true)
public class AddCommand implements Runnable {
    @Parameters(description = "Item content.")
    String content = "";

    @Inject
    SessionRepository sessionRepository;

    @Inject
    AddService addService;

    @Override
    public void run() {
        String currentUser = sessionRepository.getCurrentUser();

        addService.add(currentUser, content);

        Long nextIndex = addService.add(currentUser, content);

        System.out.printf("\n%d. %s\n", nextIndex, content);
        System.out.printf("\nItem %d added\n", nextIndex);
    }
}

如此一来,如果要支持rest api的接口,只需要引入controller包,调用下service即可,不需要改动核心业务逻辑处理的代码。

详见pull!3

每一行代码要找到属于它的位置

2. 依赖反转

ioc-1

上图的箭头表示的是依赖关系,也就是AddService依赖FileItemRepository,这里存在的问题是,FileItemRepository实际上是对应文件存储的实现方式而已,如果后续换了存储方式,比如换成了数据库,那么就需要修改AddService,也就是需要修改核心的处理业务逻辑的代码。

ioc-1

为此,引入了ItemRepository接口,AddService依赖这个接口,包含具体实现的FileItemRepository也依赖于这个接口,从依赖方向上看,对比前图,方向是反转了。

这么做的好处是,假设后续要支持数据库存储,只需要实现ItemRepository接口,在编译时或运行时决定ItemRepository的具体实现是哪个类即可,核心业务处理逻辑不需要改动。

高层策略不应该依赖低层细节

参考