乐趣区

关于分布式:重拾面向对象软件设计

简介:从上个世纪五十年代冯·诺依曼发明第一台计算机开始,始终到当初只有短短 70 年工夫,从第一门计算机语言 FORTRAN,到当初咱们罕用的 C ++,JAVA,PYTHON 等,计算机语言的演进速度远超咱们所应用的任何一门自然语言。从最早的面向机器,再到面向过程,到演变为当初咱们所应用的面向对象。不变的是编程的主旨,变动的是编程的思维。

作者 | 聂晓龙
起源 | 阿里技术公众号

你还在用面向对象的语言,写着面向过程的代码吗?

一 前言

在欧洲文艺复兴期间,一位平凡的数学家天文学家 - 哥白尼,在过后提出了日心说,批驳了以地球为宇宙核心的天体思维,因为思维极其超前,直到半个世纪后开普勒伽利略等人通过前期钻研,才逐渐认可并确立了过后哥白尼思维的先进性。

独一无二,在软件工程畛域也演出着同样的故事。半个世纪前 Kristen Nygaard 创造了 Simula 语言,这也是当初被认同的世界上第一个明确实现面向对象编程的语言,他提出了基于类的编程格调,确定了 ” 万物皆对象 ” 这一面向对象实践的 ” 终极思维 ”,但在过后同样未受到认可。Peter Norvig 在 Design Patterns in Dynamic Programming 对此予以了批驳,并表述咱们并不需要什么面向对象。半个世纪后 Robert C.Martin、Bertrand Meyer、Martin Fowler 等人,再次印证并升华了面向对象的设计理念。编程思维的演进也不是欲速不达,但在这一个世纪失去了飞速的倒退。

二 编程思维的演进

从上个世纪五十年代冯·诺依曼发明第一台计算机开始,始终到当初只有短短 70 年工夫,从第一门计算机语言 FORTRAN,到当初咱们罕用的 C ++,JAVA,PYTHON 等,计算机语言的演进速度远超咱们所应用的任何一门自然语言。从最早的面向机器,再到面向过程,到演变为当初咱们所应用的面向对象。不变的是编程的主旨,变动的是编程的思维。

1 面向机器

计算机是 01 的世界,最早的程序就是通过这种 01 机器码来管制计算机的,比方 0000 代表读取,0001 代表保留等。实践上这才是世界上最快的语言,无需翻译间接运行。但弊病也很显著,那就是简直无奈保护。运行 5 毫秒,编程 3 小时。因为机器码无奈保护,人们在此基础上创造了汇编语言,READ 代表 0000,SAVE 代表 0001,这样更易了解和保护。尽管汇编在机器码上更可视更直观,但实质上还是一门面向机器的语言,仍然还是存在很高的编程老本。

2 面向过程

面向过程是一种以事件为核心的编程思维,相比于面向机器的编程形式,是一种微小的提高。咱们不必再关注机器指令,而是聚焦于具体的问题。它将一件事件拆分成若干个执行的步骤,而后通过函数实现每一个环节,最终串联起来实现软件设计。

流程化的设计让编码更加清晰,相比于机器码或汇编,开发效率失去了极大改善,包含当初依然有很多场景更适宜面向过程来实现。但软件工程最大的老本在于保护,因为面向过程更多聚焦于问题的解决而非畛域的设计,代码的重用性与扩展性弊病逐渐彰显进去,随着业务逻辑越来越简单,软件的复杂性也变得越来越不可控。

3 面向对象

面向对象以分类的形式进行思考和解决问题,面向对象的外围是抽象思维。通过形象提取共性,通过封装收敛逻辑,通过多态实现扩大。面向对象的思维实质是将数据与行为做联合,数据与行为的载体称之为对象,而对象要负责的是定义职责的边界。面向过程简略快捷,在解决简略的业务零碎时,面向对象的成果其实并不如面向过程。但在简单零碎的设计上,通用性的业务流程,个性化的差别点,原子化的性能组件等等,更适宜面向对象的编程模式。

但面向对象也不是银弹,甚至有些场景用比不必还糟,所有的本源就是形象。依据 MECE 法令 将一个事物进行分类,if else 是软件工程最谨严的分类。咱们在设计形象进行分类时,不肯定能抓住最合适的切入点,谬误的形象比没有形象复杂度更高。里氏替换准则的创始人 Barbara Liskov 谈形象的力量 The Power of Abstraction。

三 面向畛域设计

1 真在“面向对象”吗

// 捡入客户到销售私海
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 的债权。

2 畛域驱动设计

还有一种形式咱们也能够这么来写,新增“商机”模型,通过商机来关联客户与销售之间的关系。而商机的归属也分为公海、私海等具体归属场景。商机除了有必要的数据外,还应该收拢一些业务行为,捡入、凋谢、散发等。通过领域建模,利用面向对象的个性,确定边界、形象封装、行为收拢,对业务分而治之。

当咱们业务上说“商机散发到私海”,而咱们代码则是“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》

译:应用模型作为语言的支柱,意识到语言的扭转就是对模型的扭转,而后重构代码,重命名类,办法和模块以合乎新模型。

