查看: 2784|回复: 0

[PHP实例] 面向对象4原则

发表于 2018-3-13 08:00:11
OCP(开闭原则)

类应该对扩展开放,对修改而关闭。

应用举例

本人是做彩票业务的,就以彩票举例吧。下面是一段设计不良的校验投注号码的代码

  1. public boolean validate(String drawNum){
  2. if (type.equals("PL3")) {
  3. PL3Validate validatePL3 = new PL3Validate();
  4. validatePL3.validate();
  5. }
  6. else if (type.equals("PL5")) {
  7. PL5Validate validatePL5 = new PL5Validate();
  8. validatePL5.validate();
  9. }
  10. }
复制代码

其对应的类图为:

image

若这时添加大乐透彩种的校验,需要修改OCPDemo中的validate的代码,加入另外一个else if 分支,这违反了OCP原则,并没有对修改而关闭。
可以进行如下修改:
我们添加抽象类AbstractNumberValidate,让PL3Validate和PL5Validate继承该类,OCPDemo仅依赖AbstractNumberValidate类。上面的代码修改为:

  1. AbstractNumberValidate validate;
  2. public static class PL3ValidateImpl extends AbstractNumberValidate{
  3. public boolean validate(String drawNum){
  4. return false;
  5. }
  6. }
复制代码

修改后的类图为:

image

这样无论添加任何彩种,OCPDemo的validate都不需要更改。若这时添加大乐透彩种的校验,只需要添加一个DLTValidate类继承AbstractNumberValidate实现自己的校验规则,并注入到OCPDemo中即可。

这里仅仅以继承的方式来解决上边的问题,解法不唯一。

OCP不仅仅是继承

OCP关系到灵活性,而不只是继承。
例如:你在类中有一些private的方法,(这就是禁止为修改而关闭),但是你有一些public方法以不同的方式调用private方法(允许为扩展而开放)

OCP的核心是 让你有效的扩展程序,而不是改变之前的程序代码。

DRY(不自我重复)

通过将共同之物抽取出来并置于单一地方避免重复的程序代码。

举例说明

Java初学者,使用JDBC,查询数据库中数据时,会有如下代码,每调用一个查询均会有
3部分,执行查询,提取结果,关闭结果集合。

  1. //调用查询
  2. stmt = conn.createStatement();
  3. result = stmt.executeQuery("select * from person");//执行sql语句,结果集放在result中
  4. //提取结果
  5. while(result.next()){//判断是否还有下一行
  6. String name = result.getString("name");//获取数据库person表中name字段的值
  7. Person p=new Person();
  8. p.setName(name);
  9. }
  10. //关闭结果集合
  11. result.close();
  12. stmt.close();
复制代码

如果每调用查询一次数据库均要写上述代码,绝对会非常的累,也违反DRY原则,系统中会出现大量的重复代码。
下面让我们看看Spring的JdbcTemplate如何遵循DRY原则。上边的模式,有一定的套路,Spring总结了套路,封装成了模板,经过Spring的封装,只需传入Sql,和结果集合转换的类。代码如下:

  1. //实际只需调用queryForObject即可
  2. @Override
  3. public <T> T queryForObject(String sql, Class<T> requiredType) throws DataAccessException {
  4. return queryForObject(sql, getSingleColumnRowMapper(requiredType));
  5. }
  6. public <T> T query(final String sql, final ResultSetExtractor<T> rse) throws DataAccessException {
  7. Assert.notNull(sql, "SQL must not be null");
  8. Assert.notNull(rse, "ResultSetExtractor must not be null");
  9. if (logger.isDebugEnabled()) {
  10. logger.debug("Executing SQL query [" + sql + "]");
  11. }
  12. class QueryStatementCallback implements StatementCallback<T>, SqlProvider {
  13. @Override
  14. public T doInStatement(Statement stmt) throws SQLException {
  15. ResultSet rs = null;
  16. try {
  17. //执行SQL
  18. rs = stmt.executeQuery(sql);
  19. //----提取结果-start
  20. ResultSet rsToUse = rs;
  21. if (nativeJdbcExtractor != null) {
  22. rsToUse = nativeJdbcExtractor.getNativeResultSet(rs);
  23. }
  24. return rse.extractData(rsToUse);
  25. //--------提取结果-end
  26. }
  27. finally {
  28. //关闭结果集合
  29. JdbcUtils.closeResultSet(rs);
  30. }
  31. }
  32. @Override
  33. public String getSql() {
  34. return sql;
  35. }
  36. }
  37. return execute(new QueryStatementCallback());
  38. }
