美文网首页
开篇:设计模式六大原则

开篇:设计模式六大原则

作者: 抓兔子的猫 | 来源:发表于2016-10-31 17:21 被阅读52次

    设计模式也反复看了一段日子了,但是疏于使用的话,时间久了还是会弄混其中的一些模式,为了加深理解,特此开设专题进行总结,主要是方便自己查阅,所以会比较简练,就不投稿了,无意中进来的小伙伴们看不明白的话也请原谅。


    先简单粗暴的罗列一下概念

    1.单一职责原则描述的意思是每个类都只负责单一的功能,切不可太多,并且一个类应当尽量的把一个功能做到极致

    2.里氏替换原则这个原则表达的意思是一个子类应该可以替换掉父类并且可以正常工作。(这个最难理解,需仔细体会)

    3.接口隔离原则也称接口最小化原则,强调的是一个接口拥有的行为应该尽可能的小

    4.依赖倒置原则这个原则描述的是高层模块不该依赖于低层模块,二者都应该依赖于抽象,抽象不应该依赖于细节,细节应该依赖于抽象

    5.迪米特原则也称最小知道原则,即一个类应该尽量不要知道其他类太多的东西,不要和陌生的类有太多接触

    6.开-闭原则最后一个原则,一句话,对修改关闭,对扩展开放

    下面说一下对各个原则的理解。
    单一职责 原则,从名字就可以看出来,就是使职责单一化,只负责干一件事情,可以联系到国家政府机构的划分

    Paste_Image.png

    大体分了如图这些机构,每个机构又划分为很多个科室,每个科室中的人也分管不同的工作,这样设计的目的也是使职责单一化,提高运作效率。如果一个人负责的工作过多,不但增加了这个人的负担而且也更容易出错,想找这个人办事儿的人太多,也提高了复杂度和耦合。反观到java的类中也是如此,所以单一职责原则是我觉得六大原则当中最应该遵守的原则。

    里氏替换原则简单说来就是子类可以扩展父类的功能,但不能改变父类原有的功能。
    它包含以下4层含义:
    1 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。
    2 子类中可以增加自己特有的方法。
    3 当子类的方法重载父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。
    4 当子类的方法重载父类的方法时,方法的后置条件(即方法的返回值)要比父类更严格。

    里氏替换原则的关键点在于不能覆盖父类的非抽象方法。父类中凡是已经实现好的方法,实际上是在设定一系列的规范和契约,虽然它不强制要求所有的子类必须遵从这些规范,但是如果子类对这些非抽象方法任意修改,就会对整个继承体系造成破坏。而里氏替换原则就是表达了这一层含义。
    关于第3点做一下说明:
    简单理解:为什么是放大?因为父类方法的参数类型相对较小,所以当传入父类方法的参数类型,重载的时候优先匹配父类的方法,而子类的重载方法不会匹配,因此仍保证执行父类的方法(子类继承的时候其实操作的是子类中的父类成分),所以业务逻辑不会改变。

    关于第4点做一下说明:
    简单理解:如果是重载,由于前置条件的要求,会调用到父类的函数,因此子函数不会被调用。
    如果是覆盖,则调用子类的函数,这时子类的返回值比父类要求的小。因为父类调用函数的时候,返回值的类型是父类的类型,而子类的返回值更小,赋值合法。
    Father F = ClassF.Func();//;用子类替换时Father F = ClassC.Func()是合法的 子类赋值父类转是合法的,父类赋值给子类是不合法的。

    方法中的输入参数称为前置条件,这是什么意思呢?大家做过Web Service开发就应该知道有一个“契约优先”的原则,也就是先定义出WSDL接口,制定好双方的开发协议,然后再各自实现。里氏替换原则也要求制定一个契约,就是父类或接口,这种设计方法也叫做Design by Contract,契约设计,是与里氏替换原则融合在一起的。契约制定了,也就同时制定了前置条件和后置条件,前置条件就是你要让我执行,就必须满足我的条件;后置条件就是我执行完了需要反馈,标准是什么。这个比较难理解,使用一个网友的例子

    public class Father {
        public void func(HashMap m){
            System.out.println("执行父类...");
        }
    }
    
    import java.util.Map;
    public class Son extends Father{
        public void func(Map m){//方法的形参比父类的更宽松
            System.out.println("执行子类...");
        }
    }
    
    import java.util.HashMap;
    public class Client{
        public static void main(String[] args) {
            //引用基类的地方能透明地使用其子类的对象。
            //Father f = new Father();
            Son f = new Son();  
            HashMap h = new HashMap();
            f.func(h);
        }
    }
    
    输出  :执行父类...
    

    ps:这里引申一个Java重载方法匹配优先级问题
    写出以下程序的输出:

    public class Overload {
        
        public static void say(long arg) {
            System.out.println("hello long");
        }
        
        public static void say(Character arg) {
            System.out.println("hello character");
        }
        
        public static void say(char... arg) {
            System.out.println("hello char...");
        }
        
        // Serializable 参数
        public static void say(Serializable arg) {
            System.out.println("hello serializable");
        }
        
        
        public static void main(String[] args) {
            say('a');
        }
    
    }
    

    答案: hello long
    这条题目考的是重载方法匹配的优先级,那么它的匹配优先级是怎样的呢?
    我们可以扩充一下这个程序,加入一些其他的参数,然后测试一下:

    public class Overload {
    
        // Object 参数
        public static void say(Object arg) {
            System.out.println("hello object");
        }
        
        
        // int 参数
        public static void say(int arg) {
            System.out.println("hello int");
        }
        
        // long 参数 
        public static void say(long arg) {
            System.out.println("hello long");
        }
        
        // char 参数
        public static void say(char arg) {
            System.out.println("hello char");
        }
        
        // Character 参数
        public static void say(Character arg) {
            System.out.println("hello character");
        }
        
        // 变长参数
        public static void say(char... arg) {
            System.out.println("hello char...");
        }
        
        // Serializable 参数
        public static void say(Serializable arg) {
            System.out.println("hello serializable");
        }
        
        
        public static void main(String[] args) {
                   say('a');
        }
    
    }
    
    

    如果直接运行的话,毫无疑问,输出为: hello char
    如果将char参数的函数注释之后,会输出什么呢?
    答案是:hello int
    因为这期间,字符a发生了一次自动转型,它除了能够表示字符a外,还能表示数字65,于是重载方法匹配了int参数的重载方法。
    现在我们再将这个方法注释了,输出的结果大家应该知道是什么了吧?
    那就是:hello long
    原因就是int自动转型为long。其实还可以转化为float和double的,但不能转化为byte和short,因为char到这两个类型的转化是不安全的,这几个类型的转化优先级为:char->int->long->float->double。
    好,我们再继续注释掉这个函数,然后输出是什么呢?
    答案:hello character
    为什么?大家应该知道Java里面为每种基本数据类型都提供一种封装类型吧?char对应的就是Character,所以调用函数期间,当找不到基本类型转化的匹配之后,char就会发生一次自动装箱,变成了Character类型。
    根本停不下来啊,再继续注释了它,看下输出。
    输出:hello serializable
    这什么东西嘛。。。怎么会输出这个家伙啊。。。。原来是因为Character实现了Serializable接口,当它找不到匹配的类型之后,就会找它所实现的接口。但是,如果我们再增加一个重载函数:

    public static void say(Comparable arg) { 
        System.out.println("hello Comparable");
    }
    

    那么就会报错了, 因为Character实现了Serializable和Comparable这两个接口,而接口匹配的优先级是一样的,编译器无法判断转型为哪种类型,提示类型模糊,拒绝编译。
    好,继续注释掉Serializable参数的函数,看输出:hello object
    接口找不到匹配之后,就会开始找匹配的父类,优先级是顺着继承链,由下往上进行匹配。
    最后,连这个函数也注释了的话,大家应该知道输出的是什么了吧?
    当然就是:hello char...
    由此可见,变长参数的优先级是最低的。


    接口隔离原则 讲一个极端点的例子,平常很多人常说,原来上学的时候学的那些个东西有啥用,生活中也用不到,先不说这种观点正确与否,我们以这种想法去想象一下,如果把人所具有的技能定义为一个接口,以上的观点就是往这个接口中定义了 语文 英语 数学 物理 化学 等等方法,有的人可能一辈子没学过英语,那么这个方法对于这些人来说就是个不必有实现的方法,但接口这偏偏定义了,那可能就会屈就的实现成一个空方法,这还没什么,最严重的是会给使用者造成假象,即这个实现类拥有接口中所有的行为,结果调用方法时却没收获到想要的结果。你和他说hello,他也不会跟你回答 world!
    所以定义接口的时候,其中的方法要是实现这个接口的类所共有的才好。


    依赖倒置原则 相对于细节的多变性,抽象的东西要稳定的多。以抽象为基础搭建起来的架构比以细节为基础搭建起来的架构要稳定的多。在java中,抽象指的是接口或者抽象类,细节就是具体的实现类,使用接口或者抽象类的目的是制定好规范和契约,而不去涉及任何具体的操作,把展现细节的任务交给他们的实现类去完成。

    class Newspaper implements IReader {
     public String getContent(){
     return "林书豪17+9助尼克斯击败老鹰……";
     }
    }
    class Book implements IReader{
     public String getContent(){
     return "很久很久以前有一个阿拉伯的故事……";
     }
    }
     
    class Mother{
     public void narrate(IReader reader){
     System.out.println("妈妈开始讲故事");
     System.out.println(reader.getContent());
     }
    }
     
    public class Client{
     public static void main(String[] args){
     Mother mother = new Mother();
     mother.narrate(new Book());
     mother.narrate(new Newspaper());
     }
    }
    

    迪米特原则应该将细节全部高内聚于类的内部,其他的类只需要知道这个类主要提供的功能即可。 所谓高内聚就是尽可能将一个类的细节全部写在这个类的内部,不要漏出来给其他类知道,否则其他类就很容易会依赖于这些细节,这样类之间的耦合度就会急速上升,这样做的后果往往是一个类随便改点东西,依赖于它的类全部都要改。
    迪米特原则虽说是指的一个类应当尽量不要知道其他类太多细节,但其实更重要的是一个类应当不要让外部的类知道自己太多。两者是相辅相成的,只要你将类的封装性做的很好,那么外部的类就无法依赖当中的细节。

    开闭原则 这个原则更像是前五个原则的总纲,前五个原则就是围着它转的,只要我们尽量的遵守前五个原则,那么设计出来的系统应该就比较符合开闭原则了,相反,如果你违背了太多,那么你的系统或许也不太遵循开闭原则。

    参考:
    设计模式详解(总纲)
    里氏替换原则
    第3章 里氏替换原则(LSP)

    相关文章

      网友评论

          本文标题:开篇:设计模式六大原则

          本文链接:https://www.haomeiwen.com/subject/aalputtx.html