3 软件的复杂度

这是 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 个指标进行测量:耦合性、内聚性、充分性、完整性与基础性。

1 耦合性

一个模块与另一个模块之间建设起来的关联强度的测量称之为耦合性。一个模块与其余模块高度相干,那它就难以独立得被了解、变动或批改。TCL 语言发明者 John Ousterhout 传授也有同样的观点。咱们应该尽可能减少模块间的耦合依赖,从而升高复杂度。

Complexity is caused by two things: dependencies and obscurity.
— John Ousterhout《A Philosophy of Software Design》
译:复杂性是由两件事引起的:依赖性和模糊性。

但这并不意味着咱们就不须要耦合。软件设计是朝着扩展性与复用性倒退的,继承人造就是强耦合,但它为咱们提供了软件系统的复用能力。如同摩擦力个别,起初认为它妨碍了咱们后退的步调,实则没有摩擦力,咱们举步维艰。

2 内聚性

内聚性与耦合性都是结构化设计中的概念,内聚性测量的是单个模块里,各个元素的的分割水平。高内聚低耦合,是写在教科书里的观点,但咱们也并非何时何地都应该自觉谋求高内聚。

内聚性分为必然性内聚与功能性内聚。金鱼与消防栓,咱们一样能够因为它们都不会吹口哨,将他们形象在一起,但很显著咱们不该这么干,这就是必然性内聚。最心愿呈现的内聚是功能性内聚,即一个类或模式的各元素一起工作,提供某种清晰界定的行为。比方我将消防栓、灭火器、探测仪等内聚在一起,他们是都属于消防设施,这是功能性内聚。

3 充分性

充分性指一个类或模块须要应该记录某个形象足够多的特色,否则组件将变得不必。比方 Set 汇合类,如果咱们只有 remove、get 却没有 add,那这个类肯定没法用了,因为它没有造成一个闭环。不过这种状况绝对呈现较少,只有当咱们真正去应用,实现它的一系列流程操作后,缺失的一些内容是比拟容易发现并解决的。

4 完整性

完整性指类或模块须要记录某个形象全副有意义的特色。完整性与充分性绝对,充分性是模块的最小外延,完整性则是模块的最大内涵。咱们走完一个流程,能够清晰得晓得咱们缺哪些,能够让咱们马上补齐形象的充分性,但可能在另一个场景这些特色就又不够了,咱们须要思考模块还须要具备哪些特色或者他应该还补齐哪些能力。

5 基础性

充分性、完整性与基础性能够说是 3 个互相辅助互相制约的准则。基础性指形象底层表现形式最无效的基础性操作(仿佛用本人在解释本人)。比方 Set 中的 add 操作,是一个基础性操作,在曾经存在 add 的状况下,咱们是否须要一次性增加 2 个元素的 add2 操作?很显著咱们不须要,因为咱们能够通过调用 2 次 add 来实现,所以 add2 并不合乎基础性。

但咱们试想另一个场景,如果要判断一个元素是否在 Set 汇合中,咱们是否须要减少一个 contains 办法。Set 曾经有 foreach、get 等操作了,依照基础性实践,咱们也能够把所有的元素遍历一遍,而后看该元素是否蕴含其中。但基础性有一个关键词叫“无效”,尽管咱们能够通过一些根底操作进行组合,但它会耗费大量资源或者复杂度,那它也能够作为根底操作的一个候选者。

五 软件设计准则

形象的品质能够领导咱们形象与建模,但总归还是不够具象,在此基础上一些更落地更易执行的设计准则涌现进去,最驰名的当属面向对象的五大设计准则 S.O.L.I.D。

1 开闭准则 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 的排序其实曾经为咱们定义了这样的规范。咱们将不同的排序形式进行形象,每种逻辑独自实现,单个调整逻辑不影响其余内容,新增排序形式也无需对已有模块进行调整。

2 依赖倒置 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 接口,杯子依赖咱们的需要进行设计。所以是杯子依赖咱们,而非咱们依赖杯子。

依赖倒置准则并非一个新发明的实践,咱们生存的很多中央都有在使用。比方一家公司须要设立“法人”,如果这家公司出了问题,监管局就会找公司法人。并非监管局依赖公司提供的法人职位,它能够找到人,而是公司依赖监管局的要求,才设立法人职位。这也是依赖倒置的一种体现。

3 其余设计准则

这里没有一一将 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 准则的案例,咱们来看上面这段代码。

在分享会上,我成心将这行正文遮盖起来,大家都猜不到 newFormatter.getClass() 这句代码写在这里的作用。如果要查看空指针,齐全能够用 Objects 工具类提供的办法,实现齐全一样,但代码体现进去的含意就千差万别了。

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》
译:你不该自觉的听从这些规定,应该只在偶然状况下,有充沛理由后才去突破这些规定

学习编程艺术首先要学会根本的规定,而后能力晓得什么时候能够突破这些规定

原文链接
本文为阿里云原创内容,未经容许不得转载。

退出移动版