如何书写一手优雅的代码之2:不得不该掌握的编程六大原则(设计模式)

       在学习设计模式之前,为了不让设计模式显得很僵硬,我们必须先了解一样东西,那就是程序设计模式的六大原则。

       这些原则并非要你完完全全去遵守,只能说是在我们编程设计上能给我们一个很好参照点,只能说在可能的情况下,尽量遵守。

       单一职责原则(六大原则中的小柴犬,听话懂事,人见人爱):描述是每个类只负责单一的功能,切不可太多,并且一个类应当尽量的把一个功能做到极致。

       这里给大家上一个列子,仅供参考:

package com.effective.roles;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
 
public class Calculator {
    public int add()throws IOException{
        File file = new File("C:/mianmian.txt");
        BufferedReader bufferedReader = new BufferedReader(new FileReader(file));
        int a = Integer.valueOf(bufferedReader.readLine());
        int b = Integer.valueOf(bufferedReader.readLine());
        return a + b;
    }
    public static void main(String[] args)throws IOException{
        Calculator calculator = new Calculator();
        System.out.println(calculator.add());
    }
}

       来看上面这个例子,这个方法的作用是从一个文件中读取两行的数字,相加并返回总和。那问题来了,如果要对数字进行减法、乘法、除法等运算呢?

       不能总copy n份然后再修改运算符吧!这个就说明这个类存在多职责问题......存在加减乘除多个运算职责还有读取内容的职责(我的理解)

       所以我们要把它分离出来,单独写一个读取数据,如下:

package com.effective.roles;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;

/**
 * 单独读写文件类
 */
public class Reader {
    int a,b;
    public Reader(String path) throws IOException{
        BufferedReader reader = new BufferedReader(new FileReader(new File(path)));
        a = Integer.valueOf(reader.readLine());
        b = Integer.valueOf(reader.readLine());
    }
    public int getA(){
        return a;
    }
    public int getB(){
        return b;
    }
}
package com.effective.roles;

import java.io.IOException;

/**
 * 单独计算器类
 */
public class Calculator01 {
    public int add(int a,int b){
        return a + b;
    }
    public static void main(String[] args)throws IOException{
        Reader reader = new Reader("C:/mianmian.txt");
        System.out.println(new Calculator01().add(reader.getA(), reader.getB()));

    }
}

       我们将一个类拆分为两个类,这样我们如果有减法、乘法等等,就不用出现那么多重复代码了。

       以上是我对单一职责的一点理解,没有实际用处,只是希望大家能够明白单一职责的意义。个人觉得是六大原则当中最最应该遵守的原则。



       里式替换原则(六大原则中最高冷的小藏獒,兢兢业业,但却不招人喜欢):描述是一个子类应该可以替换掉父类并且可以正常工作。

       那么我们可以这样理解,子类一般不该重新父类的方法,因为父类的方法一般都是对外公布的接口,是具有不可变性的,你不该将一些变化的东西修改掉。

       上述只是通常意义上的说法,很多情况下,我们不必太理解里式替换这个高冷的小藏獒,比如模板方法模式,缺省适配器,装饰器模式等一些设计模式,就完全不搭理这个小藏獒。

       不过,如果你真的遇见了不得不重写父类方法的场景,那么你还是需要考虑,是否真的要把这个类作为子类出现,或者说这样做所换来的是否能弥补你失去的东西,比如子类无法代替父类工作,那么意味着如果你的父类可以在某一个场景工作的很正常,那么你的子类当然也应该可以,否则就会出现下述场景。

       比如我们有某一个类,其实有一个方法,调用了某一个父类的方法。

package com.effective.roles;

/**
 * 某一个类
 */
public class MouClass {
    /**
     * 某一个方法,父类类型
     * @param parent
     */
    public void MouMethod(Parent parent){
        parent.method();
    }
}
package com.effective.roles;

/**
 * 某父类
 */
public class Parent {
    public void method(){
        System.out.println("parent method");
    }
}

       结果我有一个子类把父类的方法给覆盖了,并且抛出了一个异常。

package com.effective.roles;

public class SubClass extends Parent {
    /**
     * 结果某一个子类重写了父类的方法,说不支持该操作
     */
    @Override
    public void method() {
        throw new UnsupportedOperationException();
    }
}

       这个异常是运行时才会产生的,也就是说,我们的MouClass并不知道会出现这种情况,结果就是在我调用下面这段代码的时候,本来我们的思维是Parent都可以传给MouMethod完成我的功能,我的SubClass继承了Parent,当然也可以了,但是最终这个调用会抛出异常。

