欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

设计模式之设计原则(整理,非原创)

程序员文章站 2024-03-16 22:17:52
...

设计模式

写在前面:这是一个总结。所有讲述都是摘录,摘抄,总结各种帖子,非原创。如果侵犯您的权益,请联系小编删除!!!!

(文末附有原文链接可能有遗漏,请见谅!!!)

目的:总结复习设计模式

设计模式的目的

  每一个模式描述了一个在我们周围不断重复发生的问题,以及该问题的解决方案的核心。这样你就能一次又一次地使用该方案而不必做重复劳动。”

每一个设计模式系统地命名、解释和评价了面向对象系统中一个重要的和重复出现的设计。

设计模式六大原则

1.开闭原则

定义

软件实现应该对扩展开放,对修改关闭。
其含义是说一个软件实体应该通过扩展来实现变化,而不是通过修改已有的代码来实现变化的。


我的理解是,开闭原则是用扩展来替换更改。

示例

设计模式之设计原则(整理,非原创)

当我们需要打折销售时,该如何应对呢?

通常情况下,有以下三种方法来解决:

  1. 修改接口。在IBook接口中,增加一个getPrice(),专门用于进行打折处理,所有的实现类实现此方法。但是这样的一个修改方式,实现类NovelBook要修改,同时IBook接口应该是稳定且可靠,不应该经常发生改变,否则接口作为契约的作用就失去了。因此,此方案否定。

  2. 修改实现类。修改NovelBook类的方法,直接在getPrice()方法中实现打折处理。此方法是有问题的,例如我们如果getPrice()方法中只需要读取书籍的打折前的价格呢?这不是有问题吗?当然我们也可以再增加getOffPrice()方法,这也是可以实现其需求,但是这就有二个读取价格的方法,因此,该方案也不是一个最优方案。

  3. 通过扩展实现变化。我们可以增加一个子类OffNovelBook,覆写getPrice方法。此方法修改少,对现有的代码没有影响,风险少,是个好办法。

下图是修改后的类图

设计模式之设计原则(整理,非原创)

为什么要使用开闭原则

  1. 提高复用性
  2. 提高维护性
  3. 面向对象的开发要求


2.依赖倒转原则

定义

  1. 高层模块不该依赖于低层模块, 二者都该依赖于抽象
    底层模块依赖于高层的抽象的接口,这样就解决了底层模块改动高层模块也得改动的问题。
  2. 抽象不应该依赖于细节,细节应该依赖于抽象
    这里的细节值得是底层的逻辑。高层通常称为策略。也就是说底层模块的实现必须符合高层定义的接口标准。

简单来说就是面向接口编程

示例 

设计模式之设计原则(整理,非原创)

现在小明可以阅读文学经典,但是小明同学忽然想看小说。

由于XiaoMing类和LiteraryClassic类是强依赖,紧耦合关系,小明同学无法调用小说类。

因此 修改结构如下

设计模式之设计原则(整理,非原创)

至此,小明同学就可以愉快的阅读了。

  为什么依赖抽象的接口可以适应变化的需求?这就要从接口的本质来说,接口就是把一些公司的方法和属性声明,然后具体的业务逻辑是可以在实现接口的具体类中实现的。所以我们当依赖对象是接口时,就可以适应所有的实现此接口的具体类变化。

依赖的三种方法

构造函数传递依赖对象

class XiaoMing(IReader):
    # 构造函数注入
    def __init__(self, reader):
        self.reader = reader
    #阅读
    def read():
        reader.read()

Setter 方法注入

class XiaoMing(IReader):
    # 构造函数注入
    __read = None
    #阅读
    def setRead(reader):
        this.reader = reader

    def read():
        reader.read()

接口声明依赖

  在接口的方法中声明依赖对象,在为什么我们要符合依赖倒置原则的例子中,我们采用了接口声明依赖的方式,该方法也叫做接口注入。

