面向对象设计4原则 原
OCP(开闭原则)
类应该对扩展开放,对修改而关闭。
应用举例
本人是做彩票业务的,就以彩票举例吧。下面是一段设计不良的校验投注号码的代码
public boolean validate(String drawNum){
if (type.equals("PL3")) {
PL3Validate validatePL3 = new PL3Validate();
validatePL3.validate();
}
else if (type.equals("PL5")) {
PL5Validate validatePL5 = new PL5Validate();
validatePL5.validate();
}
}
其对应的类图为:
若这时添加大乐透彩种的校验,需要修改OCPDemo中的validate的代码,加入另外一个else if 分支,这违反了OCP原则,并没有对修改而关闭。 可以进行如下修改: 我们添加抽象类AbstractNumberValidate,让PL3Validate和PL5Validate继承该类,OCPDemo仅依赖AbstractNumberValidate类。上面的代码修改为:
AbstractNumberValidate validate;
public static class PL3ValidateImpl extends AbstractNumberValidate{
public boolean validate(String drawNum){
return false;
}
}
修改后的类图为:
这样无论添加任何彩种,OCPDemo的validate都不需要更改。若这时添加大乐透彩种的校验,只需要添加一个DLTValidate类继承AbstractNumberValidate实现自己的校验规则,并注入到OCPDemo中即可。
这里仅仅以继承的方式来解决上边的问题,解法不唯一。
OCP不仅仅是继承
OCP关系到灵活性,而不只是继承。 例如:你在类中有一些private的方法,(这就是禁止为修改而关闭),但是你有一些public方法以不同的方式调用private方法(允许为扩展而开放)
OCP的核心是 让你有效的扩展程序,而不是改变之前的程序代码。
DRY(不自我重复)
通过将共同之物抽取出来并置于单一地方避免重复的程序代码。
举例说明
Java初学者,使用JDBC,查询数据库中数据时,会有如下代码,每调用一个查询均会有 3部分,执行查询,提取结果,关闭结果集合。
//调用查询
stmt = conn.createStatement();
result = stmt.executeQuery("select * from person");//执行sql语句,结果集放在result中
//提取结果
while(result.next()){//判断是否还有下一行
String name = result.getString("name");//获取数据库person表中name字段的值
Person p=new Person();
p.setName(name);
}
//关闭结果集合
result.close();
stmt.close();
如果每调用查询一次数据库均要写上述代码,绝对会非常的累,也违反DRY原则,系统中会出现大量的重复代码。 下面让我们看看Spring的JdbcTemplate如何遵循DRY原则。上边的模式,有一定的套路,Spring总结了套路,封装成了模板,经过Spring的封装,只需传入Sql,和结果集合转换的类。代码如下:
//实际只需调用queryForObject即可
@Override
public <T> T queryForObject(String sql, Class<T> requiredType) throws DataAccessException {
return queryForObject(sql, getSingleColumnRowMapper(requiredType));
}
public <T> T query(final String sql, final ResultSetExtractor<T> rse) throws DataAccessException {
Assert.notNull(sql, "SQL must not be null");
Assert.notNull(rse, "ResultSetExtractor must not be null");
if (logger.isDebugEnabled()) {
logger.debug("Executing SQL query [" + sql + "]");
}
class QueryStatementCallback implements StatementCallback<T>, SqlProvider {
@Override
public T doInStatement(Statement stmt) throws SQLException {
ResultSet rs = null;
try {
//执行SQL
rs = stmt.executeQuery(sql);
//----提取结果-start
ResultSet rsToUse = rs;
if (nativeJdbcExtractor != null) {
rsToUse = nativeJdbcExtractor.getNativeResultSet(rs);
}
return rse.extractData(rsToUse);
//--------提取结果-end
}
finally {
//关闭结果集合
JdbcUtils.closeResultSet(rs);
}
}
@Override
public String getSql() {
return sql;
}
}
return execute(new QueryStatementCallback());
}
DRY不仅应用于编码
抽取出重复程序代码是运用DRY的好开始,但DRY的内涵可不只是如此!当试图避免重复程序代码时,实际也在试着确保你对应用程序中每一个功能和需求只实现一次。 其实无论编写需求,开发用例或者编写代码都应该遵守DRY原则!
举个我工作中的例子 关于红包回收业务需求 我们的业务需求文档写了如下需求:
- 红包过期应该进行自动回收
- 红包领取后30天内有效,过期应该回收。
- 红包活动过期,应该回收未使用的红包。
这个是明显的不遵循DRY,当然产品经理可能没有听说过DRY,如果你遇到了这种情况,请默默的在心里将需求凝练下即可。例如:
- 应按规则回收红包,规则如下: a. 未使用的在红包活动过期后回收 b. 已领取部分使用的自领取之日起30天后进行回收 c. 已使用完毕的不进行回收
SRP(单一职责)
系统中每一个对象应该具有单一职责,所有对象的服务都应该聚焦在实现该职责上。
应用举例
假设系统中有如下一个简单的Car类,其内部结果如下类图:
下面我们针对这个简单的例子,找出其不符合SRP的地方。
找出一个类中不符合SRP的方法为:
- 做填空,该 【XXX类】 自己 【XXX 方法】,找出语义不通顺的地方
- 结合自身业务理解进行进一步分析,最终确定不符合SRP的部分。
以Car类为例子 我们先进行第一步 :
该 Car 自己 start
该 Car 自己 stop
该 Car 自己 getOil
** 该 Car 自己 wash (?车自己洗车) **
** 该 Car 自己 drive (?车自己驾驶,难道是自动驾驶的车)**
我们找出两个方法可能不遵循SRP,一个是wash,一个是drive。
下面我们执行第二步,根据根据业务理解进行分析。 这里我们没有什么业务背景,仅依据生活经验进行分析。
- 车一般有其他人或机构进行清洗,不属于车的部分。应该从Car移除
- drive,处理自动驾驶车以外,车均由司机驾驶,自动驾驶车的驾驶员可以理解为电脑,所以drive也不属于Car类,应该从Car类移除。
从上边的小例子 我们可以看出:
- 方法名称要与具体实现的功能相符,否则第一步无法部分进行。
- 对业务的理解很重要,否则无法最终决定违反SRP的部分。
2点说明
- DRY和SRP往往一同出现,DRY关注把一个功能片段放到一个单独的地方。 SRP是关于一个类只做一件事。
- 内聚力的另外一个名称就是SRP。
LSP(里氏替换原则)
子类型必须能够替换其基类型。
违反LSP的情形举例
假设我们有一个Graph2D 用于制作2D平面,现在要新创建一个Graph3D类,用于构建立体图,下面我们使用违反LSP原则的方式实现。
public static class Graph2D{
int x;
int y;
public void setGraph(int x,int y){
this.x=x;
this.y=y;
}
}
public static class Graph3D extends Graph2D{
int z;
public void setGraph(int x,int y,int z){
this.x=x;
this.y=y;
this.z=z;
}
}
public static void main(String[] args) {
Graph3D Graph3D=new Graph3D();
// 由于继承,使用者会非常迷茫,如何设置x,y,z
Graph3D.setGraph(x, y);//来自父类Graph2D
Graph3D.setGraph(x, y, z);//自己的
}
上边的代码我们让Graph3D继承了Graph2D,造成Graph3D的使用者对setGraph产生了疑惑。 因为有2个setGraph方法。若不了解内部实现的人,将难以使用。
如何解决不满足LSP的情况
一共有3种处理方式:委托,聚合,组合。
委托
将特定工作的责任委派给另外一个类或方法。
如果你想要使用另一个类的功能性,但不想改变该功能,考虑以委托代替继承。
下面我们以委托的方式,解决上的问题,修改后代码,仅有一个setGraph方法,不会产生不必要的麻烦。 原本的类图为:
以委托的方式修改后的类图,这时Graph3D依赖时Graph2D
相应的代码如下:
public static class Graph2D{
int x;
int y;
public void setGraph(int x,int y){
this.x=x;
this.y=y;
}
}
public static class Graph3D {
int z;
private Graph2D graph2D;//将平面部分委托给Graph2D处理
public void setGraph(int x,int y,int z){
graph2D.setGraph(x, y);
this.z=z;
}
}
public static void main(String[] args) {
Graph3D graph3D=new Graph3D();
graph3D.setGraph(x, y, z);
}
组合
组合让你使用来自一组其他的行为,并且可以在运行时切换该行为。
组合类图举例:
在组合中,由其他行为组成的对象(本例子中是Unit类)拥有那些行为(本例中指Weapon的attack方法)。当拥有者对象被销毁时(Unit被销毁),其所有行为也被销毁(Weapon的所有实现也被销毁)。组合中的行为不存在组合之外。
聚合
当一个类被用作另一个类的一部分时,但仍然可以存在于该类之外。(组合单式没有结束)
聚合举例类图:
总结
类应该对扩展开发,对修改而关闭。(OCP)
通过将共同之物抽取出来并置于单一地方避免重复的程序代码(DRY)
系统中每一个对象应该具有单一职责,所有对象的服务都应该聚焦在实现该职责上。(SRP)
子类型必须能够替换其基类型。(LSP)
- 接口测试 23 requests基础入门二
- 写让别人能读懂的代码+网页性能管理详解
- CDOJ 1330 柱爷与远古法阵【高斯消元,卡精度】
- Robot Framework | 04 参数化基于Public API的RFS测试
- 接口测试 | 22 requests基础入门
- 分享一个Mvc的多层架构,欢迎大家拍砖斧正
- Robot Framework | 03 基于Public API创建你RFS测试
- Robot Framework | 02 从抛弃RIDE开始创建你的RFS测试
- ASP.NET5 Beta8可用性
- Docker Swarm集群初探
- 数据库逻辑设计
- 06.移动先行之谁主沉浮----我的代码我来写(Xaml的优势)
- [快学Python3]迭代器和生成器
- [快学Python3]INI文件读写
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法