高效编码,快乐工作。

Martin Fowler 《Patterns of Enterprise Application Architecture》国外首次出版是2002年,国内中文版是2010年,书中有一节是讲架构分层的,之前写过一篇文章也讲系统架构分层的事情。

 

软件系统的分层,有效降低层与层之间的依赖

 

三层架构各层主要职责如下:

 

用户层:用户界面,比如 html 页面给用户提供服务、显示信息;Http 请求、批处理API等等

 

领域层:核心业务逻辑,系统的核心

 

数据层:与数据库打交道、缓存信息、发送消息、事务管理等等

 

今天结合实际案例聊一下三层架构以及很多企业依旧采用一种僵化、机械化的项目目录结构写着代码,变成了 crud 的搬运工。

 

记得学校学习开发,并没有明确的分层,那个时候还是 jsp 和 servlet ,用的 jdbc 链接数据库,有些业务直接就在 jsp 上面写,用 jstl 各种标签进行处理,写得还挺爽。

 

后来学习了 struts2、hibernate 、spring 也就是 ssh 框架后,project 的工程目录发生了变化,有了分层的概念,开始有了 dao、service 、controller、model 等等包。在 dao 和 service 每一层都会有一个 interfaces 接口包 和一个 impl 实现包。如下:

 

  • order

    •  — controller

    • — model

    • — service

      • — impl

      • — interfaces

    • — dao

      • — impl

      • — interfaces

           

这个目录结构我们在DDD群里聊并不知道出处,群里有不少朋友公司仍使用着这个分包结构(文章并不是说这种分包结构是错的,仅个人理解对此分包结构写点总结),我在读书那会大学的时候老师也是这样教的,在学校学习时自己在网上找得视频课程也是这样的情况,比如各个培训学校大佬们,马士兵、张孝祥课程都是这样,那就照葫芦画瓢这样写吧。

 

从学校出来实习工作,企业单位使用的也是 ssh ,项目工程目录如上面说的一样,时间久了,发现这个结构太僵化,每次都要在 dao、service 层 interfaces 加一个接口,然后再 impl里面实现。有时候,简单的保存基础数据操作,需要从 dao -> service -> controller 来一套(真是煎饼果子来一套),复制粘贴草草了事写完,细一看 dao 与 service 基本一样,就这样一写好几年。

 

今天一起聊聊这类项目结构的问题 和 如何调整,写得不一定对,大家一起探讨,改善软件开发效率,提升软件质量。

 


 

示例项目是一个非常简单的保存订单功能,用户选择一个商品,输入购买数量,点击保存订单按钮,后台会保存订单信息。因示例代码,只保存订单金额和数量,同时会生成一个订单编号,并未涉及到订单项相关的信息。

 

Product 为伪实现,直接在 dao 层返回了一个 价位:100,id:1 的 Product 对象。

 

 

问题1、业务分散不集中

 

1)业务在 用户层(controller) 实现

 

@PostMapping("/{proId}")
public void save(@PathVariable String proId, @RequestParam(name = "number") int number) {
    Product product = productService.getById(proId);
    if (product == null) {
        throw new IllegalArgumentException("product id is illegal");
    }
    String no = String.valueOf(Instant.now().toEpochMilli());
    Order order = new Order(no, product.getPrice() * number, number);
    orderService.save(order);
}

 

2)业务在 service 实现,也就是 service + 贫血模型 方式

 

@PostMapping("/2/{proId}")
public void save2(@PathVariable String proId, @RequestParam(name = "number") int number) {
    orderService.save(proId, number);
}

 

public void save(String id, int number) {
    Product product = productService.getById(id);
    if (product == null) {
        throw new IllegalArgumentException("product id is illegal");
    }
    String no = String.valueOf(Instant.now().toEpochMilli());
    Order order = new Order(no, product.getPrice() * number, number);
    orderDao.save(order);
}

 

3)业务在 用户层(controller) 和 service  中实现

 