package com.effective.roles;

public class Client {
    public static void main(String[] args){
        MouClass mouClass = new MouClass();
        mouClass.MouMethod(new Parent());
        mouClass.MouMethod(new SubClass());
    }
}

       这就相当于了一个陷阱,因为本来我们的原则是,父类可以完成的地方,我用子类替代是绝对没有问题的,但是这下反了,我每次使用一个子类替换一个父类的时候,我还要担心这个子类有没有给我埋一个上面这种炸弹。

       所以里式替换原则是一个需要我们深刻理解的原则,因为往往有时候违反它我们可以得到很多,失去了一小部分,但是有时候却会相反,所以要想做到活学活用,就要深刻理解这个原则的意义所在。

       


       接口隔离原则(六大原则当中最挑剔的哈士奇-二哈,总是挑三拣四,不按套路出牌):描述也称接口最小化原则,强调的是一个接口拥有的行为应该尽可能的小。

       如果你做不到这一点你经常会发现这样的状况,一个类实现了一个接口,里面很多方法都是空着的,只有个别几个方法实现了。这样会给使用者造成假象,即这个实现类拥有接口中所有的行为,结果调用方法却没收获想要的结果。

       比如我们设计一个手机的接口时,就要手机哪些行为是必须的,要让这个接口尽量的小,或者通俗点讲,就是里面的行为应该都是这样一种行为,就是说只要手机,你就必须可以做到。

       上面就是接口隔离原则这个挑剔的二哈所挑剔的地方,假设你没有满足它,你或许会写出下面这样的手机接口。

package com.effective.roles;

public interface MobilePhone {
    public void call(); //打电话
    public void sendMessage(); //发短信
    public void playWangZheRongYao(); //玩王者荣耀
}

       上面第三个行为明显就不是每个手机都必须有的,那么上面这个手机的接口就不是最小接口,假设我现在的非智能手机去实现这个接口,那么playWangZheRongYao方法就只能空着了,因为它不能玩。

       所以我们更好的做法就是去掉这个方法,让MobilePhone接口最小化,然后再建立下面这个接口去扩展现有的MobilePhone接口。

package com.effective.roles;

public interface SmartPhone extends MobilePhone {
    public void playWangZheRongYao(); //玩王者荣耀
}

       这样两个接口就是最小化的了,这样我们的非智能手机就去实现MobilePhone接口,实现打电话和发短信的功能了,而智能手机就实现SmartPhone接口,实现打电话、发短信以及玩王者荣耀的功能,两者都不会有多余的要实现的方法。

       最小接口原则一般我们是尽量满足的,如果实在有多余的方法,我们也有补救的办法,而且有的时候也确实不可避免有一些实现类无法全部实现接口中的方法,这时候就轮到缺省适配器上场了,这个在后面再介绍。



       依赖倒置原则(六大原则中最安静、乖巧的比熊犬,对抽象的东西非常依赖):描述这个原则描述是高层模块不该依赖于低层模块,二者都应该依赖于抽象,抽象不应该依赖于细节,细节应该依赖于抽象。

       上面这段原版描述,我大致理解为:描述的是一个现实当中的事实,即实现都是易变得,而只有抽象是稳定的,所以当依赖于抽象时,实现的变化并不会影响客户端的调用。

        比如上述的计算器的例子,我们的计算器其实是依赖于数据读取类的,这样做并不是很好,因为如果我的数据不是文件里的了,而是在数据库里,这样的话,为了不影响你现有的代码,你就只能将你的Reader类整个改头换面。 

        或者还有一种方式就是,你再添加一个DBReader类,然后把你所有使用Reader读取的地方,全部手动替换成DBReader,这样其实也还可以接受,那假设我有的从文件读取,有的从数据库读取,有的从XML文件读取,有的从网络中读取,有的从标准的键盘输入读取等等。

        你想怎么办嘞?

        所以我们最好的做法就是抽象一个类或者接口,来表达数据读取的行为,然后让上面所有的读取方式所实现的类都实现这个接口,而我们的客户端,只使用我们定义好的接口,当我们的实现变化时,我只需要设置不同的实际类型就可以了,这样对于系统的扩展性是一个大大的提升。

       针对上面简单的数据读取,我们可以定义如下接口去描述。

package com.effective.roles;

