如何写好代码

如何写好代码

一、什么是好代码

抛开性能、并发、一致性等技术因素,好的业务代码,应当如一篇显浅易懂的业务叙实文章,满足以下几个基本条件:

  • 词要达意:最基础的变量、函数、类的命名,是否名达其意。
  • 结构清晰:类的关系结构,函数的调用结构,是否如文章的章节、段落划分层次分明和逻辑清晰。
  • 紧扣主题:包、类、函数是否内聚,是否破坏了单一和开闭原则。

因此,好代码如同好文章,它应该是饱含业务语义(词要达意)、具有自明性和可读性(结构清晰),能够显性化表达业务意图(紧扣主题),让人赏心悦目。

二、从最基本做起

1、命名

好的代码,从好的命名开始,做到名副其实。

变量命名

变量名是名词,要正确和清晰地描述业务语义,如果一个变量需要通过注释补充说明,那可能就是没取好变量名。

变量命名的关键点:

  1. 词要达意:避免无业务语义的命名,如:list、val、a…;
  2. 语境范围:避免小范围词套大范围数据,反之亦然,不使用过于宽泛的名词。
  3. 名词复数:统一风格,加s或List尾缀,变量名建议使用s尾缀,函数名建议使用List尾缀。
  4. 后置限定词:限定词是对前面变量名的修饰,可以描述名词的作用范围属性,例如:
  • 请求入参:xxxQuery/xxxRequest
  • 返回结果:xxxResponse/xxxResult
  • 传参数据:xxxDTO/xxxVO/xxxInfo
  • 运算结果:xxxTotal(总和)/xxxMax(最大值)/xxxAverage(平均值)
  • Bad case:
// 1、coupon是promotion的子类型,这里把大范围的数据赋值给一个小范围的名词,表达失准。
List<ShoppingPromotionInfo> couponList = JSON.parseArray(promotionInfoStr, ShoppingPromotionInfo.class);
// 2、实际是取运费活动优惠,promotion蕴含的语义范围超出了运费活动优惠。
// 3、命名没有保持一致性,上面用List做后置限定词,这里用s做尾缀。
List<ShoppingPromotionInfo> promotionInfos = couponList.stream()
    .filter(x->x.getType()== PromotionTypeEnum.POSTAGE_ACTIVITY.getType())
    .collect(Collectors.toList());
  • Good case:
List<ShoppingPromotionInfo> promotions = JSON.parseArray(promotionInfoStr, ShoppingPromotionInfo.class);
// 通过变量名的范围,就可以知道是从promotions中筛选出运费活动优化。
List<ShoppingPromotionInfo> postageActivityPromotions = promotions.stream()
    .filter(x->x.getType()== PromotionTypeEnum.POSTAGE_ACTIVITY.getType())
    .collect(Collectors.toList());

函数命名

函数命名要体现做什么,而不是怎么做,要清楚表达出操作意图业务语义

函数命名的关键点:

  1. 动名词搭配,动词表达操作意图,名词表达业务语义。
  2. 正反操作使用对仗词,例如:
  • add/remove
  • open/close
  • begin/end
  • insert/delete
  • first/last
  • min/max
  • Bad Case:
public interface MemberFacade {
    /**
     * 查询会员信息
     *
     * 命名的问题:
     *  1.没有表达操作意图:build是构建,没有表达查询的意图。
     *  2.没有表达业务语义:没有表达build的目标对象是什么。
     **/
    SingleResult<MemberDTO> build(MemberBuildRequest request);
    /**
     * 更新会员信息
     *
     * 命名的问题:
     *  1.没有表达业务语义:不知道具体更新会员的哪些信息
     *  2.操作意图表达过于宽泛:update可以把所有会员信息更新都放在这个函数里。
     **/
    SingleResult update(MemberUpdateRequest request);
}
  • Good Case:
public interface MemberFacade {
    /**
     * 查询会员信息
     **/
    SingleResult<MemberDTO> queryMember(MemberQuery query);
    /**
     * 更新会员昵称
     **/
    SingleResult updateNick(MemberUpdateRequest request);
}

类命名

类是面向对象中最重要的概念,是一组关联数据的相关操作的封装,通常可以把类分为两种:

  • **实体类:**承载业务的核心数据和业务逻辑,命名要充分体现业务语义,比如Order/Buyer/Item。
  • 辅助类:协调实体类完成业务逻辑,命名通常加后缀体现出其功能性,比如OrderQueryService/OrderRepository。

