可扩展性是系统可维护性的重要指标之一,是软件工程系统的设计准则,是软件设计时一项必须考虑的目标,在程序设计时要求考虑未来可能的需求增长和变动情况。核心设计要素是提供改变 (通常是增强功能)能力,同时最大限度地减少对现有系统功能的影响.在软件设计和开发领域程序设计的六大原则中有一条开闭原则(OCP,OPEN-CLOSED PRINCIPLE):一个软件系统对功能扩展是开放的,对代码是封闭的。这意味着面对软件变更,软件系统要能支持功能扩展,满足新需求;而在实现新需求的同时,要尽量避免对已存在代码的修改,以减少对现有系统功能的影响。
据开闭的程度,在程序设计可扩展设计可分为三种模式:白盒(程序)扩展,灰盒(程序)扩展,和黑盒(程序)扩展。
开盒 | 玻璃盒 | 灰盒 | 黑盒 | |
---|---|---|---|---|
源码可见 | 是 | 是 | 否 | 否 |
源码可改 | 是 | 否 | 否 | 否 |
可扩展 | 是 | 是 | 是 | 否 |
白盒扩展
也称白箱扩展。在白盒扩展模式下,软件系统可以通过修改源代码进行扩展。这是最开放、最灵活、约束最少的模式。它有两个子形式:开盒扩展、玻璃盒扩展。
开盒扩展
在开盒(Open-Box)的系统中,我们进行扩展是具有侵入性的,源代码可以被直接改写。开盒扩展应仅限于BUG修复、内部代码重构、或者软件产品的下一个版本研发。
在这种扩展模式中,由于源代码可以直接修改,最容易出现编码过程中的各种问题。对于通过直接修改代码进行扩展的程序,应做好以下几点:
- 编写并参照代码规范要求,按照规范在程序中有足够并且正确的注释,必要时可借助外部文档等方式进行说明
- 有良好的程序编写规范,统一程序编写风格,利于他人快速理解并进行正确的维护和再开发
- 按照单一职责原则,一个接口或类者只负责一项职责,一个方法尽可能的只做一件事,提升其被复用的可能性,减少重复开发
- 制定程序组织约定,按照技术公共类库、业务公共类库、接口类库、业务实现类库等方式进行有效的程序组织,同时可借助一些外部工具形成程序清单的鸟撖图,便于快速搜索和复用。
以动物呼吸场景为例
class Animal{
public void breathe(String animal){
System.out.println(animal+"呼吸空气");
}
}
public class Client{
public static void main(String[] args){
Animal animal = new Animal();
animal.breathe("牛");
animal.breathe("羊");
animal.breathe("猪");
}
}
上面的程序在客户端加入新的动物类型就就出现问题,例如鱼呼吸的是水。一般最直接的扩展思维一般会按照如下处理
class Animal{
public void breathe(String animal){
if("鱼".equals(animal)){
System.out.println(animal+"呼吸水");
}else{
System.out.println(animal+"呼吸空气");
}
}
}
public class Client{
public static void main(String[] args){
Animal animal = new Animal();
animal.breathe("牛");
animal.breathe("羊");
animal.breathe("猪");
animal.breathe("鱼");
}
}
这种扩展思路虽然修改的花销小,但是所带来的测试花销却不小,而且违背了单一职责的原则,如果后续有新的动物呼吸场景加入,又需要修改并且全部呼吸情况重新测试,因此总体成本实际上并不小。下面这种修改方式遵从单一职责原则,花销也不大,而且后续维护只需要加入新的方法支持新场景,对原有的功能都没有改变,是相对可取的一种扩展思路。
class Animal{
public void breatheAir(String animal){
System.out.println(animal+"呼吸空气");
}
public void breatheWater(String animal){
System.out.println(animal+"呼吸水");
}
}
public class Client{
public static void main(String[] args){
Animal animal = new Animal();
animal.breatheAir("牛");
animal.breatheAir("羊");
animal.breatheAir("猪");
animal.breatheWater("鱼");
}
}
玻璃盒扩展
玻璃盒(Glass-Box)扩展(也称为架构驱动框架)允许我们使用可用的源代码扩展软件系统,但这些源代码可能不允许被修改的。扩展实现必须与原系统内容分离,以保证原始系统不受影响。这种可扩展性的一个例子是面向对象的应用程序框架,通常通过使用继承和动态绑定来实现可扩展性。
玻璃盒扩展就是里氏替换原则的一个实际体现。里氏替换原则的定义如下:如果对每一个类型为 T1的对象 o1,都有类型为 T2 的对象o2,使得以 T1定义的所有程序 P 在所有的对象 o1 都代换成 o2 时,程序 P 的行为没有发生变化,那么类型 T2 是类型 T1 的子类型。字面意思就是所有引用基类的地方必须能透明地使用其子类的对象。具体到程序设计则是定义方法/对象时使用抽象类,实际运行时使用具体的子类,通过运行时子类替换基类,达到不修改原程序也能支持新功能的目的。里氏替换原则通俗的来讲就是:子类可以扩展父类的功能,但不能改变父类原有的功能。它包含以下4层含义:
- 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。
- 子类中可以增加自己特有的方法。
- 当子类的方法重载父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。
- 当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。
玻璃盒扩展的方式给程序设计带来巨大便利的同时,也带来了弊端。基于基类产生创建新的子类时,增加了对象间的耦合性,程序的可移植性降低,对原系统有一定的代码入侵。如果一个类被其他的类所继承,则当这个类需要修改时,必须考虑到所有的子类,并且父类修改后,所有涉及到子类的功能都有可能会产生故障。
以下面电影租赁计价程序为例
/**
* 电影信息类
*
*/
public class Movie {
public static final int REGULAR = 0;
public static final int NEW_RELEASE = 1;
private String title;
private int priceCode;
public Movie(String title, int priceCode) {
this.title = title;
this.priceCode = priceCode;
}
public String getTitle() {
return this.title;
}
public int getPriceCode() {
return priceCode;
}
public void setPriceCode(int ipriceCode) {
this.priceCode = ipriceCode;
}
}
/**
* 租赁信息类
*
*/
public class Rental {
private Movie movie;
private int daysRented;
public Rental(Movie movie, int daysRented) {
this.movie = movie;
this.daysRented = daysRented;
}
public Movie getMovie() {
return this.movie;
}
public int getDaysRented() {
return this.daysRented;
}
}
/**
* 客户信息类
*
*/
public class Customer {
private String name;
private List<Rental> rentals = new ArrayList<Rental>();
public Customer(String name) {
this.name = name;
}
public void addRental(Rental rental) {
this.rentals.add(rental);
}
public String getName() {
return this.name;
}
/**
* 当前客户情况报告,包括已租借影片名称及应付款明细,汇总应付款明细和积分。
*
* @return
*/
public String statement() {
double totalAmount = 0;
int rentalpoints = 0;
StringBuilder sb = new StringBuilder();
for (int i = 0; i < rentals.size(); i++) {
double thisAmount = 0;
Rental rental = rentals.get(i);
// 计算应付款。常规影片单价为2元/日;新片单价为3元/日
switch (rental.getMovie().getPriceCode()) {
case Movie.REGULAR:
thisAmount += 2;
break;
case Movie.NEW_RELEASE:
thisAmount += rental.getDaysRented() * 3;
break;
}
// 计算积分。一篇影片积分为1。如果是新片且租借日期大于1天,则积分再加1
rentalpoints++;
if (rental.getMovie().getPriceCode() == Movie.NEW_RELEASE && rental.getDaysRented() > 1) {
rentalpoints++;
}
// 记录影片租借情况
sb.append(rental.getMovie().getTitle()).append(":").append(thisAmount).append("\n");
totalAmount += thisAmount;
}
sb.append("总计应付款:").append(totalAmount).append("\n").append("总计积分:").append(rentalpoints);
return sb.toString();
}
}
快速随性的设计一个简单的小程序来满足初步需求没有问题,但上面的程序往往是复杂系统中的典型代表。如果需求发生修改,上面的程序会出现上面样的变化呢?例如用户想要一个另外组织形式的租赁情况描述,例如HTML格式的,那么statement方法中的内容就要重新复制一份到新建htmlstatement方法中;如果进一步,如果租赁费用的计算发生了修改,又要同时调整statement和htmlstatement方法;再进一步,随着市场细化,影片分类逐步增加,每类费用的租赁费用和积分计算都有各自的特色,那么又要再改一通。
通过上面的分析,会发现简单的小程序并不简单。简单的只是现在的要求,不简单的是如何通过快速、安全的在已有的功能上扩展。
让我们对这个程序进行下调整。首先分析下业务发展的方向,基本可以确定最大的变化因素是影片种类,以及价格、积分的计算调整;而影片种类的内部变化因素就是也是价格。因此价格和积分的计算逻辑是最可能发生变化的内容。将这部分内容进行抽取作为一个基类,根据不同的影片类型形成各自的价格和积分计算逻辑的子类,是比较优的一种可支持扩展的方案。一方面,对某类影片的价格调整不会影响其他影片类型的逻辑,另一方面,新类型影片的价格则可以继承基类形成新的子类,通过多态特性轻松嵌入到原有的处理功能中,实现轻松扩展。
以下为按照灰盒扩展的思想调整后的程序。可以很明显的看出,调整后的程序结构落地面向对象设计思想,每个类和方法都很简单,具有针对影片类型变化的良好扩展能力。
/**
* 客户信息类
*
*/
public class Customer {
private String name;
private List<Rental> rentals = new ArrayList<Rental>();
public Customer(String name) {
this.name = name;
}
public void addRental(Rental rental) {
this.rentals.add(rental);
}
public String getName() {
return this.name;
}
/**
* 计算总计积分
* @return 所有影片的租赁积分
*/
private int getTotalRentalPoints() {
int points = 0;
for(int i=0;i<rentals.size();i++) {
points += rentals.get(i).getRentalPoints();
}
return points;
}
/**
* 计算总计应付费用
* @return 所有影片的应付费用
*/
private double getTotalCharge() {
double charge = 0.0d;
for(int i=0;i<rentals.size();i++) {
charge += rentals.get(i).getCharge();
}
return charge;
}
/**
* 当前客户情况报告,包括已租借影片名称及应付款明细,汇总应付款明细和积分。
* @return 当前客户情况描述
*/
public String statement() {
StringBuilder sb = new StringBuilder();
for(int i=0;i<rentals.size();i++) {
Rental rental = rentals.get(i);
//记录影片租借情况
sb.append(rental.getMovie().getTitle()).append(":").append(rental.getCharge()).append("\n");
}
sb.append("总计应付费用:").append(getTotalCharge()).append("\n");
sb.append("总计积分:").append(getTotalRentalPoints());
return sb.toString();
}
/**
* 以HTML形式输出的客户情况报告
* @return 以HTML形式输出的客户情况报告
*/
public String htmlStatement() {
StringBuilder sb = new StringBuilder();
sb.append("<H1>客户").append(getName()).append("</H1>").append("\n");
for(int i=0;i<rentals.size();i++) {
Rental rental = rentals.get(i);
//记录影片租借情况
sb.append(rental.getMovie().getTitle()).append(":").append(rental.getCharge()).append("<BR>\n");
}
sb.append("<P>总计应付费用:").append(getTotalCharge()).append("</P>\n");
sb.append("<P>总计积分:").append(getTotalRentalPoints()).append("</P>\n");
return sb.toString();
}
}
/**
* 租赁信息类
*
*/
public class Rental {
private Movie movie;
private int daysRented;
public Rental(Movie movie,int daysRented) {
this.movie = movie;
this.daysRented = daysRented;
}
public Movie getMovie() {
return this.movie;
}
public int getDaysRented() {
return this.daysRented;
}
/**
* 计算所租赁的电影的应付费用
* @return 应付费用
*/
public double getCharge() {
return this.movie.getCharge(daysRented);
}
/**
* 计算所租赁电影的积分
* @return 产生的积分
*/
public int getRentalPoints() {
return this.movie.getRentalPoints(daysRented);
}
}/**
* 电影信息类
*
*/
public class Movie {
public static final int REGULAR = 0;//常规影片价格类型
public static final int NEW_RELEASE = 1;//新片价格类型
private String title;
private int priceCode;
private Price price;
/**
*
* @param title 电影名称
* @param priceCode 价格类型
*/
public Movie(String title , int priceCode) {
this.title = title;
this.priceCode = priceCode;
setPriceCode(priceCode);
}
public String getTitle() {
return this.title;
}
public int getPriceCode() {
return priceCode;
}
/**
* 设置价格类型
* @param ipriceCode
*/
public void setPriceCode(int ipriceCode) {
switch(ipriceCode) {
case REGULAR:
this.price = new RegularPrice();
break;
case NEW_RELEASE:
this.price = new NewReleasePrice();
break;
default:
throw new IllegalArgumentException("Incorrent Price Code");
}
}
/**
* 计算应付费用
* @param daysRented 租赁天数
* @return 应付费用
*/
public double getCharge(int daysRented) {
return this.price.getCharge(daysRented);
}
/**
* 计算积分
* @param daysRented 租赁天数
* @return 积分
*/
public int getRentalPoints(int daysRented) {
return this.price.getRentalPoints(daysRented);
}
}
/**
* 价格类
* @author jamsry
*
*/
public abstract class Price {
/**
* 计算费用方法
* @param daysRented 租赁天数
* @return 按天数计算的应付费用
*/
abstract double getCharge(int daysRented);
/**
* 返回默认积分值
* @param daysRented 租赁天数
* @return 默认积分
*/
int getRentalPoints(int daysRented) {
return 1;
}
}
/**
* 新近影片的价格计算类
* @author jamsry
*
*/
public class NewReleasePrice extends Price {
@Override
double getCharge(int daysRented) {
// TODO Auto-generated method stub
return daysRented * 3;
}
/**
* 按照新片的规则计算积分
*/
@Override
int getRentalPoints(int daysRented) {
return (daysRented > 1) ? 2:1;//租赁日期大于1天积分增加1
}
}
/**
* 常规影片价格计算类
* @author jamsry
*
*/
public class RegularPrice extends Price {
@Override
double getCharge(int daysRented) {
// TODO Auto-generated method stub
return 2;
}
}
调整后的程序结构还可以进一步进行优化,例如按照单一职责原则,将费用与积分的计算分开、在Movie类的setPriceCode方法中采用工厂模式创建price对象、由于getTotalCharge和getTotalRentalPoints导致多次循环数组列表可能带来的性能影响等,但还是那句话,扩展是对现在与未来的一种平衡。抽象未来可能变化的对象,简单基本不变的内容,平衡扩展所带来的灵活性与实现成本,是考虑是否需要考虑扩展、如果设计扩展的大体思路。
黑盒扩展
在黑盒(Black-Box)扩展(也称为数据驱动框架)中,我们无法得到用于实施系统部署或扩展的详细信息,只能获取到接口说明文档等信息。黑盒扩展会比上述各种白盒扩展都要受限,无法看到源码也不能对源码进行修改。黑盒扩展通常的实现包括通过系统配置的应用程序,或者通过定义组件接口形成的服务于特定应用程序的脚本语言的使用。我们平常最常见的形式就是通过调用其它应用程序对外暴露的接口,实现功能的扩展。这也是当前服务化架构的主要扩展方式:通过调用各种接口组装形成新的功能。
黑盒扩展准遵从迪米特法则(LOD,又叫做最少知道原则),就是说,一个对象应当对其他对象有尽可能少的了解。一个设计好的模块可以将它所有的实现细节隐藏起来,彻底地将提供给外界的API和自己的实现分割开来。这样一来,模块与模块之间就可以仅仅通过彼此的API相互通信,而不理会模块内部的工作细节。模块内容做的各种调整,只要不涉及接口定义的改变,均对外部没有影响,是最理想的一种扩展方式。
关于接口定义应当遵从接口隔离原则(ISP),一个程序对另外一个程序的依赖应该建立在最小接口上。这里最小的接口指的是接口的定义是最小功能粒度的。每个接口应该是都是专用接口,而不是设计一个综合类的接口,支持各种场景,这样容易导致接口中某项定义发生变化就涉及所以调用该接口的功能进行排查调整,不论这些调用该接口的目的是使用其他定义。因此有效的划分接口定义对可扩展性也有影响。
在接口粒度定义方面,应该区分为与应用无关的细粒度接口和与应用相关的粗粒度接口。与应用无关的细粒度接口一般为基础类、通用类接口,与应用相关的粗粒度接口一般是面向特定业务场景的定制类接口。通过黑盒进行应用扩展,内部扩展应该通过重用细粒度接口,就像搭积木一样,组装出面向业务的粗粒度接口;在业务流程的扩展上则通过重用粗粒度接口,组成面向业务处理流程的服务,支持新业务新产品的快速上线。
黑盒扩展能最大化实现开闭原则的定义,通过提供各种接口,形成服务,进而形成服务市场,很方便的就能供其他功能使用或组成新的功能。但同时也存在一些缺点。一是是否需要使用接口的方式,很多情况程序的实现目的都是唯一性的,极少具有复用的可能性。对于这种程序使用接口方式进行实现,则会导致过度设计;二是随着接口的不断增多,如果没有有效的管理和使用,也容易导致接口的重复开发;三是接口说明文档的正确性。黑盒扩展对外最终的就是接口的说明文档,外部只能通过这份文档了解接口的定义,包括实现的功能,输入和输出参数。如果设计文档描述不正确或者不明确,就会导致用错接口。
灰盒扩展
灰盒(Gray-Box)扩展介于白盒和纯黑盒之间,不完全依赖于源代码的暴露。程序员可以得到系统的专用接口清单,清单列出了所有可用的抽象,包括扩展的规范和方法,以细化和说明如何开发扩展内容。
面向切面编程(AOP)让我们的开发更加方面,是灰盒扩展的一种形式。AOP主要实现的目的是针对业务处理过程中的切面进行提取,它所面对的是处理过程中的某个步骤或阶段,以获得逻辑过程中各部分之间低耦合性的隔离效果。AOP以模块化扩展的思维,不修改原来的程序代码,以处理过程中的某个步骤或阶段作为切入点,通过新增切入内容的说明定义,实现对原有处理逻辑的扩展。
举个例子。一个业务流程要经过会话检查,数据准备,业务处理,结果视图返回等步骤,一个步骤就是一个切面,通过多个切面组合的形成完整的业务处理流程。
<!--交易处理链的定义-->
<module id="defaultChain" class="com.system.core.channel.HandlerChain">
<!--所匹配的url请求地址-->
<property name="urlMapping">
<list>
<value>/servlet/submitBusniessServlet</value>
</list>
</property>
<!--处理步骤链-->
<propertye name="channelHandler" >
<list>
<ref module="sessionChecker" /><!--会话检查步骤-->
<ref module="prepareContext" /><!--数据准备步骤-->
<ref module="transEngine" /><!--交易引擎处理步骤-->
<ref module="replyView" /><!--返回视图步骤-->
</list>
</property>
</module>
<!--会话检查步骤的定义-->
<module id="sessionChecker" class="com.system.control.handlers.SessionCheckerHandler" >
<!--会话检查需要的参数:会话管理器对象-->
<property name="sessionManager">
<ref module="sessionManager"><!--会话管理器对象,可通过应用启动显示初始化或者控制反转等方式建立-->
</property>
</module>
<!--数据准备步骤的定义-->
<module id="prepareContext" class="com.system.control.handlers.ContextPrepareHandler" >
<!--数据准备需要的参数:会话管理器对象-->
<property name="sessionManager">
<ref module="sessionManager"><!--会话管理器对象,可通过应用启动显示初始化或者由框架控制反转等方式建立-->
</property>
<!--数据准备需要的参数:交易对象的管理器-->
<property name="transManager">
<ref module="transManager"><!--交易对象的管理器,可通过应用启动显示初始化或者由框架控制反转等方式建立-->
</property>
</module>
<!--交易引擎处理步骤的定义-->
<module id="transEngine" class="com.system.core.handlers.CoreEngineHandler" >
<!--交易引擎处理需要的参数:交易对象的管理器-->
<property name="transManager">
<ref module="transManager">
</property>
</module>
<!--返回视图步骤的定义-->
<module id="replyView" class="com.system.view.handlers.ReplyViewHandler" >
<!--返回视图步骤需要的参数:交易对象的管理器-->
<property name="transManager">
<ref module="transManager">
</property>
<!--返回视图步骤处理需要的参数:视图工厂-->
<property name="viewFactory">
<ref module="viewFactory"><!--视图工厂可通过应用启动显示初始化、由框架控制反转或者固定某几类视图定义等方式定义-->
</property>
</module>
假设现在要对系统的访问进行针对性的流量控制,增加一个流量控制的切面是最优的方法。调整后的步骤包括会话检查,流量控制,数据准备,业务处理,结果返回。对应在程序结构上则只要按照切面的开发规范,实现流量控制步骤的具体逻辑,然后组合到原有流程就能很轻松的实现目标。
<!--交易处理链的定义-->
<module id="defaultChain" class="com.system.core.channel.HandlerChain">
<!--所匹配的url请求地址-->
<property name="urlMapping">
<list>
<value>/servlet/submitBusniessServlet</value>
</list>
</property>
<!--处理步骤链-->
<propertye name="channelHandler" >
<list>
<ref module="sessionChecker" /><!--会话检查步骤-->
<ref module="parellelControl" /><!--限流控制-->
<ref module="prepareContext" /><!--数据准备步骤-->
<ref module="transEngine" /><!--交易引擎处理步骤-->
<ref module="replyView" /><!--返回视图步骤-->
</list>
</property>
</module>
<!--会话检查步骤的定义-->
<module id="sessionChecker" class="com.system.control.handlers.SessionCheckerHandler" >
<!--会话检查需要的参数:会话管理器对象-->
<property name="sessionManager">
<ref module="sessionManager"><!--会话管理器对象,可通过应用启动显示初始化或者控制反转等方式建立-->
</property>
</module>
<!--限流控制步骤的定义-->
<module id="parellelControl" class="com.system.control.handlers.ParellelControlHandler" >
<!--限流控制需要的参数:交易对象管理器-->
<property name="transManager">
<ref module="transManager"><!--会话管理器对象,可通过应用启动显示初始化或者控制反转等方式建立-->
</property>
<!--限流控制步骤需要参数:限流管理器-->
<property name="parellelManager">
<ref module="parellelManager"><!--限流管理器,可通过应用启动显示初始化或者控制反转等方式建立-->
</property>
</module>
<!--数据准备步骤的定义-->
<module id="prepareContext" class="com.system.control.handlers.ContextPrepareHandler" >
<!--数据准备需要的参数:会话管理器对象-->
<property name="sessionManager">
<ref module="sessionManager"><!--会话管理器对象,可通过应用启动显示初始化或者由框架控制反转等方式建立-->
</property>
<!--数据准备需要的参数:交易对象的管理器-->
<property name="transManager">
<ref module="transManager"><!--交易对象的管理器,可通过应用启动显示初始化或者由框架控制反转等方式建立-->
</property>
</module>
<!--交易引擎处理步骤的定义-->
<module id="transEngine" class="com.system.core.handlers.CoreEngineHandler" >
<!--交易引擎处理需要的参数:交易对象的管理器-->
<property name="transManager">
<ref module="transManager">
</property>
</module>
<!--返回视图步骤的定义-->
<module id="replyView" class="com.system.view.handlers.ReplyViewHandler" >
<!--返回视图步骤需要的参数:交易对象的管理器-->
<property name="transManager">
<ref module="transManager">
</property>
<!--返回视图步骤处理需要的参数:视图工厂-->
<property name="viewFactory">
<ref module="viewFactory"><!--视图工厂可通过应用启动显示初始化、由框架控制反转或者固定某几类视图定义等方式定义-->
</property>
</module>
上面例子中所有的module都可以理解为一个切面或者一个厚切面(例如defaultChain是多个切面组合形成).这些切面对应的程序实现类只要按照规范确定的接口,实现相关的方法就可以在实际运作中被执行引擎正常执行。无论是新增、删除或者修改切面逻辑都对其他切面无影响。
/**
* 请求处理接口类
*/
public abstract interface IChannelHandler {
/**
* 处理请求方法
* @param channelRequest 处理请求对象
* @param channelResponse 处理结果对象
* @throws HandlerException 处理异常
*/
public abstract void handleRequest(IRequest channelRequest,IResponse channelResponse) throws HandlerException;
}
/**
* 限流校验切面的实现类
*/
public class ParellelControlHandler implements IChannelHandler {
private TransManager transManager;//交易对象管理器
private ParellelManager parellelManager;//限流管理器
@Override
public void handleRequest(IRequest channelRequest, IResponse channelResponse) throws HandlerException {
// TODO Auto-generated method stub
(String) transName = channelRequest.getParam("transName");//交易名称
(String) transType = channelRequest.getParam("transType");//交易行为
//判断如果设置了针对当前交易和行为的限流参数,进行限流校验
if(parellelManager.isControl(transName) && parellelManager.isControl(transType)){
int proprity = transManager.getProprity(transName);//获取交易的优先级
//调用限流管理器校验是否允许交易执行。未通过则抛出限流校验不通过的异常
if(parellelManager.isOverRun(transName,transType,proprity)){
throw new ParellelException(Constans.ErrorCodes.OR000010,"交易限流控制不通过");
}
}
//限流校验通过/不需要限流校验是的处理内容
...
}
public void setTransManager(TransManager transManager){
this.transManager = transManager;
}
public void setParellelManager(ParellelManager parellelManager){
this.parellelManager = parellelManager;
}
}
数据结构扩展
数据结构的可扩展性也是系统设计时需要考虑的可扩展内容之一。在平时的系统设计中,要充分考虑数据结构的扩展和复用,在后续维护过程中出现类似的场景时,能够有效的复用之前的数据结构,在快速响应业务的同时,也减少对存量功能的影响,确保系统的稳定性。下面以数据库表为例,介绍几种具有扩展性的设计方法。
- 提前预留数据库表字段
在设计表结构的时候提前预留部分字段,以便后续使用。例如债券信息,可以获取常见的债券的信息要素,预留一些目前业务还未涉及的属性;或者预留一些通用性的字段,在实际程序中字段映射的方式进行数据存储和查询。
优点:能快速业务响应;对于明确的预留字段,甚至可以提前做到索引的建立
缺点:扩展字段有限,可扩展的内容也比较局限。通用性字段可读性不佳,对于已经确定的要素,仍需要进行固化优化。
- 使用一个值的不同位置
一般用于标识是否、有无等信息的扩展。每一类表示具体的值。例如,通过涉及一个10位数字,用来表示某用户对某个菜单的权限,每一位数字表示对应某种权限是否拥有。例如1000010000表示有调用第1位(查询)和第6位(导出)对应的权限。
优点:方便简单,可支持的扩展范围大。
缺点:使用的场景比较局限;在多人都要进行扩展时,对于每一位代表的服务内容同意产生冲突;不能建立简单索引实现快速检索。
- 动态增加表字段
根据实际场景,动态对表列进行新增、删除等操作。一般使用的比较少,因为涉及到数据结构,可能对存量功能产生影响。
优点:能实时响应业务,新增的字段意义明确,可读性佳
缺点:需要评估动态扩展对存量功能的影响,并且新增的内容不可控,一般无法做到快速索引
- 使用外部可扩展的数据结构
使用外部可扩展到数据结构(或者可扩展的语言)标识数据内容,例如JSON,XML等形式支持自定义形式,在数据表中仅保存可扩展的数据结构内容。
优点:可扩展性强,不依赖数据表的结构。
缺点:无法对数据结构内部要素实时快速检索;无法快速简单的提取数据结构中的特定要素进行提取。
- 改列为行,使用Key-value形式
比较常见扩展方式。分析对象的属性,将属性进行归类,分类操作类和知识类。操作类就是字段会用于筛选、关联等操作,知识类就是字段仅用于展示的。由于知识类字段仅用于展示,可以独立保存不需要与操作类字段一起存放,并且经常扩展的也是这一类字段。因此使用key-value的方式将这些字段保存下来,后续扩展时就不用变动表结构就可以实现。例如用户信息,userinfo表保存操作类属性,例如userName(名称),deparment(部门);其他一些则通过key-value形式保存在扩展通用表(generalextend)或者专用通用表(userextend)中,例如address(地址),email(邮箱)。userinfo与userextend可通过关键字userid关联。
优点:比较灵活,理论上支持无限扩展;
缺点:需要有良好的中间件支持,使得对外的服务能方便的按列获取和访问;要对Key有良好的组织管理,避免出现混用
系统扩展
系统可扩展性(可伸缩性scalability)是一种对软件系统计算处理能力的设计指标,在系统扩展成长过程中,通过很少的改动甚至只是硬件设备的添置,就能实现整个系统处理能力的线性增长,实现系统平滑线性的性能提升,达到高吞吐量和低延迟高性能的目标。
下面是几种常见的系统扩展的设计模式
- 负载均衡 -把一个请求按一定hash算法或规则分配到服务器组中的一台去处理,以分担单个服务器的压力。
- 分头收集(Scatter and Gather)- 把一个请求分解成好几个服务请求分发到多个server上,每个server处理后返回的结果会被合并成一个返回结果给请求端。 常见于搜索引擎如google,百度,搜狗,对一个关键词的搜索结果是由多台server处理并合并成一个搜索结果页
- 结果缓存-服务器缓存某个请求的结果,下次对同样的请求只返回缓存的结果就可以了,避免下次同样的请求进来时去做重复的计算。
- 空间共享 – 分布式计算常用的模式,所有的数据、对象都放在一个共享虚拟空间,所有的计算进程共享并控制这些数据
- 管道过滤 – 所有的请求都先进入某个管道,然后以先进先出的方式接受处理和返回结果
- MapReduce – 在处理批量任务时,如果磁盘I / O是主要瓶颈,可以采取这种模式。它的使用分布式的文件系统,从而使多个I/O操作能够并行。这种模式在谷歌的内部应用程序中使用的较多,Hadoop就是个典型案例
- 批量同步并行 – 该模式下的所有任务是基于锁步执行,由Master来协调。每个任务重复以下步骤,直到再没有活跃的任务。 每个任务从输入队列中读取数据 、每个任务根据自己读取到的数据进行处理 、每个任务将自己的处理结果直接返回
软件系统的可扩展性设计非常重要,但又比较难以掌握,业界试图通过云计算或高并发语言等方式节省开发者精力,但是,无论采取什么技术,如果应用系统内部是铁板一块,例如严重依赖数据库,系统达到一定访问规模,负载都会集中到一两台数据库服务器上,这时进行分区扩展伸缩就比较困难,正如Hibernate框架创建人Gavin King所说:关系数据库是最不可扩展的。