@PostMapping("/3/{proId}")
public void save3(@PathVariable String proId, @RequestParam(name = "number") int number) {
    Product product = productService.getById(proId);
    if (product == null) {
        throw new IllegalArgumentException("product id is illegal");
    }
    orderService.save(product, number);
}

 

@Override
public void save(Product product, int number) {
    String no = String.valueOf(Instant.now().toEpochMilli());
    Order order = new Order(no, product.getPrice() * number, number);
    orderDao.save(order);
}

 

以上是个人能想到的三种情况,这三种情况中有 Service + 贫血模型,Order 对象中只有 setter 、getter 和 构造函数 来封装领域对象 Order ,通过orderDao 进行存储。另外,通过事务脚本的方式去做处理,Order 存在的目的仅仅只是为了传输值,没有其它任何作用。

 

采用这种方式进行开发,最优的是将代码逻辑放在 service ,假如业务复杂度较高,就会看见一个很长的 service 方法,尽管通过重构提取代码,service 都会变得臃肿,service 内部充斥着各种静态、私有、公有的方法。

 

 

问题2、对象行为难以重复使用,或者说这种方式就没有领域对象行为

 

有一天业务需求发生变更,系统要进行升级,需要有一个单独保存订单的接口,系统无法满足。需要将 orderService 中的业务逻辑进行复制,单独提取一个保存订单的接口。

 

public void save(double price, int number){
    String no = String.valueOf(Instant.now().toEpochMilli());
    Order order = new Order(no, price*number, number);
    orderDao.save(order);
}

 

此时,系统的重复代码会随着系统的复杂度不断的增加,重构影响的地方也会随着复杂度上升影响到更多代码,导致 bug 数量上升。比如,Order 编号no 的生成规则需要进行调整,则会发现,你需要检查很多个地方,测试各种情况,无形中增加了时间成本和降低研发效率。

 

有很多人会想,没事,我把这些封装成方法,统一在 service 中调用,想法不错,或许有一天你会忘记编号生成方法在哪里(但能很快找到一个OrderNo 的对象),同时你会有一个难看的 service,可读性差,难以维护。

 

 

问题3、结构僵化 和 重复代码

 

上面已经提到重复代码,这要说的是 dao  和 service 中的重复接口,重复代码问题。

 

比如,要实现一个方法根据产品 Id 进行查询产品。

 

代码如下:

 

public interface ProductDao {
    Product getById(String proId);
}

 

@Service("productDao")
public class ProductDaoImpl implements ProductDao {


    @Override
    public Product getById(String proId) {
        return new Product(1, 100d);
    }
}

 

public interface ProductService {
    Product getById(String proId);
}

 

@Service("productService")
public class ProductServiceImpl implements ProductService {
    @Autowired
    private ProductDao productDao;
    @Override
    public Product getById(String proId) {
        return productDao.getById(proId);
    }
}

 

特别僵化,必须要在 Dao 和 Service 都定义相同的接口,然后去实现它,如果是一个简单的管理功能,没有业务的情况下,多半都是重复代码,并且每一层都要一个接口是完全没有必要,不能为了面向接口编程而编程。

 

以上方式也有好处,简单,便于上手,对于一个逻辑较少的系统来说,使用起来非常自然,而且非常简便,一层套一层就行,系统运行也不会有多大开销。

 

 

如何改善?

 

基于领域对象实现,将核心业务逻辑内聚领域对象 Order 中

 

如下:

 

order

  • controller

    • DoOrderController

  • domain

    • Order

  • dao

    • DoOrderDao

 

Do 前缀为了在项目中区分,避免报错。假如业务简单,都可直接分一个业务包 order

 

实现如下:

 

@Data
public class Order {
    private String no;
    private double totalPrice;
    private int number;
    public Order(Product product, int aNumber) {
        this.no = no();
        this.totalPrice = totalPrice(product.getPrice(), aNumber);
        this.number = aNumber;
    }