函数命名的关键点:

  1. 辅助类尽量避免用 Helper/Util 之类的后缀,因为其含义过于笼统,容易破坏单一职责原则。
  2. 针对某个实体的辅助操作过多,或单个操作很复杂,可通过 “实体 + 操作类型 + 功能后缀”来命名,同时符合职责单一和接口隔离的原则,比如OrderService:
  • OrderCreateService:订单创建服务。
  • OrderUpdateService:订单更新服务。
  • OrderQueryService:订单查询服务。

建模命名

1.事件命名
事件是表示发生在过去的事情,所以在命名上推荐使用Domain Name +动词的过去式+ Event,这样可以更准确地表达业务语义。例如,在银行转账的例子中,对于转账成功和失败我们都需要发出事件通知,可以定义两个领域事件如下。
(1)MoneyTransferedEvent:表示转账成功发出的事件。
(2)MoneyTransferFailedEvent:表示转账失败发出的事件。

包命名

包(package)是一组强关联(内聚)的类的集合,起分类收纳和命名空间的作用。

  • 包名应该要反映一组类在更高抽象层次上的联系,比如类Apple、Orange都是水果,可以收纳进fruit包内。
  • 包的命名要大小适中,不能太具体,也不能太抽象。比如包名叫Apple,太具体导致类Orange放不进去,又比如包名叫food,太抽象导致其他非水果也被放进来了。

实际工程中,常见的分类维度主要是两种,按功能性或业务域分类。

  • 功能性分类:metaq、mapper、service、dao等。
  • 业务域分类:user、item、order、promotion等。

同一层级的包,要严格保持分类维度的一致性,要么先按业务域分类,再按功能性分类;要么就先按功能性分类,再按业务域分类。

client
  |----request
  				|----order
          |----item
  |----response
  				|----order
          |----item
  |----service
  				|----OrderQueryService.java
          |----ItemQueryService.java

2、函数设计原则

有时候,优雅的实现仅仅是一个函数,不是一个类,不是一个框架,只是一个函数。 —— John Carmack

a. 函数要短小、专一

  • **短小:**一个函数不超过50行代码,大量的setXXX()除外。
  • **专一:**一个函数只做一件事情,符合单一职责原则。

b. 函数抽象层次保持一致

遵循金字塔原则,把函数层层递进的调用,理解成结论先行,自上而下的表达过程

同层函数是对上一层的支撑,同层间要符合MECE法则,应描述和处理同一逻辑范畴的事情,高层抽象和底层细节不能杂糅在一起,否则会变得凌乱和难以理解。

MECE是(Mutually Exclusive Collectively Exhaustive)的缩写,指的是“相互独立,完全穷尽”的分类原则。通过MECE方法对问题进行分类,能做到清晰准确,从而容易找到答案。
1

3、模块分层原则

a. 模块分层

2

  • client:外部可见层(暴露服务声明)
  • service:业务逻辑层,对client层的实现,协调domain和infrastructure一起完成业务逻辑。
  • domain:领域层,对应DDD中的领域知识。
  • infrastructure:基础设施层,数据库访问、消息、外部调用等。
  • start:应用启动层,主要是项目启动时的静态配置。

b. 模块内包分层

分包的建议:

  • 如果有多个一级域,建议:一级按业务分包,二级按功能分包,三级可按子领域分包。
  • 如果仅一个一级域,建议:一级按功能分包,二级按子领域分包。

例如:

|- xxx
|---- xxx-client             // 只提供仅需的外部依赖(DO不能再这层定义)
    |----biz1                // 子域module
        |----event					 // 领域事件声明
        |----constant				 // 常量(enum、final)
        |----dto             // 服务的出参对象,value object/view object
        |----request         // request or query
        |----service         // 本地/HSF服务接口声明
        |----facade          // 提供给Top/MTop的HSF接口
|---- xxx-domain             // 提供领域能力(entity、domainservice等)
    |----biz1                // 子域module
        |----factory         // 类工厂(构建器/转换器/工厂类)
        |----entity          // 充血模型的实体类
        |----service         // 领域服务
        |----repository      // api gateway 或 db repository接口,实现放在infrastructure
  |--test										 // 领域层单元测试