为什么要使用依赖倒置原则

  1. 在新增加底层模块时,只需要修改业务场景类,也就是高层模块,对其他的底层模块不需要修改,业务就可以运行。这样,把“变更”的风险扩散到最低。

  2. 解决并行开发引起的风险。两个类之间有依赖关系,只要制定出两者之间的接口(或抽象类)就可以独立开发了,而且项目之间的单元测试也可以独立的运行

  3. 抽象是对实现的约束,对依赖者而言,也是一种契约,不仅仅约束自己,还同时约束自己与外部的关系,其目的是保证所有的细节不逃脱契约的范畴,确保约束双方按照既定的契约(抽象)共同发展,只要抽象这根基线在,细节就逃脱不了这个圈圈,始终让你的对象做到“言而有信”“言出必行”


3.单一职责原则

(一)定义

一个类应该只有一个引起它变化的原因。
也就是说,一个类的职责是单一的,它只负责某一件任务。只实现某一项功能。

(二)优点

  1. 降低了类的复杂度。
  2. 提高了代码的可读性。
  3. 提改了系统的可维护性。
  4. 变更引起的风险变低了。

示例1

用户信息维护类图

设计模式之设计原则(整理,非原创)

为了实现单一职责原则,我们需要把用户的属性和用户的行为分开。
修正之后:
设计模式之设计原则(整理,非原创)

  重新拆封成两个接口,IUserBO负责用户的属性,简单地说,IUserBO的职责就是收集和反馈用户的属性信息;IUserBiz负责用户的行为,完成用户信息的维护和变更。

其实这样更好一些

设计模式之设计原则(整理,非原创)

示例2

电话通话的时候有四个过程发生:拨号、通话、回应、挂机,那我们写一个接口,其类图应该如图所示:
设计模式之设计原则(整理,非原创)

  IPhone这个接口可不是只有一个职责,它包含了两个职责:一个是协议管理,一个是数据传送。diag()和huangup()两个方法实现的是协议管理,分别负责拨号接通和挂机;chat()和answer()是数据的传送,把我们说的话转换成模拟信号或数字信号传递到对方,然后再把对方传递过来的信号还原成我们听得懂语言。协议接通的变化会引起这个接口或实现类的变化!数据传送的变化会引起这个接口或实现类的变化,这里有两个原因都引起了类的变化,而且这两个职责不会相互影响。通过这样的分析,我们发现类图上的IPhone接口包含了两个职责,而且这两个职责的变化不相互影响,那就考虑拆开成两个接口,其类图如图所示。
设计模式之设计原则(整理,非原创)

  这个类图虽然看着复杂,但是完全满足了单一职责的要求,每个接口职责分明,结构清晰。但是手机类要把ConnectionManager和DataTransfer组合在一块才能使用。因此这种强耦合的组合关系增加了类的复杂性。再次改进如下:

设计模式之设计原则(整理,非原创)

小叙

单一职责适用于接口、类,同时也适用于方法

改进前

设计模式之设计原则(整理,非原创)

改进后

设计模式之设计原则(整理,非原创)

这样的设计,每个方法的职责非常清晰明确,不仅开发简单,而且日后的维护也非常容易。


4.里式替换原则

(一)继承的优缺点

继承的优点

  1. 代码共享,减少创建类的工作量,每个子类都拥有父类的 方法和属性。
  2. 提高代码的重用性。
  3. 子类形似父类,却又异于父类。
  4. 提高代码的可扩展性。
  5. 提高产品或者项目的开放性。

继承的缺点

  1. 继承是侵入性的。只要继承,就必须拥有父类的所有属性和方法。
  2. 降低代码的灵活性。子类必须拥有父类的属性和方法。
  3. 增强了耦合性。当父类的常量,变量和方法被修改时,必须考虑子类的修改,而且在缺乏规范的环境下,这种修改可能导致大片代码需要重构。

(二)定义

所有引用基类的地方必须透明的使用其子类的对象。

换言之,只要是父类能够出现的地方,子类也可以出现,并且子类替换父类不会出现异常。反之,则不可。

以上定义包含四层含义

1. 子类必须完全实现父类的方法

以FPS游戏为例:

类图如下所示:
设计模式之设计原则(整理,非原创)

代码实现

枪支的抽象类
public abstract class AbstractGun {
    //枪用来干什么的?射击杀戮!
    public abstract void shoot();
}
各种枪械的实现类
public class Handgun extends AbstractGun {
    //手枪的特点是携带方便,射程短
    @Override
    public void shoot() {
        System.out.println("手 枪射击...");
    }
}

