如何从容应对复杂性(2)

简介: 如何从容应对复杂性

04

应对复杂性的利器


01


领域驱动设计——DDD

DDD是把业务模型翻译成系统架构设计的一种方式, 领域模型是对业务模型的抽象。

不是所有的业务服务都合适做DDD架构,DDD合适产品化,可持续迭代,业务逻辑足够复杂的业务系统,小规模的系统与简单业务不适合使用,毕竟相比较于MVC架构,认知成本和开发成本会大不少。但是DDD里面的一些战略思想我认为还是较为通用的。

1、对通用语言的提炼和推广


清晰语言认知, 比如之前在详情装修系统中:

ItemTemplate : 表示当前具体的装修页面ItemDescTemplate、Template,两个都能表示模板概念

刚开始接触这块的时候比较难理解这一块逻辑,之后在负责设计详情编辑器大融合这个项目时第一件事就是团队内先重新统一认知。

  • 装修页面统一使用 —— Page概念
  • 模板统一使用 —— Template概念


不将模板和页面的概念糅杂在一起,含糊不清,避免重复和混乱的概念定义。

2、贫血模型和充血模型


1)贫血模型


贫血模型的基本特征是:它第一眼看起来还真像这么回事儿。项目中有许多对象,它们的命名都是根据领域模型来的。然而当你真正检视这些对象的行为时,会发现它们基本上没有任何行为,仅仅是一堆getter/setter方法。

这些贫血对象在设计之初就被定义为只能包含数据,不能加入领域逻辑;所有的业务逻辑是放在所谓的业务层(xxxService, xxxManager对象中),需要使用这些模型来传递数据。


@Data
public class Person {
    /**
     * 姓名
     */
    private String name;
    /**
     * 年龄
     */
    private Integer age;
    /**
     * 生日
     */
    private Date birthday;
    /**
     * 当前状态
     */
    private Stauts stauts;
}
public class PersonServiceImpl implements PersonService {
    public void sleep(Person person) {
        person.setStauts(SleepStatus.get());
    }
    public void setAgeByBirth(Person person) {
        Date birthday = person.getBirthday();
        if (currentDate.before(birthday)) {
            throw new IllegalArgumentException("The birthday is before Now,It's unbelievable");
        }
        int yearNow = cal.get(Calendar.YEAR);
        int dayBirth = bir.get(Calendar.DAY_OF_MONTH);
        /*大概计算, 忽略月份等,年龄是当前年减去出生年*/
        int age = yearNow - yearBirth;
        person.setAge(age);
    }
}
}
public class WorkServiceImpl implements WorkService{
    public void code(Person person) {
        person.setStauts(CodeStatus.get());
    }
}

这一段代码就是贫血对象的处理过程,Person类, 通过PersonService、WorkingService去控制Person的行为,第一眼看起来像是没什么问题,但是真正去思考整个流程。WorkingService, PersonService到底是什么样的存在?与真实世界逻辑相比, 过于抽象。基于贫血模型的传统开发模式,将数据与业务逻辑分离,违反了 OOP 的封装特性,实际上是一种面向过程的编程风格。但是,现在几乎所有的 Web 项目,都是基于这种贫血模型的开发模式,甚至连 Java Spring 框架的官方 demo,都是按照这种开发模式来编写的。

面向过程编程风格有种种弊端,比如,数据和操作分离之后,数据本身的操作就不受限制了。任何代码都可以随意修改数据。

2)充血模型


充血模型是一种有行为的模型,模型中状态的改变只能通过模型上的行为来触发,同时所有的约束及业务逻辑都收敛在模型上。



@Data
public class Person extends Entity {
    /**
     * 姓名
     */
    private String name;
    /**
     * 年龄
     */
    private Integer age;
    /**
     * 生日
     */
    private Date birthday;
    /**
     * 当前状态
     */
    private Stauts stauts;
    public void code() {
        this.setStauts(CodeStatus.get());
    }
    public void sleep() {
        this.setStauts(SleepStatus.get());
    }
    public void setAgeByBirth() {
        Date birthday = this.getBirthday();
        Calendar currentDate = Calendar.getInstance();
        if (currentDate.before(birthday)) {
            throw new IllegalArgumentException("The birthday is before Now,It's unbelievable");
        }
        int yearNow = currentDate.get(Calendar.YEAR);
        int yearBirth = birthday.getYear();
        /*粗略计算, 忽略月份等,年龄是当前年减去出生年*/
        int age = yearNow - yearBirth;
        this.setAge(age);
    }
}
@Datapublic class Person extends Entity {    /**     * 姓名     */    private String name;
    /**     * 年龄     */    private Integer age;
    /**     * 生日     */    private Date birthday;
    /**     * 当前状态     */    private Stauts stauts;
    public void code() {        this.setStauts(CodeStatus.get());    }
    public void sleep() {        this.setStauts(SleepStatus.get());    }
    public void setAgeByBirth() {        Date birthday = this.getBirthday();        Calendar currentDate = Calendar.getInstance();        if (currentDate.before(birthday)) {            throw new IllegalArgumentException("The birthday is before Now,It's unbelievable");        }        int yearNow = currentDate.get(Calendar.YEAR);        int yearBirth = birthday.getYear();        /*粗略计算, 忽略月份等,年龄是当前年减去出生年*/        int age = yearNow - yearBirth;        this.setAge(age);    }
}