public interface Reader01 {
    public void getA();
    public void getB();
}

       让我们原来的Reader改名为FileReader去实现这个接口,这样计算器就依赖于抽象的接口,这个依赖是非常稳定的,因为不论你以后要从哪读取数据,你的两个获取数据的方法永远都不会变。

       这样,我们让DBReader、XMLReader、NETReader等等,都可以实现Reader这个接口,而我们的客户端调用依赖于一个Reader,这样不管数据是从哪来的,我们都可以应对自如,因为我根本不关心你是什么Reader,我只知道你能让我获得A和B这两个值就行了。

       这便是我们依赖于抽象所得到的灵活性,这也是Java语言的动态特性给我们带来的便利,所以我们一定要好好珍惜这个依赖于抽象的乖巧可爱的比熊犬。



       迪米克原则(六大原则中最爱卖萌、最害羞的萨摩耶-小萨,不爱和陌生人说话):描述也称最小知道原则,即一个类应该尽量不要知道其他类太多的东西,不要和陌生的类有太多的接触。

       这个原则的制定,是因为如果一个类知道或者是依赖于另外一个类太多的细节,这样会耦合度过高,应该将细节全部高内聚于类的内部,其他的类只需要知道这个类提供的功能即可。

       所谓高内聚就是尽可能将一个类的细节实现全部写在这个类的内部,不要漏出来给其他类知道,否则其他类就很容易依赖于这些细节,这样类之间的耦合度就会急速上升,这样做的后果往往是一个类随便改点东西,依赖于它的类全部都要改。

       比如我把上述例子改变一下。

package com.effective.roles;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;

/**
 * 单独读写文件类
 */
public class Reader {
    int a,b;
    private String path;
    private BufferedReader bufferedReader;
    public Reader(String path){
        this.path = path;
    }
    public void setBufferedReader()throws IOException{
        bufferedReader = new BufferedReader(new FileReader(new File(path)));
    }
    public void readLine()throws IOException{
        a = Integer.valueOf(bufferedReader.readLine());
        b = Integer.valueOf(bufferedReader.readLine());
    }
    public int getA(){
        return a;
    }
    public int getB(){
        return b;
    }
}

       Reader类改成上述这样显然它给其他的类透漏了太多的细节,让别人知道了它的太多细节,这样我客户端调用的时候就很可能写成如下形式。

package com.effective.roles;

import java.io.IOException;

public class Client {
    public static void main(String[] args)throws IOException{
        Reader reader = new Reader("C:/mianmian.txt");
        reader.setBufferedReader();
        reader.readLine();
        int a = reader.getA();
        int b = reader.getB();
        //以下用于计算,略
    }
}

       这样客户端就依赖于reader的多个行为才能最终获取到A和B两个数值,这时候两个类的耦合度就太高了,我们更好的做法使用访问权限限制将两者都给隐藏起来不让外部调用的类知道这些,就像下面这样。

package com.effective.roles;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;

/**
 * 单独读写文件类
 */
public class Reader {
    int a,b;
    private String path;
    private BufferedReader bufferedReader;
    public Reader(String path)throws IOException{
        this.path = path;
        setBufferedReader();
        readLine();
    }
    //这里变为私有的方法
    private void setBufferedReader()throws IOException{
        bufferedReader = new BufferedReader(new FileReader(new File(path)));
    }
    //这里变为私有的方法
    private void readLine()throws IOException{
        a = Integer.valueOf(bufferedReader.readLine());
        b = Integer.valueOf(bufferedReader.readLine());
    }
    public int getA(){
        return a;
    }
    public int getB(){
        return b;
    }
}

       我们最终将两个方法都变为私有封装在Reader类当中,这样外部的类就无法知道这两个方法了,所以迪米特原则虽说是指一个类应当尽量不要知道其他类太多细节,但其实更重要的是一个类应当不要让外部的类知道自己太多,两者相辅相成,只要将类的封装做得很好,那么外部的类将无法依赖当中的细节。



       开-闭原则(六大原则中绝对的狗中贵族-沙漠死神内瑟斯):描述一个原则,一句话,对修改关闭,对扩展开放。

       就是说我任何的改变都不需要修改原有的代码,而只需要加入一些新的实现,就可以达到我的目的,这是系统设计的理想境界,但是没有任何一个系统可以做到这一点,哪怕我一直最欣赏的spring框架也做不到,虽说它的扩展性已经强到变态。

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



       最后,在大话设计模式中,提到了一句话与君共勉,用抽象构建框架,用细节实现扩展。祝大家狗年行大运。汪...汪...汪......


你可能感兴趣的:(Java设计模式)