|---- xxx-infrastructure     // 基础设施层: mapper/config/repository impl
  |--main
    |----biz1                // 子域module
        |----dataobject      // do: 贫血模型
        |----mapper					 // mybatis mapper
        |----repository      // repository impl
  |--test										 // 基础设施层单元测试
|---- xxx-service            // 调用外域服务,使用domain层的能力
    |----biz1                // 子域module
        |----factory         // 类工厂(构建器/转换器/工厂类)
        |----service         // HSF服务接口实现
        |----facade          // 提供给Top/MTop的HSF接口实现,通常是调service的服务
        |----dts
        |----metaq
  |--test					 // 服务实现层单元测试
|---- xxx-starter
  |--test				     // 集成测试

三、耦合与内聚

软件设计的目标是高内聚、低耦合。

如果代码是高耦合和低内聚的,就会出现修改一个逻辑,会导致多处代码要修改,可能影响到多个业务链路,这增加了出bug的业务风险,同时增加了测试回归的范围,导致研发成本增加。

耦合和内聚,是我们常挂在嘴边的话,但是大家却说不太清楚,讲不太明白,很难衡量:

  • 什么样的叫高内聚,什么样的叫低耦合?
  • 高内聚要高到什么程度,低耦合要低到什么程度?

1、耦合的类型

3

耦合是描述模块(系统/模块/类/函数)之间相互联系(控制/调用/数据传递)紧密程度的一种度量。

  • 紧耦合:模块之间联系越紧密,耦合性就越强,模块的独立性则越差。
  • 松耦合:模块之间联系越松散,单个模块解决问题的目的越明确,模块的独立性越强。

a. 非直接耦合(Nondirect Coupling)

4

如果两个模块之间没有直接关系,它们之间的联系完全是通过主模块控制调用来实现的,这就是非直接耦合,这种耦合的模块独立性最强。

class User {
    long userId;
    String userNick;
}

class MessageService {
    void pushMessage(long userId, String message);
}

class UserLoginService {
    void onLoginEvent(long userId) {
            User user = queryUserById(userId);
            String message = user.getUserNick() + "登录成功。";
            messageService.pushMessage(userId, message);
        }
    }
}

b. 数据耦合(Data Coupling)

5

如果一个模块访问另一个模块时,彼此之间是通过数据参数(不是控制参数、公共数据结构或外部变量)来交换输入、输出信息的,则称这种耦合为数据耦合,它是较好的耦合形式。

class MessageService {
    void pushMessage(long userId, String userNick) {
        String message = userNick + "登录成功。";
        doPushMessage(userId, message);
    }
}

class UserLoginService {
    void onLoginEvent(User user) {
        messageService.pushMessage(user.getUserId(), user.getUserNick());
    }
}

C. 印记(引用)耦合(Stamp Coupling)

6

当模块之间使用复合数据结构进行通信时,就会发生印记耦合。

复合数据结构可以是数组、类、结构体、联合体等的引用,通过复合数据结构在模块之间传递的参数,可能会或不会被接收模块完全使用。

class User {
    long userId;
    String userNick;
    // 该属性未被MessageService使用
    int level;
}

class MessageService {
    void pushMessage(User user) {
        String message = user.getUserNick() + "登录成功。";
        doPushMessage(user.getUserId(), message);
    }
}

class UserLoginService {
    void onLoginEvent(User user) {
        messageService.pushMessage(user);
    }
}
  • **印记耦合优点:**把模块A的引用一把传递给模块B,模块B只需要接受少量参数,接口说明简单。
  • 印记耦合缺点:
  • 不必要的参数:模块B可能只使用了模块A中部分的数据。
    • 模块B捆绑了模块A:任何需要用到模块B的地方,都需要先获取到模块A,无法脱离模块A单独使用。
    • 修改可能互相影响:修改模块A或模块B,可能导致对方也需要跟着修改,不符合开闭原则。
  • **印记耦合优化:**增加入参数类型,进传入模块需要的必要数据,如下:

7

d. 控制耦合(Control Coupling)

8

如果一个模块通过传送开关、标志等控制信息,明显地控制选择另一模块的功能,就是控制耦合。

class MessageService {
    void pushMessage(long userId, bool isNewUser) {
        if(isNewUser) {
            doPushMessage(userId, "登录成功。");
        }
    }
}