public class Rifle extends AbstractGun{
    //步枪的特点是射程远,威力大
    public void shoot(){
        System.out.println("步枪射击...");
    }
}

public class MachineGun extends AbstractGun{
    public void shoot(){
        System.out.println("机枪扫射...");
    }
}
士兵的实现类
public class Soldier {
    public void killEnemy(AbstractGun gun){
        System.out.println("士兵开始杀人...");
        gun.shoot();
    } 
}

定义中士兵的枪械时抽象的,具体是哪种枪需要在场景中确定

场景类
public class Client {
    public static void main(String[] args) {
    //产生三毛这个士兵
    Soldier sanMao = new Soldier();
    sanMao.killEnemy(new Rifle());
    }
}

  在这个程序中,我们给士兵三毛一把步枪,然后就可以运行杀敌了。在编写程序时,士兵类并不需要知道哪种型号的枪(子类)被传入。

注意:在类中调用其他类时务必要使用父类或者接口,如果不能使用父类或者接口,则说明类的设计违背了LSP(里氏替换原则)

ps:扩展部分见该博客

2. 子类可以有自己的个性

  因为子类有自己的特性,所以在子类出现的地方,父类未必就可以胜任。

以刚才的FPS游戏为例

步枪有几个比较“响亮”的型号,比如AK47、G3狙击步枪等,把这两个型号的枪引入后的Rifle子类图如图。
设计模式之设计原则(整理,非原创)

狙击枪源代码
public class G3 extends Rifle {
    //狙击枪都是携带一个精准的望远镜
    public void zoomOut(){
        System.out.println("通过望远镜查看敌人...");
    }
    public void shoot(){
        System.out.println("G3射击...");
    }
}
狙击手源代码
public class Snipper {
    public void killEnemy(G3 g3){
        //首先看看敌人的情况,别杀死敌人,自己也被人干掉
        g3.zoomOut();
        //开始射击
        g3.shoot();
    }
}
业务场景(狙击手杀死敌人)
public class Client {
    public static void main(String[] args) {
        //产生三毛这个狙击手
        Snipper sanMao = new Snipper();
        sanMao.killEnemy(new G3());
    }
}

在这个场景中,我们直接传递了子类,那我们能不能直接把父类传递进来呢?修改代码如下。

public class Client {
    public static void main(String[] args) {
        //产生三毛这个狙击手
        Snipper sanMao = new Snipper();
        Rifle rifle = new Rifle();
        sanMao.killEnemy((G3)Rifle);
    }
}

运行后,抛出java.lang.CllassCastException异常,这也就是大家常说的向下转型不安全。

3. 覆盖或者实现父类的方法时输入参数可以被放大

例子如下:

父类源代码(把HashMap转换为Collection集合类型)
public class Father {
    public Collection doSomething(HashMap map){
        System.out.println("父类被执行...");
        return map.values();
    }
}
子类源代码
public class Son extends Father { 
//放大输入参数类型
    public Collection doSomething(Map map){
        System.out.println("子类被执行...");
        return map.values();
    }
}

场景类源代码

public class Client { 
    public static void invoker(){
        //父类存在的地方,子类就应该能够存在
        Father f = new Father();
        HashMap map = new HashMap();
        f.doSomething(map);
    }
    public static void main(String[] args) {
        invoker();
    }
}
//运行结果:父类被执行...

子类替换父类后的源代码
public class Client {
    public static void invoker(){
        //父类存在的地方,子类就应该能够存在
        Son f =new Son();
        HashMap map = new HashMap();
        f.doSomething(map);
    }
    public static void main(String[] args) {
        invoker();
    }
}
//运行结果:父类被执行...

  父类方法的输入参数是HashMap类型,子类的输入参数是Map类型,也就是说子类的输入参数类型的范围扩大了,子类代替父类传递到调用者中,子类的方法永远都不回被执行。这是正确的,如果你想让子类的方法运行,就必须覆写父类的方法。大家可以这样想,在一个Invoker类中关联了一个父类,调用了一个父类的方法,子类可以覆写这个方法,也可以重载这个方法,前提是要扩大这个前置条件,就是输入参数的类型大于父类的类型覆盖范围。