    private String no() {
        return String.valueOf(Instant.now().getEpochSecond());
    }

    private double totalPrice(double price, int number) {
        return price * number;
    }

    public static Order save(Product product, int number) {
        if (isProduct(product)) {
            throw new IllegalArgumentException("product id is illegal");
        }
        return new Order(product, number);

}

 

    

    private static boolean isProduct(Product product) {

        return product == null;
    }}

 

@PostMapping
public void save(@RequestBody @Valid CreateOrderCommand command) {
    Product product = productDao.getById(command.getProId());
    Order order = Order.save(product, command.getNumber());
    doOrderDao.save(order);
}

 

@Service("doOrderDao")
public class DoOrderDao implements BaseSaveDao<Order> {

    @Autowired
    private JdbcTemplate jdbcTemplate;
    @Override
    @Transactional
    public void save(Order order) {
        StringBuffer stringBuffer = new StringBuffer();
        stringBuffer.append("insert into arc_t_order(no, totalPrice, number) values(?,?,?)");
        jdbcTemplate.update(stringBuffer.toString(), new  Object[]{order.getNo(), order.getTotalPrice(), order.getNumber()});
    }
}

 

通过调整可以看到核心业务,生成订单编号、计算订单金额、验证产品判断等等行为都内聚到领域对象 Order 中,丰富 Order 对象职责,也是 Order 应具有的职责。案例中类似 product 的判断,系统项目中会频繁的出现相似的判断,我们可将判断提取到领域对象结构中。

 

以上是基于领域对象实现方式,按照上面的结构实现会存在一个问题,用户层 Controller 中代码会很多,用户层原本的职责开始扭曲,那如何解决此问题呢?

 

Martin Fowler 在《Patterns of Enterprise Application Architecture》书中讲到 服务层 概念。

 

服务层

 

通过一个服务层来定义应用程序边界,在服务层中建立一组可用的操作集合,并在每个操作内部协调应用程序响应。

 

与DDD中的应用层职责非常相似,在DDD应用层的职责为领域服务编排,事务等等。

 

调整如下:

 

  • controller

    • DoOrderController

    • CreateOrderCommand

  • domain

    • Order

  • dao

    • DoOrderDao

  • service

    • DoOrderService

            

代码调整如下:

 

@PostMapping("/1")
public void save1(@RequestBody @Valid CreateOrderCommand command) {
    doOrderService.save(command);
}

 

@Component
public class DoOrderService {

    @Autowired
    private ProductDao productDao;
    @Autowired

private DoOrderDao doOrderDao;

 

    @Transactional

public void save(CreateOrderCommand command){
Product product = productDao.getById(command.getProId());
Order order = Order.save(product, command.getNumber());
doOrderDao.save(order);
}

}

 

服务层进行组织业务逻辑,封装了应用层的业务逻辑,同时简化了用户层,对于用户层来说,只需要调用一个操作集合即可。

 

系统架构问题,没有什么不能通过加一层解决的。

 

以上是个人的对系统架构的一个理解,很多企业仍使用一种僵化,形式化的结构进行开发,当系统业务变复杂,系统的冗余代码变多,变得难以维护。

 

领域模型的方式是将行为内聚到对象中,我们要思考是将行为拆分到相应对象中,丰富对象职责,提升代码的复用性。

 

另外,领域模型分为简单模型和复杂模型,文中案例就是简单领域模型,简单领域模型可与数据库表一一对应。

 

复杂领域模型则不行,它使用继承、策略和其它设计模式,是一张互联的细粒度对象组成的复杂网。

 

系统分层,分层带来的好处是职责清晰,使层级之间的依赖度尽可能的小。现在大家都是开始使用四层架构,我一直讲三层与四层就是一个应用层的区别。

 

所以,系统架构问题,没有什么不能通过加一层解决的。

 

https://github.com/geinimaichide/arc

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值