编写低耦合高内聚的业务

分享会-编写低耦合高内聚的业务

代码不分层引发的问题

  • 代码完全没有复用性,只能为某个特定业务做硬实现
  • 可扩展,可维护性极差,改一个bug引发多个bug
  • 经过需求不断迭代,代码健壮性越来越脆弱,最终变为祖传代码

MVCS模式中各个逻辑层的职责以及协作方式

  • 说明
    • C:控制器层,负责接收参数,验证参数,调用service,响应结果。自身不参与任何业务逻辑处理
    • S:服务层,接收控制器参数,处理业务逻辑,调用Model处理数据,把处理结果返回控制器
      • Service是按照业务模块划分的,并非一个控制器对应一个Service,如:UserService
    • M:模型层,被动接收服务层参数作为条件,编写sql语句,对数据库操作,结果返回服务层
      • 一张表就对应一个模型类
      • model只负责定义表结构,表与表之间的逻辑关系,以及getter,setter方法
  • 协作方式
    • 控制器注入一个或多个Service对象。Service中注入一个或多个Model对象。
    • 注入方式:在构造中创建对象,并存储在类的属性上,便于统一管理,不要每次使用时都new了
// controller中注入service
public function construct() {
 $this-userService = new UserService();
}
 
// service中注入model
public function construct() {
 $this-userModel = new User();
 $this->articleModel = new Article();
}

拿到需求后的开发前准备工作

  • 拆分需求,把一个功能按单一职责,划分成若干个动作(方法)
  • 考虑servie和model的复用,能复用的就不要重写一遍,也不要复制粘贴
  • 单独编写请求参数校验和最终响应参数返回(修改最多的地方)
// 规划拆分(开放平台短信发送)
初始化 - 请求参数合法性验证 - 验证用户是否有可用条数 - 验证短信模板是否审核通过 - 调用短信发送接口 - 响应数据格式化返回

// 参数验证
class BaseSMSVerify implements IBaseVerify {
	public function rules() {
		return [
			'mobile' => [
				['required', '手机号码必须填写'],
				['phone', '手机格式不合法']
			],
			'template_id' => [
				['required', '模板ID必须填写'],
				['integer', '模板ID必须是整数']
			],
			'sms_sign' => [
				['required', '短信签名必须填写'],
				['length_between:2,8', '短信签名必须在2~8个字符范围内']
			],
			'content' => [
				['required', '短信内容必须填写']
			]
		];
	}
}

// 返回响应
public function smsSendResponse() {
  [
	  'msg_id' => $messageId, 
	  'success' => $success,
	  'count'   => $count,
  ] = $this->response
  
	return [
		'message_id' => $messageId,
		'is_success' => $success ? '1' : '0',
		'sms_count'  => $count
	]
}

如何编写低耦合高扩展性的业务代码

表驱动法的使用
  • 通过配置数据映射关系,来优化代码结构,减少大量的if、switch使用
  • 优点:方便业务扩展
// 根据业务编号,来执行不同的业务处理方法
public const CONVERT_METHOD = [
	'4001' => 'registerUser',
	'4002' => 'updateOrder',
];

function doMethodByBussinessType() {
	['bussiness_type' => $bussinessType] = $this->request;
	$method = static::CONVERT_METHOD[$bussinessType];
	
	if (false === method_exists($this, $method)) {
		throw new Exception(KBMsg::REASON['INNER'], KBCOde::CODE['INNER']);
	}
	
	// 动态执行
	$this->response = $this->{$method}();
	
	return $this;
}
插件式法的使用
  • 原则一:所有方法都是可插拔式的,方法和方法之间没有耦合,可以根据业务随时调整
  • 原则二:每个方法都是业务流程中的一个执行步骤,为得到最终结果服务。
