类应该对扩展开放,对修改而关闭。
应用举例本人是做彩票业务的,就以彩票举例吧。下面是一段设计不良的校验投注号码的代码
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. 已使用完毕的不进行回收
系统中每一个对象应该具有单一职责,所有对象的服务都应该聚焦在实现该职责上。
应用举例假设系统中有如下一个简单的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的部分。
- DRY和SRP往往一同出现,DRY关注把一个功能片段放到一个单独的地方。
SRP是关于一个类只做一件事。 - 内聚力的另外一个名称就是SRP。
子类型必须能够替换其基类型。
违反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)
共同学习,写下你的评论
评论加载中...
作者其他优质文章