class UserLoginService {
    void onLoginEvent(User user) {
        messageService.pushMessage(user.getUserId, user.getIsNewUser());
    }
}
  • **数据耦合和控制耦合的主要区别:**在数据耦合中,模块之间的依赖关系非常小,而在控制耦合中,模块之间的依赖关系很高。在数据耦合中,模块之间通过传递数据进行通信,而在控制耦合中,模块之间通过传递模块的控制信息进行通信。
  • **控制耦合优化:**把控制的逻辑放在模块A之中,或增加模块C封装控制逻辑,不然模块B只做某一件独立的事情。

e. 外部耦合(External Coupling)

9

外部耦合,是指多个模块同时依赖同一个外部因素(IO设备/文件/协议/DB等),如上图所示:

外部耦合与与外部设备的通信有关,而不是与公共数据或数据流有关。

一个模块对外部数据或通信协议所做的任何更改都会影响其他模块,可以通过增加中间模块隔离外部变化来降低耦合度,如下:

10

f. 共用耦合(Common Coupling)

11

共用耦合是指不同的模块共享全局数据的信息(全局数据结构、共享的通信区、内存的公共覆盖区)

public Response loadInitInfo(Request request) {
    // request&response是Commands的全局数据
    Response response = new Response();
    commandExecutor.serial(request, response,
        orderRenderRateLimitCommand,
        renderInitResponseCommand,
        renderEnrichTradeNoCommand,
        renderEnrichItemCommand,
        renderEnrichCombinationCommand,
        renderEnrichPriceCommand
	);
    return response;
}

共用耦合的问题:

  1. 较难控制各个模块对公共数据的存取,容易影响模块的可靠性和适应性。
  2. 使软件的可维护性变差,若一个模块修改了共用数据,则会影响相关模块。
  3. 降低了软件的可理解性,不容易清楚知道哪些数据被哪些模块所共享,排错困难。

g. 内容耦合(Content Coupling)

内容耦合在低级语言(汇编)中出现,高级语言从设计上已避免出现内容耦合。

如果发生下列情形,两个模块之间就发生了内容耦合:

  1. 一个模块直接访问另一个模块的内部数据。
  2. 一个模块不通过正常入口而直接转入到另一个模块的内部。
  3. 两个模块有一部分代码重叠(该部分代码具有一定的独立功能)。
  4. 一个模块有多个入口。

2、内聚的类型

12

内聚,是描述一个模块内各元素彼此结合的紧密程度,是从功能角度来度量模块内的联系。

  • **低内聚:**模块内的元素的职责相关性低,通常也意味着模块与外部是紧耦合的。
  • **高内聚:**模块内的元素的职责相关性强,通常也意味着模块与外部是松耦合的。

通常,解决了耦合的问题,就解决了内聚的问题,反之亦然。

a. 偶然性内聚

偶然内聚,一个模块内的各元素之间没有任何联系,仅是恰好放在同一个模块内,业务的“Util/Helper”类有大量例子。

  • **问题的原因:**通常是模块名起的过于抽象,导致不同职责的元素都可以放进去,从而引起了低内聚。
  • **问题的解法:**将抽象的模块拆解成多个更小的具体模块,例如RetailTradeHelper可以拆为OrderAmountHelper/OrderPaymentParamHelper。

image

b. 逻辑性内聚

逻辑内聚,把几种相关的功能组合在一起,由调用方传入的参数来确定具体执行哪一种功能。

逻辑内聚是一种“低内聚”,某程度上对应了“控制耦合”,它把内部的逻辑处理暴露给了接口之外,当内部逻辑发生变更时,原本无辜的调用方也会受牵连改动。

public void syncOrder(Order order, String dist) {
	if(dist == "oc") {
        syncOrder2Oc(order);
    }
    if(dist == "mis") {
        syncOrder2Mis(order);
    }
    if(dist == "tp") {
    	syncOrder2Tp(order);
    }
}

c. 时间性内聚

时间内聚,指一个模块内的组件除了在同一时间都会被执行外,相互之间没有任何关联。

13

d. 过程性内聚

过程内聚,指一个模块内的组件以特定次序被执行,但相互之间没有数据传递。

e. 通信性内聚

