tags:设计模式
OCP–Closed for Modification;Open for Extension.
开闭原则要求软件有一个良好的基本结构,确保面对变化的时候,仅仅扩展而不是修改现有对象的组织框架就可以随需而动.但我们知道越是这样趋于道可道非常道的内容就越难以找到一个固定的方式达成,我们可以举出很多例子指出这样或那样是违反开闭原则的,但不容易展示怎样才是开闭原则,原因在于影响软件变化的原因很多,即便现有的对象组织框架针对现状基本上满足了扩展要求,未来也常常发现框架本身成了事件开闭原则的阻力.
我们把之前讲的DataHandler的例子进一步具体化,以讨论如何设计更符合开闭原则的要求.
我们的DataHandler是一个简易的ETL工具,即数据的抽取,装载,转换,它只负责提取整个表或视图的数据.
这个工具深受好评,公司决定将这个工具作为主打产品,但问题是数据库产品总是在增加,数据库产品版本总在升级,最近NO-SQL运行兴起后又出现了一大批非关系型的新型数据库.
在第一版是产品只支持oracle数据库和sql server数据库:
class Etx{
ConnectionManager cm;
List getDataObject(dbName,name)
type = cm.getDbType(dbName);
if(type == ORACLE)
{
db = new OracleDataBase();
return db.getObject(name);
}
else if(type = SQL Server)
{
db = new SQLserverDataBase();
return db.getObject(name);
}else
{
throw new ClassNotFoundException(type);
}
}
class ConnectionManager
{
String getDbType(name)
}
ETL类负责提取,ConnectionManager类管理数据库连接,两个类各司其职,而且根据上面示例情景的假设,影响ETL变化的因素只有一个–新类型的数据库,所以这个设计基本上满足单一职责原则的要求.
没有满足里氏替换原则和依赖倒置原则的要求.
两个接口类简单,仅从方法数量上看,各自简单到对外只有一个公共方法,基本符合接口隔离原则.
迪米特法则的各层意思基本上都能满足.
虽然满足了以上一些准则但是这个类有很大的缺陷,就是因为一旦有新的数据库,ETL类就要修改,而不是扩展,于是我们又重构了第二版
伪码如下:
class interface DatabaseEtlEntity
{
List getObjectByName(name);
}
class Etl
{
List getObjectByName(name)
{
return ConnectionManager
.getDatabaseEtlEntity(dbName);
}
}
class ConnectionManager
{
public DatabaseEtlEntity getDatabaseEtlEntity(dbName)
{
switch(getDbType(dbName))
{
case ORACLE:
return new OracleDbAdapter();
case SQL Server:
return new SqlDbAdapter();
default:
throw new ClassNotFoundException();
}
}
private String getDbType(dbName)
{
return ...
}
}
class SqlDbAdapter{}
class OracleDbAdapter{}
上面的设计在保留了之前的一些特点外,还通过里氏替换原则和依赖倒置原则实现支持多数据库的目标,而且ConnectionManager的getDbType()方法被置为private方法,所以传承了接口隔离原则.
但我们也注意到,尽管ETL类在当前语境设计下满足开闭原则要求了,但却把麻烦抛给了ConnectionManager类了,因为以后如果要增加MYSQL DB2这些数据库的时候,ConnectionManager类还要继续修改.由于修改集中在构造过程,既然应用自己来解决问题总是难免造成与具体类型的依赖关系(ConnectionManager类对于SqlDbAdapter的依赖性),不妨考虑将变化转移到语言运行环境层面.构造对象时,java平台可以使用Class.newInstance()方法.
置于数据库具体该用什么对象来处理,这些信息也找一个相对稳定的机制–配置文件保存即可.
class interface DatabaseEtlEntity
{
List getObjectByName(name);
}
class Etl
{
List getDataObject(dbName,name)
{
return ConnectionManager.
getDatabaseEtlEntity(dbName).
getObjectByName(name);
}
}
class ConnectionManager
{
DatabaseEtlEntity getDatabaseEtlEntity(dbName)
{
typeName = ConfigManager.getType(dbName);
return Class.newInstance(typeName);
}
}
class ConfigManager
{
string getConfig(sectionName)
{
read...config-file;
}
}
第三版设计在当前语境下也大致满足了开闭原则的要求,但代价是增加了一系列对象,是在用现在的工作量减轻未来可能出现的工作量,投入是明显的,收入却是不确定的.实际项目中,面对开闭原则我们除了技术上这种敝帚自珍外还要考虑这么做是否值得,我们是不是把真的80%的精力放到那20%关键对象部分,而不是抛开软件需要不论,过度打磨自己的设计.