如果Father类的输入参数类型大于子类的输入参数类型,会出现什么问题呢?

//父类的前置条件大
public class Father {

    public Collection doSomething(Map map){
        System.out.println("Map 转Collection被执行");
        return map.values();
    }
}
//子类的前置条件小
public class Son extends Father {
    //缩小输入参数范 
    public Collection doSomething(HashMap map){
        System.out.println("HashMap转Collection被执行...");
        return map.values(); 
    }
}
//业务场景类
public class Client {
    public static void invoker(){
    //有父类的地方就有子类

        Father f= new Father();
        HashMap map = new HashMap();
        f.doSomething(map);
        }
    public static void main(String[] args) { 
        invoker();
    }
}
//      代码运行结果如下所示:父类被执行...
//采用里氏替换原则后的业务场景类
public class Client {
    public static void invoker(){
        //有父类的地方就有子类
        Son f =new Son();
        HashMap map = new HashMap();
        f.doSomething(map);
    }
    public static void main(String[] args) {
        invoker();
    }
}
//执行结果:子类被执行...

  子类在没有覆写父类的方法的前提下,子类方法被执行了,这会引起业务逻辑混乱,因为在实际应用中父类一般都是抽象类,子类是实现类,你传递一个这样的实现类就会“歪曲”了父类的意图,引起一堆意想不到的业务逻辑混乱,所以子类中方法的前置条件必须与超类中被覆盖的方法的前置条件相同或者更宽松。

4. 覆盖或者实现父类的方法时输出结果可以被缩小

  这是什么意思呢,父类的一个方法的返回值是一个类型T,子类的相同方法(重载或覆写)的返回值为S,那么里氏替换原则就要求S必须小于等于T,也就是说要么S和T是同一个类型,要么S是T的子类,为什么呢?分两种情况,如果是覆写,父类和子类的同名方法的输入参数是相同的,两个方法的范围值S小于等于T,这是覆写的要求,这才是重中之重,子类覆写父类的方法,天经地义。如果是重载,则要求方法的输入参数类型或数量不相同,在里氏替换原则要求下,就是子类的输入参数大于或等于父类的输入参数,也就是说你写的这个方法是不会被调用到的,参考上面讲的前置条件。

(三)采用里氏替换原则的目的

  1. 增强程序的健壮性。
  2. 版本升级时也可以保持非常好的兼容性。
  3. 即使增加子类,原有的子类还可以继续运行。
  4. 在实际项目中,每个子类对应不同的业务含义,使用父类作为参数,传递不同的子类完成不同的业务逻辑。

(四)注意事项

  但是,子类必须完全实现父类的方法。
如果子类不能完整地实现父类的方法,或者父类的一些方法在子类中已经发生畸变,则可能会产生异常。
也就是说子类可以扩展和实现父类的功能,但是不能改变父类原有的功能。
主要包括以下四点:

  1. 子类可以实现父类中的抽象方法,但是不能覆盖父类的非抽象方法。
  2. 子类中可以增加自己特有的方法。
  3. 但子类的方法重载父类的方法时,方法的形参要比父类方法的输入参数更加宽松,保证在调用时不会引起业务逻辑的混乱
  4. 当子类的方法实现父类的抽象方法时,方法的后置条件要比父类更加严格。

5.接口隔离原则

(一)定义

  • 客户端不应该以来它不需要的接口。
  • 类之间的依赖关系应该建立在最小的接口上。

设计模式之设计原则(整理,非原创)

(二)目的

接口隔离原则的目的是为了保证接口的纯洁性。

(三)原则

  1. 接口要尽量小
  2. 接口要高内聚
  3. 接口的设计是有限度的

(四)示例

假设有个客户提出了软件系统的需求:

  1. 用户可以使用第三方QQ,微信,微博登录到系统。
  2. 系统中包括人员管理人员管理。
  3. 访问第三方的API获取一些数据。