复制代码
DRY不仅应用于编码

抽取出重复程序代码是运用DRY的好开始,但DRY的内涵可不只是如此!当试图避免重复程序代码时,实际也在试着确保你对应用程序中每一个功能和需求只实现一次。
其实无论编写需求,开发用例或者编写代码都应该遵守DRY原则!

举个我工作中的例子
关于红包回收业务需求
我们的业务需求文档写了如下需求:

红包过期应该进行自动回收 红包领取后30天内有效,过期应该回收。 红包活动过期,应该回收未使用的红包。

这个是明显的不遵循DRY,当然产品经理可能没有听说过DRY,如果你遇到了这种情况,请默默的在心里将需求凝练下即可。例如:

应按规则回收红包,规则如下:
a. 未使用的在红包活动过期后回收
b. 已领取部分使用的自领取之日起30天后进行回收
c. 已使用完毕的不进行回收 SRP(单一职责)

系统中每一个对象应该具有单一职责,所有对象的服务都应该聚焦在实现该职责上。

应用举例

假设系统中有如下一个简单的Car类,其内部结果如下类图:

image

下面我们针对这个简单的例子,找出其不符合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原则的方式实现。

  1. public static class Graph2D{
  2. int x;
  3. int y;
  4. public void setGraph(int x,int y){
  5. this.x=x;
  6. this.y=y;
  7. }
  8. }
  9. public static class Graph3D extends Graph2D{
  10. int z;
  11. public void setGraph(int x,int y,int z){
  12. this.x=x;
  13. this.y=y;
  14. this.z=z;
  15. }
  16. }
  17. public static void main(String[] args) {
  18. Graph3D Graph3D=new Graph3D();
  19. // 由于继承,使用者会非常迷茫,如何设置x,y,z
  20. Graph3D.setGraph(x, y);//来自父类Graph2D
  21. Graph3D.setGraph(x, y, z);//自己的
  22. }
复制代码

上边的代码我们让Graph3D继承了Graph2D,造成Graph3D的使用者对setGraph产生了疑惑。 因为有2个setGraph方法。若不了解内部实现的人,将难以使用。

如何解决不满足LSP的情况

一共有3种处理方式:委托,聚合,组合。

委托

将特定工作的责任委派给另外一个类或方法。

如果你想要使用另一个类的功能性,但不想改变该功能,考虑以委托代替继承。

下面我们以委托的方式,解决上的问题,修改后代码,仅有一个setGraph方法,不会产生不必要的麻烦。
原本的类图为:

输入图片说明

以委托的方式修改后的类图,这时Graph3D依赖时Graph2D

image

相应的代码如下:

  1. public static class Graph2D{
  2. int x;
  3. int y;
  4. public void setGraph(int x,int y){
  5. this.x=x;
  6. this.y=y;
  7. }
  8. }
  9. public static class Graph3D {
  10. int z;
  11. private Graph2D graph2D;//将平面部分委托给Graph2D处理
  12. public void setGraph(int x,int y,int z){
  13. graph2D.setGraph(x, y);
  14. this.z=z;
  15. }
  16. }
  17. public static void main(String[] args) {
  18. Graph3D graph3D=new Graph3D();
  19. graph3D.setGraph(x, y, z);
  20. }
复制代码
组合

组合让你使用来自一组其他的行为,并且可以在运行时切换该行为。

组合类图举例:

image

在组合中,由其他行为组成的对象(本例子中是Unit类)拥有那些行为(本例中指Weapon的attack方法)。当拥有者对象被销毁时(Unit被销毁),其所有行为也被销毁(Weapon的所有实现也被销毁)。组合中的行为不存在组合之外。

聚合

当一个类被用作另一个类的一部分时,但仍然可以存在于该类之外。(组合单式没有结束)

聚合举例类图:

image

总结

类应该对扩展开发,对修改而关闭。(OCP)

通过将共同之物抽取出来并置于单一地方避免重复的程序代码(DRY)

系统中每一个对象应该具有单一职责,所有对象的服务都应该聚焦在实现该职责上。(SRP)

子类型必须能够替换其基类型。(LSP)



回复

使用道具 举报