作者: 聂晓龙

你还在用面向目标的言语,写着面向进程的代码吗?

前语

在欧洲文艺复兴时期,一位伟大的数学家天文学家-哥白尼,在其时提出了日心说,批驳了以地球为国际中心的天体思想,由于思想极其超前,直到半个世纪后开普勒伽利略等人经过后期研究,才逐步认可并确立了其时哥白尼思想的先进性。

无独有偶,在软件工程范畴也上演着相同的故事。半个世纪前 Kristen Nygaard 发明晰 Simula 言语,这也是现在被认同的国际上榜首个明晰完结面向目标编程的言语,他提出了根据类的编程风格,确认了”万物皆目标”这一面向目标理论的”终极思想”,但在其时相同未受到认可。Peter Norvig 在 Design Patterns in Dynamic Programming 对此予以了批驳,并表述咱们并不需求什么面向目标。半个世纪后 Robert C.Martin、Bertrand Meyer、Martin Fowler 等人,再次印证并提高了面向目标的规划理念。编程思想的演进也不是一蹴即至,但在这一个世纪得到了飞速的开展。

编程思想的演进

从上个世纪五十年代冯诺依曼发明榜首台计算机开端,一直到现在只要短短 70 年时刻,从榜首门计算机言语 FORTRAN,到现在咱们常用的 C++,JAVA,PYTHON 等,计算机言语的演进速度远超咱们所运用的任何一门自然言语。从最早的面向机器,再到面向进程,到演化为现在咱们所运用的面向目标。不变的是编程的主旨,改动的是编程的思想。

面向机器

重拾面向对象软件设计

计算机是 01 的国际,最早的程序便是经过这种 01 机器码来控制计算机的,比如 0000 代表读取,0001 代表保存等。理论上这才是国际上最快的言语,无需翻译直接运转。但弊端也很显着,那便是简直无法保护。运转 5 毫秒,编程 3 小时。由于机器码无法保护,人们在此根底上发明晰汇编言语,READ 代表 0000,SAVE 代表 0001,这样更易理解和保护。尽管汇编在机器码上更可视更直观,但实质上仍是一门面向机器的言语,依然仍是存在很高的编程本钱。

面向进程

重拾面向对象软件设计

面向进程是一种以作业为中心的编程思想,比较于面向机器的编程办法,是一种巨大的进步。咱们不必再重视机器指令,而是聚焦于详细的问题。它将一件作业拆分红若干个履行的进程,然后经过函数完结每一个环节,终究串联起来完结软件规划。

流程化的规划让编码愈加明晰,比较于机器码或汇编,开发效率得到了极大改善,包括现在仍然有许多场景更合适面向进程来完结。但软件工程最大的本钱在于保护,由于面向进程更多聚焦于问题的处理而非范畴的规划,代码的重用性与扩展性弊端逐步彰显出来,跟着事务逻辑越来越杂乱,软件的杂乱性也变得越来越不可控。

面向目标

重拾面向对象软件设计

面向目标以分类的办法进行思考和处理问题,面向目标的中心是笼统思想。经过笼统提取共性,经过封装收敛逻辑,经过多态完结扩展。面向目标的思想实质是将数据与行为做结合,数据与行为的载体称之为目标,而目标要担任的是界说责任的边界。面向进程简略快捷,在处理简略的事务系统时,面向目标的效果其实并不如面向进程。但在杂乱系统的规划上,通用性的事务流程,个性化的差异点,原子化的功能组件等等,更合适面向目标的编程形式。

但面向目标也不是银弹,甚至有些场景用比不必还糟,一切的根源便是笼统。根据 MECE 规律将一个事物进行分类,if else 是软件工程最严谨的分类。咱们在规划笼统进行分类时,纷歧定能抓住最合适的切入点,过错的笼统比没有笼统杂乱度更高。里氏替换准则的创始人 Barbara Liskov 谈笼统的力量 The Power of Abstraction。

面向范畴规划

真在”面向目标“吗

// 捡入客户到出售私海
public String pick(String salesId, String customerId){
    // 校验是否出售人物
    Operator operator = dao.find("db_operator", salesId);
    if("SALES".equals(operator.getRole())){
        return "operator not sales";
    }
    // 校验出售库容是否已满
    int hold = dao.find("sales_hold", salesId);
    List<CustomerVo> customers = dao.find("db_sales_customer", salesId);
    if(customers.size() >= hold){
        return "hold is full";
    }
    // 校验是否客户可捡入
    Opportunity opp = dao.find("db_opportunity", customerId);
    if(opp.getOwnerId() != null){
        return "can not pick other's customer";
    }
    // 捡入客户
    opp.setOwnerId(salesId);
    dao.save(opp);
    return "success";
}