好了拿到这个需求后首先经过分析,简单的原型设计,数据库设计之后开始编写代码了。 通常第一步定义接口。很快接口就定义出来了如下:

 public interface IObject
    {
        void Connection(string connectionString);
        SqlDataReader ExcuteSql(string sql);
        string LoginWithQQ(string token);
        string LoginWithWeibo(string token);
        string LoginWithWeiXin(string token);
        string GetDataFromAPI(string url, string token);
    }

  这个看起来还不错,接口已经定义了,写个具体类继承一下这个接口并实现所有的方法,现在就可以实现业务,写界面了。 等过了几天客户说 在给我加上支付宝登录。那好再加一个支付宝登录接口,代码现在长这样子:

 public interface IObject
    {
        void Connection(string connectionString);
        SqlDataReader ExcuteSql(string sql);
        string LoginWithQQ(string token);
        string LoginWithWeibo(string token);
        string LoginWithWeiXin(string token);
        string GetDataFromAPI(string url, string token);
        string LoginWithAlipay(string token);
    }

再在实现类中实现一下LoginWithAlipay方法 就好了。

时间在推移,一天客户说再给我加个百度登录,好吧套路有了加一个就是了,有啥了不起。 时间依旧。。。 客户说加个 facebook 登录, 。。。加个 Linkedin。。。, 尼玛 没完没了了, 现在接口已经变成这样子了:

public interface IObject
    {
        void Connection(string connectionString);
        SqlDataReader ExcuteSql(string sql);
        string LoginWithQQ(string token);
        string LoginWithWeibo(string token);
        string LoginWithWeiXin(string token);
        string GetDataFromAPI(string url, string token);
        string LoginWithAlipay(string token);
        string LoginWithTwitter(string token);
        string LoginWithFaceBook(string token);
        string LoginWithRenRen(string token);
        string LoginWithBaidu(string token);
        string LoginWithDropbox(string token);
        string LoginWithGithub(string token);
        //这里省略10000字       
stringLoginWithLinkedin(string token);
    }

进行重构…..

经过分析 第一步 先根据功能来划分将IObject接口拆分成三个“小”接口:

  1. 数据库操作相关的抽取到一个接口中(IDatabaseProvider)。
  2. 第三方API调用相关的方法抽取到一个接口中(IThirdpartyAPIProvider)。
  3. 第三方登陆相关的方法抽取到一个接口中(IThirdpartyAuthenticationProvider)。
    public interface IDatabaseProvider
    {
        SqlDataReader ExcuteSql(string sql);
        string LoginWithQQ(string token);
    }

    public interface IThirdpartyAPIProvider
    {
        string Get(string url, string token);
    }

    public interface IThirdpartyAuthenticationProvider
    {
        string LoginWithQQ(string token);
        string LoginWithWeibo(string token);
        string LoginWithWeiXin(string token);
        string LoginWithAlipay(string token);
        string LoginWithTwitter(string token);
        string LoginWithFaceBook(string token);
        string LoginWithRenRen(string token);
        string LoginWithBaidu(string token);
        string LoginWithDropbox(string token);
        string LoginWithGithub(string token);
        //这里省略10000字
        string LoginWithLinkedin(string token);
    }

第二步 我们可以将第三方登录的接口中的LogigWithxxx方法提到一个单独的接口中,其他具体站点的接口再继承这个接口,代码如下:

public interface IThirdpartyAuthenticationProvider
    {
        string Login(string token);
    }

    public interface IQQAuthenticationProvider:IThirdpartyAuthenticationProvider{}
    public interface IWeiboAuthenticationProvider:IThirdpartyAuthenticationProvider{}
    public interface IWeiXinAuthenticationProvider:IThirdpartyAuthenticationProvider{}
    public interface IAlipayAuthenticationProvider:IThirdpartyAuthenticationProvider{}
    public interface ITwitterAuthenticationProvider:IThirdpartyAuthenticationProvider{}
    public interface IFaceBookAuthenticationProvider:IThirdpartyAuthenticationProvider{}
    public interface IRenRenAuthenticationProvider:IThirdpartyAuthenticationProvider{}
    public interface IBaiduAuthenticationProvider:IThirdpartyAuthenticationProvider{}
    public interface IDropboxAuthenticationProvider : IThirdpartyAuthenticationProvider { }
    public interface IGitHubAuthenticationProvider:IThirdpartyAuthenticationProvider{}
    //这里省略10000字
    public interface ILinkedinAuthenticationProvider : IThirdpartyAuthenticationProvider { }