通信内聚,指一个模块内的组件以特定次序被执行,且相互之间传递和操作相同的数据。

14

f. 顺序性内聚

顺序内聚,指一个模块内的元素以特定次序被执行,且上一步的输出被下一元素所依赖。

15

g. 功能性内聚

功能内聚,指一个模块内所有组件属于一个整体,完成同一个不可切分的功能,彼此缺一不可。

四、设计原则

设计原则,是指导我们如何设计出低耦合、高内聚的代码,让代码能够更好的应对变化,从而降本提效。

设计原则的关键,是从使用方的角度看提供方的设计,一句话概括就是:请不要要我知道太多,你可以改,但请不要影响我。

1、单一职责原则(SRP)

定义:一个函数/类只能因为一个理由被修改。

单一职责原则,是所有原则中看起来最容易理解的,但是真正做到并不简单。因为遵循这一原则最关键是职责的划分

职责的划分至少要回答两个基本问题:

  • 什么是你,什么是我?
  • 什么事情归你管,什么事情归我管?

且不说写代码,工作中我们也会出现人人不管或相争的重叠地带,划分清楚职责看起容易,实际很难。

扩展:遵循意图导向编码:先给方法分配职责,再考虑实现,每个方法自然符合SRP的

"意图"导向编程主张代码实现思维方式一致:

  • 从上而下组织代码,假设每个步骤都有一个理想的方法来完成,而不关注每个步骤的具体实现。 假设理想方法存在,关心每个方法的入参,返回值以及命名(什么样的名字最符合它的含义,通常选择具有业务含义的名称),而不是实现。
  • 只有在需要实现该方法时再考虑实现方式,(代码实现符合SRP:先分配职责,再考虑实现)

2、开闭原则(OCP)

定义:对扩展开放,对修改关闭(不修改代码就可以增加新功能)。

要理解开闭原则,关键是要理解定义中隐含着的两个主语,“使用方”和“提供方”,即:

提供方可以修改,增加新的功能特性,但是使用方不需要被修改,即可享用新的功能特征。

开闭原则广泛的理解,可以指导类、模块、系统的设计,满足该原则的核心设计方法是:通过协议(接口)交互。

3、里氏替换原则(LSP)

定义:所有引用父类的地方,必须能透明的使用它的子类对象,指导类继承的设计。

面向对象的继承特性,一方面,子类可以拥有父类的属性和方法,提高了代码的复用性;另一方面,继承是有入侵性的,父类对子类有约束,子类必须拥有父类全部的属性和方法,修改父类会影响子类,增加了耦合性。

里氏替换原则是对继承进行了约束,体现在以下方面:

  • 子类可以实现父类的抽象方法,但不能重写(覆盖)父类的非抽象方法。
  • 子类可以增加父类所没有的属性和方法。
  • 子类重写父类方法时,输入参数类型要和父类的一致,或更宽松(参数类型的父类)。
  • 子类重写父类方法时,返回值类型要和父类的一致,或更严谨(返回类型的子类)。

4、依赖倒置原则(DIP)

定义:高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象,目的是降低层与层之间的耦合。

从倒置来看,该原则可以有更泛化的理解:

  • **依赖实体的倒置:**高层不依赖底层模块,抽象不依赖细节,例如模块分层规范中的domain不依赖infrastructure的实现。
  • **依赖控制的倒置:**依赖具体对象的创建控制,从程序内部交给外部,例如Spring的Ioc容器。

举个购物车的例子:

16

  • **商业能力基座:**主要包含购物车的业务流程实现、外域服务定义(非实现)、商业定制能力(扩展点),打包后需满足一套代码多处部署的要求。
  • **域服务能力实例:**针对不同运行环境,提供适配环境的域服务实现,商业基座反向依赖域服务实例,使得基座与环境无关。

5、接口隔离原则(ISP)

定义:客户端不应该被强迫去依赖它并不需要的接口。

理解接口隔离原则,需要拿单一职责的原则做对比。细品一下,如果一个接口满足了单一职责,是否就也就满足接口隔离原则?

  • 单一职责原则,解决了接口内聚的问题。
  • 接口隔离原则,认为某些场景下需要存在非内聚接口(多职责),但是又不希望客户端知道整个类,客户端只要知道具有内聚接口的抽象父类即可。