这是一段 CRM 范畴出售捡入客户的事务代码。这是咱们熟悉的 Java-面向目标言语,但这是一段面向目标代码吗?彻底面向作业,没有封装没有笼统,难以复用不易扩展。相信在咱们代码库,这样的代码不在少数。为什么?由于它将本钱放到了未来。咱们将此称之为“披着面向目标的外衣,干着面向进程的勾当。”

在系统规划的前期,事务规矩不杂乱,逻辑复用与扩展体现得也并不激烈,而面向进程的代码在支撑这些相对简略的事务场景是十分容易的。但软件工程最大的本钱在于保护,当系统足够杂乱时,当初那些写起来最 easy 的代码,将来便是保护起来最 hard 的债务。

范畴驱动规划

重拾面向对象软件设计

还有一种办法咱们也能够这么来写,新增“商机”模型,经过商机来相关客户与出售之间的关系。而商机的归属也分为公海、私海等详细归属场景。商机除了有必要的数据外,还应该收拢一些事务行为,捡入、敞开、分发等。经过范畴建模,利用面向目标的特性,确认边界、笼统封装、行为收拢,对事务分而治之。

当咱们事务上说“商机分发到私海”,而咱们代码则是“opportunity.pickTo(privateSea)”。这是范畴驱动所带来的改动,面向范畴规划,面向目标编程,范畴模型的笼统便是对现实国际的描述。但这并非一蹴即至的进程,当你只触碰到大象的身板时,你认为这是一扇门,当你触碰到大象的耳朵时,你认为是一片芭蕉。只要咱们不断笼统不断重构,咱们才能益发接近事务的实在模型。

Use the model as the backbone of a language, Recognize that a change in the language is a change to the model.Then refactor the code, renaming classes, methods, and modules to conform to the new model

— Eric Evans 《Domain-Driven Design Reference》

译:运用模型作为言语的支柱,意识到言语的改动便是对模型的改动,然后重构代码,重命名类,办法和模块以契合新模型。

软件的杂乱度

重拾面向对象软件设计

这是 Martin Flowler 在《Patterns of Enterprise Application Architecture》这本书中所提的关于杂乱度的观念,他将软件开发分为数据驱动与范畴驱动。许多时候开发的办法大家倾向于,拿到需求后看表怎样规划,然后看代码怎样写,这其实也是面向进程的一个体现。在软件初期,这样的办法杂乱度是很低的,没有复用没有扩展,一人吃饱全家不饿。但跟着事务的开展系统的演进,杂乱度会陡增。

而一开端经过范畴建模办法,以面向目标思想进行软件规划,杂乱度的上升能够得到很好的控制。先思考咱们范畴模型的规划,这是咱们事务系统的中心,再逐步外延,到接口到缓存到数据库。但范畴的边界,模型的笼统,从刚开端本钱是高于数据驱动的。

The goal of software architecture is to minimize the human resources required to build and maintain the required system.

— Robert C. Martin 《Clean Architecture》

译:软件架构的终极目标是,用最小的人力本钱来满足构建和保护该系统的需求

假如刚开端咱们直接以数据驱动面向进程的流程式代码,能够很轻松的处理问题,并且之后也不会面向更杂乱的场景与事务,那这套形式便是最合适这套系统的架构规划。假如咱们的系统会跟着事务的开展逐步杂乱,每一次的发布都会提高下一次发布的本钱,那么咱们应该考虑投入必要的本钱来面向范畴驱动规划。

笼统的质量

笼统永远是软件工程范畴最难的命题,由于它没有规矩,没有规范,甚至没有对错,只分好坏,只分是否合适。相同一份淘宝商品模型的范畴笼统,能够算是业界标杆了,但它并非合适你的系统。那咱们该如何驾御“笼统”呢?UML 的创始人 Grady booch 在 Object Oriented Analysis and Design with Applications 一书中,说到了评判一种笼统的质量能够经过如下 5 个目标进行测量:耦合性、内聚性、充沛性、完整性与根底性。

耦合性

一个模块与另一个模块之间建立起来的相关强度的测量称之为耦合性。一个模块与其他模块高度相关,那它就难以独立得被理解、改动或修正。TCL 言语发明者 John Ousterhout 教授也有相同的观念。咱们应该尽或许减少模块间的耦合依靠,从而下降杂乱度。

Complexity is caused by two things: dependencies and obscurity.

— John Ousterhout 《A Philosophy of Software Design》

译:杂乱性是由两件事引起的:依靠性和模糊性。