3)贫血模型和充血模型的区别




/**
 * 贫血模型
 */
public class Client {
    @Resource
    private PersonService personService;
    @Resource
    private WorkService workService;
    public void test() {
        Person person = new Person();
        personService.setAgeByBirth(person);
        workService.code(person);
        personService.sleep(person);
    }
}
/**
 * 充血模型
 */
public class Client {
    public void test() {
        Person person = new Person();
        person.setAgeByBirth();
        person.code();
        person.sleep();
    }
}


上面两段代码很明显第二段的认知成本更低,  这在满是Service,Manage 的系统下更为明显,Person的行为交由自己去管理, 而不是交给各种Service去管理。

贫血模型是事务脚本模式


贫血模型相对简单,模型上只有数据没有行为,业务逻辑由xxxService、xxxManger等类来承载,相对来说比较直接,针对简单的业务,贫血模型可以快速的完成交付,但后期的维护成本比较高,很容易变成我们所说的面条代码。

充血模型是领域模型模式

充血模型的实现相对比较复杂,但所有逻辑都由各自的类来负责,职责比较清晰,方便后期的迭代与维护。

面向对象设计主张将数据和行为绑定在一起也就是充血模型,而贫血领域模型则更像是一种面向过程设计,很多人认为这些贫血领域对象是真正的对象,从而彻底误解了面向对象设计的涵义。

Martin Fowler 曾经和 Eric Evans 聊天谈到它时,都觉得这个模型似乎越来越流行了。作为领域模型的推广者,他们觉得这不是一件好事,极力反对这种做法。

贫血领域模型的根本问题是,它引入了领域模型设计的所有成本,却没有带来任何好处。最主要的成本是将对象映射到数据库中,从而产生了一个O/R(对象关系)映射层。

只有当你充分使用了面向对象设计来组织复杂的业务逻辑后,这一成本才能够被抵消。如果将所有行为都写入到Service对象,那最终你会得到一组事务处理脚本,从而错过了领域模型带来的好处。而且当业务足够复杂时, 你将会得到一堆爆炸的事务处理脚本。

3、对业务的理解和抽象


限定业务边界,对业务进行与现实更自然的理解和抽象,数据模型与业务模型隔离,把业务映射成为领域模型沉淀在系统中。

4、结构与防腐层


User Interfaces负责对外交互, 提供对外远程接口

application应用程序执行其任务所需的代码。它协调域层对象以执行实际任务。该层适用于跨事务、安全检查和高级日志记录。

domain负责表达业务概念。对业务的分解,抽象,建模 。业务逻辑、程序的核心。防腐层接口放在这里。

infrastucture为其他层提供通用的技术能力。如repository的implementation(ibatis,hibernate, nosql),中间件服务等anti-corruption layer的implementation 防腐层实现放在这里。

防腐层的作用:封装三方服务。隔离内部系统对外部的依赖。

5、让隐性概念显性化

文档与注释可能会失去实时性(文档、注释没有人持续维护),但是线上生产代码是业务逻辑最真实的展现,减少代码中模糊的地方,让业务逻辑显性化体现出来,提升代码清晰度。

if (itemDO != null && MapUtils.isNotEmpty(itemDO.getFeatures()) && itemDO.getFeatures().containsKey(ITEM_PC_DESCRIPTION_PUSH)) {
   itemUpdateBO.getFeatures().put(ItemTemplateConstant.FEATURE_TSP_PC_TEMPLATEID, "" + templateId);
   itemUpdateBO.getFeatures().put(ItemTemplateConstant.FEATURE_TSP_SELL_PC_PUSH, "" + pcContent.hashCode());
} else {
   itemUpdateBO.getFeatures().put(ItemTemplateConstant.FEATURE_TSP_PC_TEMPLATEID, "" + templateId);
   itemUpdateBO.getFeatures().put(ItemTemplateConstant.FEATURE_TSP_WL_TEMPLATEID, "" + templateId);
   itemUpdateBO.getFeatures().put(ItemTemplateConstant.FEATURE_TSP_SELL_PC_PUSH, "" + pcContent.hashCode());
   itemUpdateBO.getFeatures().put(ItemTemplateConstant.FEATURE_TSP_SELL_WL_PUSH, "" + content.hashCode());
}


比如这一段代码就把判断里的业务逻辑隐藏了起来,这段代码其实的业务逻辑是这样, 判断商品是否有PC装修内容。如果有做一些操作, 如果没有做一些操作,将hasPCContent 这个逻辑表现出来, 一眼就能看出来大概的业务逻辑,让业务逻辑显现化,能让代码更清晰。可以改写成这样:


boolean hasPCContent = itemDO != null && MapUtils.isNotEmpty(itemDO.getFeatures()) && itemDO.getFeatures().containsKey(ITEM_PC_DESCRIPTION_PUSH);
if (hasPCContent) {
   itemUpdateBO.getFeatures().put(ItemTemplateConstant.FEATURE_TSP_PC_TEMPLATEID, "" + templateId);
   itemUpdateBO.getFeatures().put(ItemTemplateConstant.FEATURE_TSP_SELL_PC_PUSH, "" + pcContent.hashCode());
} else {
   itemUpdateBO.getFeatures().put(ItemTemplateConstant.FEATURE_TSP_PC_TEMPLATEID, "" + templateId);
   itemUpdateBO.getFeatures().put(ItemTemplateConstant.FEATURE_TSP_WL_TEMPLATEID, "" + templateId);
   itemUpdateBO.getFeatures().put(ItemTemplateConstant.FEATURE_TSP_SELL_PC_PUSH, "" + pcContent.hashCode());
   itemUpdateBO.getFeatures().put(ItemTemplateConstant.FEATURE_TSP_SELL_WL_PUSH, "" + content.hashCode());
}

02


简单设计原则——《Clean Code》

1、保持系统最大可测试

只要系统可测试并且越丰富的单元测试越会导向保持类短小且目的单一的设计方案,遵循单一职责的类,测试起来比较简单。

遵循有关编写测试并持续运行测试的简单、明确规则,系统就会更贴近OO低耦合度,高内聚度的目标。编写测试越多,就越会遵循DIP之类的规则,编写最大可测试可改进并走向更好的系统设计。

2、避免重复

重复是拥有良好设计系统的大敌。它代表着额外的工作、额外的风险和额外且不必要的复杂度。除了雷同的代码,功能类似的方法也可以进行包装减少重复,“小规模复用”可大量降低系统复杂性。要想实现大规模复用,必须理解如何实现小规模复用。

共性的抽取也会使代码更好符合单一职责原则。

3、更清晰的表达开发者的意图

软件项目的主要成本在于长期维护,当系统变得越来越复杂,开发者就需要越来越多的时间来理解他,而且也极有可能误解。

所以作者需要将代码写的更清晰:选用好名称、保持函数和类的短小、采用标准命名法、标准的设计模式名,编写良好的单元测试。用心是最珍贵的资源。

4、尽可能减少类和方法

如果过度使用以上原则,为了保持类的函数短小,我们可能会造出太多细小的类和方法。所以这条规则也主张函数和类的数量要少。

如应当为每个类创建接口、字段和行为必须切分到数据类和行为类中。应该抵制这类教条,采用更实用的手段。目标是在保持函数和类短小的同时,保持系统的短小精悍。不过这是优先级最低的一条。更重要的是测试,消除重复和清晰表达。

05

最后

A

总而言之,做业务开发其实一点也不简单,面对不确定性的问题域,复杂的业务变化,如何更好的理解和抽象业务,如何更优雅的应对复杂性,一直都是软件开发的一个难题。

在对抗软件熵增,寻找对抗软件复杂性,符合业务的构造定律的演进方式,我们一直都在路上。

参考


[1]  《Domain-Driven Design》 :https://book.douban.com/subject/1629512/

[2] 《Implementing Domain-Driven Design》 :https://book.douban.com/subject/25844633/

[3] 《Clean Code》:https://book.douban.com/subject/4199741/

[4]  《A Philosophy of Software Design》 :https://book.douban.com/subject/30218046/
------------------------------------------end------------------------------------------

相关文章
|
5月前
|
开发工具
链游开发的成本考量因素解析
链游开发的成本考量因素解析
|
10天前
|
供应链 安全 Java
软件架构一致性 —— 被忽视的研发成本
本文主要介绍了一些解决架构一致性问题的方法,以及我们应该如何去理解和应对部分不得不付出的成本。
|
3月前
|
缓存 运维 监控
|
7月前
|
消息中间件 存储 负载均衡
如何应对三高系统
如何应对三高系统
|
7月前
|
运维 监控 容灾
建设强大系统:提升高可用、可靠性和稳定性的秘诀
建设强大系统:提升高可用、可靠性和稳定性的秘诀
|
11月前
|
负载均衡 Kubernetes 算法
【韧性架构设计】分布式系统的韧性
【韧性架构设计】分布式系统的韧性
|
11月前
【架构质量】可靠性系列#1:可靠性与韧性
【架构质量】可靠性系列#1:可靠性与韧性
|
11月前
|
存储 分布式计算 Kubernetes
【软件架构】为杠杆(利用率)架构设计软件
【软件架构】为杠杆(利用率)架构设计软件
|
敏捷开发 设计模式 测试技术
如何从容应对复杂性(1)
如何从容应对复杂性
104 0
|
架构师 程序员 持续交付
应对复杂性
资本家主要目标是赚钱、赚很多很多的钱;他们给提出的要求是降本增效 那么作为架构师,目标是什么呢? 在《整洁架构》书中作者写到架构的主要目的是支持系统的生命周期。良好的架构使系统易于理解,易于开发,易于维护和易于部署。 最终目标是最小化系统的寿命成本并最大化程序员的生产力
107 0
应对复杂性

热门文章

最新文章