简单来讲,接口隔离原则解决的问题是,当某些类不满足职责单一原则时,客户端不应该直接使用它们,而是通过增加接口类,通过它隐藏客户端不需要感知到的部分。

五、编程范式

1、编程范式:

编程范式,本质是一种思维方式,而和具体语言没关系。

用C语言可以写出面向对象的程序,用Java语言可以写出面向过程的程序。而不争的现实是,我们大部分人是在用java写面向过程的代码。

例如下面代码,它是如何用面向过程语言实现封装、继承、多态的?

17

备注:以上代码来自开源libevent库

a. 结构化(面向过程)编程

最早使用机器和汇编语言编程,是编排好一堆命令让机器逐条执行,为了控制一些跳跃的流程(如if/for/continue/break),就会用到类似goto的语句,让程序直接跳转到希望执行的指令位置,这样程序员就拥有了直接转移程序控制权的能力。goto的无条件转移,使得程序的控制流难于追踪,程序难以修改和维护。

18

后来大家总结出了一套流程结构化的定律:任何程序都可以用顺序、选择、循环三种基本控制结构来表示。

  • 顺序:代码是至上往下顺序执行的。
  • 选择:if-else/switch选择执行。
  • 循环:for/while控制循环执行。

因此,结构化编程的本质,是对程序控制权的直接转移进行了规范和限制。

b. 面向对象编程

结构化编程思维,比较靠近机器运行的思维,当程序越来越复杂的时候,大家发现简单靠结构化思维编程,很难构建起一个庞大的应用。而在编码过程中,大家不知不觉的把一些数据和逻辑封装了起来,形成一个个可复用的组件。慢慢大家总结出了一套符合人类理解客观世界的编程范式:利用事物的信息建模概念,如实体、关系、属性等,同时运用封装、继承、多态等机制来构造模拟现实系统的方法。

  • 封装:核心是对实体建模,把客观世界的属性和行为,封装成类的数据和方法,同时通过控制访问权限(private/protect/public),对外隐藏细节。
  • 继承:在封装的基础上,可以定义子类,从而使子类获得父类的属性和行为,这也符合人类从抽象到具象的认知思维。
  • 多态:继承使得子类获得父类的行为,总有儿子不听爸爸话的时候,当子类重写了父类行为时,多态使得父类引用执行方法时,实际执行的是子类的行为。

封装、继承、多态是面向对象的三大特征,三者的关系是层层递进的,而多态实际是规范了程序控制权的间接转移,在面向对象编程之前,大家是通过函数指针来解耦不同组件的函数实现,这种方式需要工程师严格遵守约定初始化函数指针,是非常脆弱的。

因此,面向对象编程的本质,是规范了数据和行为的封装,同时限制了程序控制权的间接转移。

c. 函数式编程

**函数式思维,是一种数学思维,把一个问题分解为一系列函数。**函数式编程有多种定义,但是从根本上来看,它的核心是“纯函数”和“引用透明”:

  • 纯函数:无副作用,同样的输入永远得到同样的输出。
  • 引用透明:任意函数直接用它的计算结果替代,而不影响任何调用它的程序。

若要做到以上两点,就需要对赋值进行限制,即变量一旦初始化就不可以再修改。

因此,函数式编程的本质,是规范了函数(一等公民/高阶函数/声明式/闭包等),同时限制了赋值行为。

d. 编程范式总结

编程范式的本质,更多是告诉我们不能做什么,并且通过规范来约束我们的行为。

  • 结构化编程:限制对程序的控制权做直接转移,请按照控制结构规范来。
  • 面向对象编程:限制对程序控制权的间接转移,请按照封装、继承、多态的规范来。
  • 函数式编程:限制了赋值行为,请按照函数的规范来。

灵魂拷问一下:

  • 为什么面向对象编程大受推崇?
  • 为什么多数人在用面向对象语言写面向过程代码?
  • 为什么函数式编程束之高阁,很难产业化大规模使用?

我当前表浅的理解是:

  • 面向过程符合人类的直线直觉思维,不需要太多深度思考,可以快语直言。
  • 面向对象需要充分了解客观主体信息,才能从中思考和提炼出要素(实体)、关系(方法)和目标(职责),要求有系统性的抽象思维
  • 函数式编程基于数学,缺少合适的抽象机制,纯函数式编程很难满足企业级应用要求的严格和复杂的业务需求。