(五)经验

一个接口只服务于一个子模块或者业务逻辑
通过业务逻辑亚索接口中的public方法,通过精简接口的方式不断完善接口。
已经被污染的接口,尽量去修改。
接口的实现类要尽量少实现不需要的方法。

>

6.迪米特法则

(一)定义

一个对象应该对其他对象保持最少的了解。

也就是说
从被依赖者的角度来说:只暴露应该暴露的方法或者属性,即在编写相关的类的时候确定方法/属性的权限

从依赖者的角度来说,只依赖应该依赖的对象

迪米特法则主要包含一下四层意思

1.之和朋友交流

  只和直接的朋友通信,什么叫做直接的朋友呢?每个对象都必然会和其他对象有耦合关系,两个对象之间的耦合就成为朋友关系,这种关系有很多比如组合、聚合、依赖等等。

示例 老师想让体育委员确认全班女生人数。

类图:
设计模式之设计原则(整理,非原创)

//teacher 类代码
public class Teacher { 
    //老师对学生发布命令,清一下女生 
    public void commond(GroupLeader groupLeader){ 
        List<Girl> listGirls = new ArrayList(); 
        //初始化女生 
        for(int i=0;i<20;i++){ 
            listGirls.add(new Girl()); 
        } 
        //告诉体育委员开始执行清查任务 
        groupLeader.countGirls(listGirls); 
    } 
}
//老师就有一个方法, 发布命令给体育委员, 去清查一下女生的数量。
public class GroupLeader { 
    //有清查女生的工作 
    public void countGirls(List<Girl> listGirls){ 
        System.out.println("女生数量是:"+listGirls.size()); 
    } 
} 
//业务调用类
public class Client { 
    public static void main(String[] args) { 
        Teacher  teacher= new Teacher(); 
        //老师发布命令 
        teacher.commond(new GroupLeader()); 
    } 
} 
//运行的结果如下:
女生数量是:20

  我们回过头来看这个程序有什么问题,首先来看 Teacher有几个朋友,就一个 GroupLeader 类,这个就是朋友类,朋友类是怎么定义的呢?出现在成员变量、方法的输入输出参数中的类被称为成员朋友类,迪米特法则说是一个类只和朋友类交流, 但是commond 方法中我们与 Girl 类有了交流,声明了一个List动态数组,也就是与一个陌生的类 Girl 有了交流,这个不好,那我们再来修改一下,类图还是不变,先修改一下 GroupLeader 类,看代码:

//GroupLeader类
public class GroupLeader { 
    //有清查女生的工作 
    public void countGirls(){ 
        List<Girl> listGirls = new ArrayList<Girl>(); 
        //初始化女生 
        for(int i=0;i<20;i++){ 
            listGirls.add(new Girl()); 
        } 
        System.out.println("女生数量是:"+listGirls.size()); 
    } 
} 
//Teacher类
public class Teacher { 
    //老师对学生发布命令,清一下女生 
    public void commond(GroupLeader groupLeader){ 
    //告诉体育委员开始执行清查任务 
    groupLeader.countGirls(listGirls); 
    } 

  程序做了一个简单的修改,就是把 Teacher 中的对 List初始化(这个是有业务意义的,产生出全班的所有人员)移动到了 GroupLeader的 countGrils 方法中,避开了 Teacher 类对陌生类 Girl的访问,减少系统间的耦合。 记住了, 一个类只和朋友交流, 不与陌生类交流, 不要出现 getA().getB().getC().getD()这种情况(在一种极端的情况下是允许出现这种访问:每一个点号后面的返回类型都相同) ,那当然还要和JDK API 提供的类交流,否则你想脱离编译器存在呀!

2.朋友之间也是有距离的