// 控制器接口
public function uploadImg() {
  // 接收参数
	$data = $this->input();
	
	try {
		$this->ocrService
			->init($data) // 初始化数据,把参数放到属性上,方便使用
			->setCache()  // 建立缓存
			->checkMaxErrorCount()  // 验证最大上传错误尝试次数
			->checkMaxUploadCount() // 验证最大上传数量限制
			->isRepeatUpload() // 上传的本批次图片序列号是否重复
			->verifyUploadImgData(new OCRUploadRequest()) // 验证请求参数
			->parseBase64ImgSource() // 解析上传的base64图片
			->storeUploadImgPath()   // 把解析成功的图片保存到upload目录
			->getWaybillMatchPhone() // 通过运单号到历史记录中去匹配手机号
			->storeUploadIMgToDb()   // 保存上传数据到数据库
			->sendToOcrQueue()       // 把数据发送到OCR识别队列
			->responseUploadData();  // 最终响应数据
	} catch (Exception $e) {
	  // 记录异常错误
		$result = $this->setUploadError($e, __METHOD__)
			->responseUploadData();
	}
	// 统一响应
	raise($this->responseCode, $this->responseMessage, $result);
}
异常的使用
  • 建议:异常统一由service中抛出,在控制器中捕获。model只返回数据操作结果,不处理业务。
  • 在业务开发中异常的处理分类
    • 不可恢复性异常:必要参数缺少,参数类型不符合预期,数据库操作失败等
    • 可尝试恢复性异常:第三方接口超时、Socket链接瞬断等,可以重复尝试的异常
    • 二次封装异常:内部异常信息和异常码,转换成对外提供的异常信息和异常码
  • 如何在异常中,标记异常的特征
    • 定义一套异常码,如:1xxx都是不可恢复异常,2xxx可恢复异常,3xxx...
// 可恢复性异常
try {
		// do somthing...
	} catch (Exception $e) {
		 if (2001 === $e->getCode()) {
		   // 如果重置链接失败,则抛出不可恢复异常
			 this.chatService->resetConnect()
		 }
	}


// 异常的二次封装
try {
		// do somthing...
	} catch (Exception $e) {
		 if (422 === $e->getCode()) {
		   // 抛出统一错误
			 throw new Exception('发件人姓名不能为空', 1002)
		 }
	}
业务的抽象化
  • 理解面向接口编程的好处
    • 把业务抽象化,提供一套规范,与具体实现分离。
    • 对扩展开放,对修改关闭,为多态提供条件。(开闭原则)
  • 理解多态
    • 概念
      • 子类继承父类(或实现接口)后,拥有了父类特征。
      • 使用这些特征时,父类可以替换子类,反之则不行(猫是动物的一种,但动物不是猫的一种)。
    • php由于没有类型的概念,可以使用instance来模拟多态。
  • 案例:抽象下单动作,利用多态,把订单下到不同快递公司
// 大客户下单接口
interface ICustomerOrder {
	public function customerOrder();
}

// 申通
class STO implements ICustomerOrder {
	public function courierOrder() {
		// do somthing...
	}
}

// 圆通
class YTO implements ICustomerOrder {
	public function courierOrder() {
		// do somthing...
	}
}

// Service层中多态实现
public function placeOrder() {
	['brand' => $brand] = $this->request;
	$className = strtoupper(brand);
	// 动态创建快递公司对象
	$instance = new $className();
	
	if (!$instance instanceof ICustomerOrder) {
		throw new Exception('服务器内部错误,请联系管理员', 2001)
	}
	
	// 多态调用,不关心谁在调用,只关心是否是某个接口的实现类
	$this->response = $instance->courierOrder();
	
	return $this;
}

开发小贴士

  • if语句尽量保持单层级,结构越简单越好,有时候逻辑取个反,就可以避免嵌套。
  • 一个方法不要超过20行,功能越单一越好。确保方法只干一件事
  • 类的构造方法尽量不要传递参数,这样可以增强类的复用性。特别是Service的构造方法
    • 因为并非所有调用Service的地方,都必须要初始化参数,有时只是使用Service中的某个功能。
    • 代替方案:定义一个init方法来初始化必要参数
  • 全部使用 === 来代替 ==,使代码更健壮。并且把常量放在左面
    • 开发时,让错误暴露出来,方便排查问题,而不应该兼容错误。
    • 推荐使用strcmp进行2个字符串比较,比较时,它会自动把其他类型转换为字符串类型
    • 避免 if ($a = 10) 这种运行时能够通过的错误。
  • 类属性使用私有或保护类型修饰,不推荐属性使用public
最后修改:2019-05-27 11:07:37
0