但这并不意味着咱们就不需求耦合。软件规划是朝着扩展性与复用性开展的,承继天然便是强耦合,但它为咱们供给了软件系统的复用才能。如同摩擦力一般,起先以为它阻碍了咱们行进的步伐,实则没有摩擦力,咱们寸步难行。

内聚性

内聚性与耦合性都是结构化规划中的概念,内聚性测量的是单个模块里,各个元素的的联系程度。高内聚低耦合,是写在教科书里的观念,但咱们也并非何时何地都应该盲目寻求高内聚。

内聚性分为偶然性内聚与功能性内聚。金鱼与消防栓,咱们相同能够由于它们都不会吹口哨,将他们笼统在一起,但很显着咱们不应这么干,这便是偶然性内聚。最期望呈现的内聚是功能性内聚,即一个类或形式的各元素一起作业,供给某种明晰界定的行为。比如我将消防栓、灭火器、探测仪等内聚在一起,他们是都属于消防设施,这是功能性内聚。

充沛性

充沛性指一个类或模块需求应该记载某个笼统足够多的特征,否则组件将变得不必。比如 Set 集合类,假如咱们只要 remove、get 却没有 add,那这个类一定无法用了,由于它没有构成一个闭环 。不过这种情况相对呈现较少,只要当咱们真实去运用,完结它的一系列流程操作后,缺失的一些内容是比较容易发现并处理的。

完整性

完整性指类或模块需求记载某个笼统全部有意义的特征。完整性与充沛性相对,充沛性是模块的最小内在,完整性则是模块的最大外延。咱们走完一个流程,能够明晰得知道咱们缺哪些,能够让咱们马上补齐笼统的充沛性,但或许在另一个场景这些特征就又不行了,咱们需求考虑模块还需求具有哪些特征或许他应该还补齐哪些才能。

根底性

充沛性、完整性与根底性能够说是 3 个相互辅助相互制约的准则。根底性指笼统底层体现形式最有用的根底性操作(似乎用自己在解说自己)。比如 Set 中的 add 操作,是一个根底性操作,在现已存在 add 的情况下,咱们是否需求一次性添加 2 个元素的 add2 操作?很显着咱们不需求,由于咱们能够经过调用 2 次 add 来完结,所以 add2 并不契合根底性。

但咱们试想另一个场景,假如要判断一个元素是否在 Set 集合中,咱们是否需求添加一个 contains 办法。Set 现已有 foreach、get 等操作了,依照根底性理论,咱们也能够把所有的元素遍历一遍,然后看该元素是否包含其中。但根底性有一个关键词叫“有用”,尽管咱们能够经过一些根底操作进行组合,但它会耗费大量资源或许杂乱度,那它也能够作为根底操作的一个候选者。

软件规划准则

笼统的质量能够辅导咱们笼统与建模,但总归仍是不行具象,在此根底上一些更落地更易履行的规划准则出现出来,最著名的当属面向目标的五大规划准则 S.O.L.I.D。

开闭准则 OCP

Software entities should be open for extension,but closed for modification

— Bertrand Meyer 《Object Oriented Software Construction》

译:软件实体应当对扩展敞开,对修正封闭。

开闭准则是 Bertrand Meyer 1988 年在《Object Oriented Software Construction》书中所说到一个观念,软件实体应该对扩展敞开对修正封闭。

咱们来看一个关于开闭准则的比如,需求传进来的用户列表,分类型进行二次排序,咱们代码能够这样写。

public List<User> sort(List<User> users, Enum type){
    if(type == AGE){
        // 按年纪排序
        users = resortListByAge(users);
    }else if(type == NAME){
        // 按称号首字母排序
        users = resortListByName(users);
    }else if(type == NAME){
        // 按客户健康分排序
        users = resortListByHealth(users);
    }
    return users;
}

上述代码便是一个显着违背开闭准则的比如,当咱们需求新增一种相似时,需求修正主流程。由于这些办法都界说在私有函数中,咱们哪怕对现有逻辑做调整,咱们也需求修正到这份代码文件。

还有一种做法,能够完结对扩展敞开对修正封闭,JDK 的排序其完成已为咱们界说了这样的规范。咱们将不同的排序办法进行笼统,每种逻辑单独完结,单个调整逻辑不影响其他内容,新增排序办法也无需对已有模块进行调整。

重拾面向对象软件设计

依靠倒置 DIP

High level modules shouldnot depend upon low level modules.Both should depend upon abstractions. Abstractions should not depend upon details. Details should depend upon abstractions

— Robert C.Martin C++ Report 1996

译:高层模块不应该依靠低层模块,两者都应该依靠笼统;笼统不应该依靠细节,细节应该依靠笼统。