  人和人之间是有距离的,太远就不是朋友了,太近就浑身不自在,这和类间关系也是一样,即使朋友类也不能无话不说,无所不知。大家在项目中应该都碰到过这样的需求:调用一个类,然后必须是先执行第一个方法,然后是第二个方法,根据返回结果再来看是否可以调用第三个方法,或者第四个方法等等,我们用类图表示一下:
设计模式之设计原则(整理,非原创)

Wizard 的代码:


public class Wizard { 
    private Random rand = new Random(System.currentTimeMillis()); 
    //第一步  
    public int first(){ 
        System.out.println("执行第一个方法..."); 
        return rand.nextInt(100); 
    } 
    //第二步 
    public int second(){ 
        System.out.println("执行第二个方法..."); 
        return rand.nextInt(100); 
    } 
    //第三个方法 
    public int third(){ 
        System.out.println("执行第三个方法..."); 
        return rand.nextInt(100); 
    } 
}

再来看软件安装过程 InstallSoftware 代码:

public class InstallSoftware { 

    public void installWizard(Wizard wizard){ 
        int first = wizard.first();   
        //根据first返回的结果,看是否需要执行second 
        if(first>50){ 
            int second = wizard.second(); 
            if(second>50){ 
                int third = wizard.third(); 
                if(third >50){ 
                    wizard.first(); 
                }  
            }  
        } 
    } 
}

其中 installWizard 就是一个向导式的安装步骤,我们看场景是怎么调用的:

public class Client { 
    public static void main(String[] args) { 
        InstallSoftware invoker = new InstallSoftware(); 
        invoker.installWizard(new Wizard()); 
    } 
} 

  这个程序很简单,运行结果和随机数有关,我就不粘贴上来了。我们想想这个程序有什么问题吗?Wizard 类把太多的方法暴露给 InstallSoftware类了,这样耦合关系就非常紧了,我想修改一个方法的返回值,本来是 int 的,现在修改为 boolean,你看就需要修改其他的类,这样的耦合是极度不合适的,迪米特法则就要求类“小气”一点,尽量不要对外公布太多的public方法和非静态的public 变量,尽量内敛,多使用private,package-private、protected 等访问权限。我们来修改一下类图:
设计模式之设计原则(整理,非原创)
再来看一下程序的变更,先看 Wizard 程序:

public class Wizard { 
    private Random rand = new Random(System.currentTimeMillis()); 
    //第一步 
    private int first(){ 
        System.out.println("执行第一个方法..."); 
        return rand.nextInt(100); 
    } 
    //第二步 
    private int second(){ 
        System.out.println("执行第二个方法..."); 
        return rand.nextInt(100); 
    } 
    //第三个方法 
    private int third(){ 
        System.out.println("执行第三个方法..."); 
        return rand.nextInt(100); 
    } 
//软件安装过程   
    public void installWizard(){     
        int first = this.first();   
        //根据first返回的结果,看是否需要执行second 
        if(first>50){ 
            int second = this.second(); 
            if(second>50){ 
                int third = this.third(); 
                if(third >50){ 
                    this.first(); 
                }  
            }  
        } 
    } 
}

个步骤的访问权限修改为 private,同时把 installeWizad移动的 Wizard 方法中,这样 Wizard 类就对外只公布了一个 public 方法,类的高内聚特定显示出来了。我们再来看 InstallSoftware 源码:

public class InstallSoftware { 
    public void installWizard(Wizard wizard){ 
        //不废话,直接调用 
        wizard.installWizard(); 
    } 
} 

  Client 类没有任何改变,就不在拷贝了,这样我们的程序就做到了弱耦合,一个类公布越多的 public属性或方法,修改的涉及面也就越大,也就是变更引起的风险就越大。因此为了保持朋友类间的距离,你需要做的是:减少 public 方法,多使用 private、package-private(这个就是包类型,在类、方法、变量前不加访问权限,则默认为包类型)protected等访问权限,减少非 static 的public 属性,如果成员变量或方法能加上 final 关键字就加上,不要让外部去改变它。

3.是自己的就是自己的

  在项目中有一些方法,放在本类中也可以,放在其他类中也没有错误,那怎么去衡量呢?你可以坚持这样一个原则:如果一个方法放在本类中,即不增加类间关系,也对本类不产生负面影响,就放置在本类中。

4.谨慎使用 Serializable