三种编程范式没有好坏之分,核心是思维方式的区别,针对不同的问题和场景,如何选择适当的方式来思考和解决问题,才是我们理解它们的关键。

2、应用范式:

a. 表模式

19

**表模式关注的数据库的表,它先考虑数据库表需要管理,然后添加对数据增删改查的操作。**封装是面向对象的关键特征之一,把数据和操作数据的行为绑定在一起,拥有一个标识符(类)来表示它两的集合,而表模式允许你把数据和行为放在一起,但是它没有一个标识符来标出它所代表的主体。

这种模式在PC时代很盛行,例如VB和.net等桌面应用开发框架上,但是活久见,在JAVA服务应用中也被我发现了,如下:

20

b. 事务脚本模式

脚本,是指表演戏剧、拍摄电影等所依据的底本又或者书稿的底本。脚本可以说是故事的发展大纲,用以确定故事的发展方向。

事务脚本模式,关注点是事务的流程和步骤,是对事务流程和步骤的编排,是一种面向过程的组织和表达形式。

按照事务脚本模式编程,可以不需要任何面向对象的设计,其中任何逻辑都可以通过if/else/while等流程控制元素来表达

事务脚本模式的优点是,门槛低容易上手,也符合人的直线直觉思维;它的缺点是,当业务逻辑复杂是,事务方法会快速膨胀,因为业务属性不明确和缺乏抽象,不好复用和扩展。该模式在服务端应用中很常见,从MVC时代开始,一般通过controller组织事务流程,常见的分层结构如下:

image2

c. 领域设计模式

领域设计模式,是通过分析和发掘业务领域的概念,从中提炼和设计出具有数据和行为的对象模型(类),并建立模型之间的关系。

领域设计模式,需要建立一个完整的由对象模型组成的层,来对目标业务领域建模。业务是经常变化的,通常有会通过分层的模式,让领域模型和系统其他部分保持最小的依赖。

至此,你会发现领域设计是DDD的底层思想,是面向对象的实践,更多请查阅“对象建模”和“领域驱动设计(DDD)”相关的材料和数据,这里不做展开。

d. 应用范式总结

不同的应用范式,是随着软件复杂度逐步提升演进出来的,不同模式面对和解决不同复杂度的问题,相互之间没有好坏之分。当问题比较简单时,使用事务脚本模式足够应付,反倒使用领域设计就过度设计,增加了不必要的复杂度,适得其反。

六、代码的道与术

任何一个学科的学习,都要从基本概念、基本原理、基本方法入手,才能把握住问题的实质。

**所谓,招式套路可以千变万化,扎实深厚的内功却始终如一。**内功是基础和本源的东西,例如耦合和内聚,我们都知道低耦合高内聚好,但如何衡量代码的耦合和内聚?再如编程范式,我们都在使用面向对象语言,为什么看到的大多数是面向过程的代码?究其根本,是我们容易忽视基础和本源的东西,比如更关注设计模式,更关注架构设计,但上层的设计理念大多数是来自基础和本源的思想指引。

套用道家的一句话:道以明向,法以立本,术以立策,器以成事。

从代码的角度来看:

  • 道:是好代码,无以名状,无以表述,只能假想形式表达,仁者见仁智者见智。
  • 法:是编程范式,是规章制度,是规范约束,使其在道的合理范围之内,保住基线。
  • 术:是设计原则,执行层面的方法论。
  • 器:是spring/mos-boot,有形的工具,保障的是执行和效率。

从代码的角度来看它们的关系:

  • 以法固道:理念需要通过法制来规范和约束,才能得到贯彻、落实和巩固。
  • 以器载道:善于创造和使用工具,用工具承载理念,可以事半功倍。
  • 以道驭术:基本理念和具体操作要统一,即知行合一。

关于如何写好代码,描述如有不当之处,请大家帮忙指正。

最后,一句话与大家共勉:万丈高楼平地起,勿在浮沙筑高台。

七、参考材料

参考书籍:

  • 《重构》
  • 《代码整洁之道》
  • 《架构整洁之道》
  • 《易读代码的艺术》
  • 《代码精进之路》
  • 《编程的逻辑-如何用面向对象方法实现复杂业务需求》
  • 《企业应用架构模式》
  • 《麦肯锡教我的协作武器》equential-cohesion)
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值