Robert C.Martin 是《Clean Code》《Code Architecture》两本经典书本的作者,1996 年他在 C++ Report 中发表了一篇名为 The Dependency Inversion Principle 的文章。他认为模块间的依靠应该是有序的,高层不应该依靠低层,低层应该依靠高层,笼统不应该依靠细节,细节应该依靠笼统。

重拾面向对象软件设计

怎样理解 Robert C.Martin 的这一观念?咱们看上面这张图,咱们的手能够握住这个杯子,是咱们依靠杯子吗?有人说咱们需求调杯子供给的 hold 服务,咱们才能握住它,所以是咱们依靠杯子。但咱们再思考一下,棍子咱们是不是也能够握,水壶咱们也能够握,但猫狗却不行,为什么?由于咱们的杯子是依照咱们的手型进行规划的,咱们界说了一个可握持的 holdable 接口,杯子依靠咱们的需求进行规划。所以是杯子依靠咱们,而非咱们依靠杯子。

重拾面向对象软件设计

依靠倒置准则并非一个新发明的理论,咱们生活的许多地方都有在运用。比如一家公需求建立“法人”,假如这家公司出了问题,监管局就会找公司法人。并非监管局依靠公司供给的法人职位,它能够找到人,而是公司依靠监管局的要求,才建立法人职位。这也是依靠倒置的一种体现。

其他规划准则

这儿没有一一将 S.O.L.I.D 一一列举完,大家想了解的能够自行查阅。除了 SOLID 之外,还有一些其他的规划准则,相同也十分优秀。

PLOA 最小惊讶准则

If a necessary feature has a high astonishment factor, it may be necessary to redesign the feature

— Michael F. Cowlishaw

译:假如必要的特征具有较高的惊人要素,则或许需求从头规划该特征。

PLOA 最小惊讶准则是斯坦福大家计算机教授 Michael F. Cowlishaw 提出的。不论你的代码有“多好”,假如大部分人都对此感到吃惊,或许咱们应该从头规划它。JDK 中就存在一例违背 PLOA 准则的事例,咱们来看下面这段代码。

/**
 * Set a <tt>Formatter</tt>.  This <tt>Formatter</tt> will be used
 * to format <tt>LogRecords</tt> for this <tt>Handler</tt>.
 * <p>
 * Some <tt>Handlers</tt> may not use <tt>Formatters</tt>, in
 * which case the <tt>Formatter</tt> will be remembered, but not used.
 * <p>
 * @param newFormatter the <tt>Formatter</tt> to use (may not be null)
 * @exception  SecurityException  if a security manager exists and if
 *             the caller does not have <tt>LoggingPermission("control")</tt>.
 */
public synchronized void setFormatter(Formatter newFormatter) throws SecurityException {
    checkPermission();
    // Check for a null pointer:
    newFormatter.getClass();
    formatter = newFormatter;
}

在共享会上,我故意将这行注释遮盖起来,大家都猜不到 newFormatter.getClass() 这句代码写在这儿的效果。假如要查看空指针,彻底能够用 Objects 东西类供给的办法,完结彻底相同,但代码体现出来的含义就千差万别了。

public static <T> T requireNonNull(T obj) {
    if (obj == null)
        throw new NullPointerException();
    return obj;
}

KISS 简略准则

Keep it Simple and Stupid

— Robert S. Kaplan

译:坚持愚笨,坚持简略

KISS 准则是 Robert S. Kaplan 提出的一个理论,Kaplan 并非是一个软件学家,他是平衡积分卡 Balanced Scorecard 创始人,而他所提出的这个理论对软件职业依然适用。把作业变杂乱很简略,把作业变简略很杂乱。咱们需求尽量让杂乱的问题简明化、简略化。

写在最终

软件规划的最大目标,便是下降杂乱性,万物不为我所有,但万物皆为我用。引用 JDK 集合结构创办人Josh Bloch的一句话来完毕。学习编程艺术首先要学会根本的规矩,然后才能知道什么时候能够打破这些规矩。

You should not slavishly follow these rules, but violate them only occasionally and with good reason. Learning the art of programming, like most other disciplines, consists of first learning the rules and then learning when to break them.

— Josh Bloch 《Effective Java》

译:你不应盲目的遵照这些规矩,应该只在偶尔情况下,有充沛理由后才去打破这些规矩

学习编程艺术首先要学会根本的规矩,然后才能知道什么时候能够打破这些规矩

参阅书本

1、《Object Oriented Analysis and Design with Applications》

niexiaolong.github.io/Object%20Or…

2、《Clean Architecture》

detail.tmall.com/item.htm?id…

3 、《A Philosophy of Software Design》

www.amazon.com/-/zh/dp/173…