  实话说,这个问题会很少出现的,即使出现也会马上发现问题。是怎么回事呢?举个例子来说,如果你使用 RMI 的方式传递一个对象 VO(Value? Object) ,这个对象就必须使用 Serializable接口,也就是把你的这个对象进行序列化,然后进行网络传输。突然有一天,客户端的VO 对象修改了一个属性的访问权限,从 private 变更为 public 了,如果服务器上没有做出响应的变更的话,就会报序列化失败。这个应该属于项目管理范畴,一个类或接口客户端变更了,而服务端没有变更,那像话吗?!

(二)目的

迪米特法则就是为了降低类之间的耦合度,减少每个类之间不必要的依赖。

由于类与类之间的关系越密切,耦合度越大,当一个类发生改变时,对另一个类的影响也就越大。

(三)用法总结 ###

  1. 在类的划分上,应当创建松耦合的类,类之间的耦合度越低,就越有利于复用,一个处在松耦合中的类被修改后也不会对与之相关联的类产生不好的影响。
  2. 在类的结构设计上,每一个类都应当降低其成员变量和成员函数的访问权限。
  3. 在类的设计上,只要有可能,一个类应当设计成不变类。
  4. 在对其他类的应用上,一个对象对其他对象的引用应当降到最低。

7.合成复用原则

(一) 定义

  在面向对象设计中,如果直接继承基类,会破坏封装,因为继承将基类的实现细节暴露给子类;如果基类的实现发生改变,则子类的实现也不得不发生改变;从基类继承而来的实现是静态的,不可能在运行时发生改变,没有足够的灵活性。于是就提出了合成/聚合复用原则,也就是在实际开发设计中,尽量使用合成/聚合,不要使用类继承。

也就是说,尽量使用合成\聚合,尽量不要使用类继承。

(二) 用法 

  将已有的对象纳入到新的对象中,使之成为新对象的一部分。让新的对象可以调用已有对象的功能。

组合复用原则我们应该首选组合,然后才是继承,使用继承时应该严格的遵守里氏替换原则,必须满足“Is-A”的关系是才能使用继承,而组合却是一种“Has-A”的关系。导致错误的使用继承而不是使用组合的一个重要原因可能就是错误的把“Has-A”当成了“Is-A”。

下面看一个例子:

设计模式之设计原则(整理,非原创)

人被继承到雇员,学生,经理子类。而实际上,雇员、学生和经理分别描述一种角色,而人可以同时有几种不同的角色。比如,一个人既然是经理了就一定是雇员,使用继承来实现角色,则只能使用每一个人具有一种角色,这显然是不合理的。错误的原因就是把角色的等级结构和人的等级结构混淆起来,把Has-A的关系误认为是Is-A的关系,通过下面的改正就可以正确的做到这一点。

设计模式之设计原则(整理,非原创)

从上图可以看出,每一个人都可以有一个以上的角色,所以一个人可以同时是雇员又是经理。从这个例子可以看出,当一个类是另一个类的角色时,不应该使用继承描述这种关系。


什么时候使用继承

  当一下条件全部满足时,使用继承关系:

     1. 子类是超类的一个特殊种类,而不是超类的一个角色,也就是区分“Has-A”和“Is-A”.只有“Is-A”关系才符合继承关系,“Has-A”关系应当使用聚合来描述。

     2 .永远不会出现需要将子类换成另外一个类的子类的情况。如果不能肯定将来是否会变成另外一个子类的话,就不要使用继承。

     3 .子类具有扩展超类的责任,而不是具有置换掉或注销掉超类的责任。如果一个子类需要大量的置换掉超类的行为,那么这个类就不应该是这个超类的子类。

参考资料

  1. 设计模式
  2. hfreeman2008开闭原则
  3. 编程茶馆 
  4. hfreeman2008依赖倒置原则
  5. cbf4life 依赖倒置原则
  6. cbf4life 单一职责原则
  7. cbf4life 里氏替换原则
  8. Sleeping-cat 接口隔离原则
  9. 风之源 接口隔离原则
  10. hfreeman2008 接口隔离原则
  11. mjd507 迪米特法则全文
  12. silentjesse 迪米特法则