设计模式:六大设计原则

前言

设计模式
  面向对象语言开发过程中,遇到种种的场景和问题,提出的解决方案和思路,沉淀下来,设计模式是解决具体问题的套路

六大原则
  面向对象语言开发过程中,推荐的一些指导性原则;没有明确的招数,而且也会经常被忽视/违背;也是前辈总结,也是为了站在前辈的肩膀上。

设计模式六大原则:

  1. 单一职责原则(Single Responsibility Principle)
  2. 开闭原则(Open Closed Principle)
  3. 里氏替换原则(Liskov Substitution Principle)
  4. 依赖倒置原则(Dependence Inversion Principle)
  5. 接口隔离原则(Interface Segregation Principle)
  6. 迪米特法则(Law Of Demeter)//最少知道原则

单一职责原则(SRP)

就一个类而言,应该仅有一个引起它变化的原因。

问题由来:类T负责两个不同的职责:职责P1,职责P2。当由于职责P1需求发生改变而需要修改类T时,有可能会导致原本运行正常的职责P2功能发生故障。

解决方案:遵循单一职责原则。分别建立两个类T1、T2,使T1完成职责P1功能,T2完成职责P2功能。这样,当修改类T1时,不会使职责P2发生故障风险;同理,当修改T2时,也不会使职责P1发生故障风险。

说到单一职责原则,很多人都会不屑一顾。因为它太简单了。稍有经验的程序员即使从来没有读过设计模式、从来没有听说过单一职责原则,在设计软件时也会自觉的遵守这一重要原则,因为这是常识。在软件编程中,谁也不希望因为修改了一个功能导致其他的功能发生故障。而避免出现这一问题的方法便是遵循单一职责原则。虽然单一职责原则如此简单,并且被认为是常识,但是即便是经验丰富的程序员写出的程序,也会有违背这一原则的代码存在。为什么会出现这种现象呢?因为有职责扩散。所谓职责扩散,就是因为某种原因,职责P被分化为粒度更细的职责P1和P2。

比如:类T只负责一个职责P,这样设计是符合单一职责原则的。后来由于某种原因,也许是需求变更了,也许是程序的设计者境界提高了,需要将职责P细分为粒度更细的职责P1,P2,这时如果要使程序遵循单一职责原则,需要将类T也分解为两个类T1和T2,分别负责P1、P2两个职责。但是在程序已经写好的情况下,这样做简直太费时间了。所以,简单的修改类T,用它来负责两个职责是一个比较不错的选择,虽然这样做有悖于单一职责原则。(这样做的风险在于职责扩散的不确定性,因为我们不会想到这个职责P,在未来可能会扩散为P1,P2,P3,P4……Pn。所以记住,在职责扩散到我们无法控制的程度之前,立刻对代码进行重构。)

举例说明,用一个类描述动物呼吸这个场景:

class Animal{
    public void breathe(String animal){
        System.out.println(animal+"呼吸空气");
    }
}
public class Client{
    public static void main(String[] args){
        Animal animal = new Animal();
        animal.breathe("牛");
        animal.breathe("羊");
        animal.breathe("猪");
    }
}

运行结果如下:

牛呼吸空气
羊呼吸空气
猪呼吸空气

程序上线后,发现问题了,并不是所有的动物都呼吸空气的,比如鱼就是呼吸水的。修改时如果遵循单一职责原则,需要将Animal类细分为陆生动物类Terrestrial,水生动物Aquatic,代码如下:

class Terrestrial{
    public void breathe(String animal){
        System.out.println(animal+"呼吸空气");
    }
}
class Aquatic{
    public void breathe(String animal){
        System.out.println(animal+"呼吸水");
    }
}

public class Client{
    public static void main(String[] args){
        Terrestrial terrestrial = new Terrestrial();
        terrestrial.breathe("牛");
        terrestrial.breathe("羊");
        terrestrial.breathe("猪");

        Aquatic aquatic = new Aquatic();
        aquatic.breathe("鱼");
    }
}

可以看到,这种修改方式没有改动原来的方法,而是在类中新加了一个方法,这样虽然也违背了单一职责原则,但在方法级别上却是符合单一职责原则的,因为它并没有动原来方法的代码。这三种方式各有优缺点,那么在实际编程中,采用哪一中呢?其实这真的比较难说,需要根据实际情况来确定。我的原则是:只有逻辑足够简单,才可以在代码级别上违反单一职责原则;只有类中方法数量足够少,才可以在方法级别上违反单一职责原则;

例如本文所举的这个例子,它太简单了,它只有一个方法,所以,无论是在代码级别上违反单一职责原则,还是在方法级别上违反,都不会造成太大的影响。实际应用中的类都要复杂的多,一旦发生职责扩散而需要修改类时,除非这个类本身非常简单,否则还是遵循单一职责原则的好。

遵循单一职责原的优点有:

  • 可以降低类的复杂度,一个类只负责一项职责,其逻辑肯定要比负责多项职责简单的多;
  • 提高类的可读性,提高系统的可维护性;
  • 变更引起的风险降低,变更是必然的,如果单一职责原则遵守的好,当修改一个功能时,可以显著降低对其他功能的影响。

开闭原则(OCP)

软件中的对象(类、模块、函数等)应该对于扩展是开放的,但是,对于修改是封闭的。

一个软件产品只要在生命周期内,都会发生变化,即然变化是一个事实,我们就应该在设计时尽量适应这些变化,以提高项目的稳定性和灵活性,真正实现“拥抱变化”。开闭原则告诉我们应尽量通过扩展软件实体的行为来实现变化,而不是通过修改现有代码来完成变化,它是为软件实体的未来事件而制定的对现行开发设计进行约束的一个原则。

我们举例说明什么是开闭原则,以书店销售书籍为例,其类图如下:
书店销售书籍类图
书籍接口:

public interface IBook{
  public String getName();
  public String getPrice();
  public String getAuthor();
}

小说类书籍:

public class NovelBook implements IBook{
   private String name;
   private int price;
   private String author;

   public NovelBook(String name,int price,String author){
     this.name = name;
     this.price = price;
     this.author = author;
   }

   public String getAutor(){
     return this.author;
   }

   public String getName(){
     return this.name;
   }  

   public int getPrice(){
     return this.price;
   } 
}

Client类:

public class Client{
   public static void main(Strings[] args){
     IBook novel = new NovelBook("笑傲江湖",100,"金庸");
     System.out.println("书籍名字:"+novel.getName()+"书籍作者:"+novel.getAuthor()+"书籍价格:"+novel.getPrice());
   }
}

项目投产生,书籍正常销售,但是我们经常因为各种原因,要打折来销售书籍,这是一个变化,我们要如何应对这样一个需求变化呢?

我们有下面三种方法可以解决此问题:

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

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

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

下面是修改后的类图:
修改后的类图
打折类:

public class OffNovelBook extends NovelBook{
   public OffNovelBook(String name,int price,String author){
      super(name,price,author);
   }
   //覆写价格方法,当价格大于40,就打8析,其他价格就打9析
   public int getPrice(){
     if(this.price > 40){
        return this.price * 0.8;
     }else{
        return this.price * 0.9;
     }     
   } 
}

现在打折销售开发完成了,我们只是增加了一个OffNovelBook类,我们修改的代码都是高层次的模块,没有修改底层模块,代码改变量少,可以有效的防止风险的扩散。

里氏替换原则(LSP)

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

里氏替换原则依赖于继承、多态这两大特性。里氏替换原则简单来说就是,所有引用基类的地方必须能够透明地使用其子类的对象。通俗点讲,只要父类能出现的地方子类就可以出现,而且替换为子类也不会产生任何错误或异常,使用者可能根本就不需要知道是父类还是子类。

    //窗口类
    public class Window{
        public void show(View child){
            child.draw();
        }
    }

    //建立View抽象
    public abstract class View{
        public abstract void draw();
        public void measure(int width,int height){
            //测量视图大小
        }
    }

    //文本控件类的具体实现
    public class TextView extends View{
        @Override
        public void draw() {
            //绘制文本
        }
    }

    //ImageView的具体实现
    public class ImageView extends View{
        @Override
        public void draw() {
            //绘制图片
        }
    }

上述示例中,子类通过覆写View的draw方法实现具有各自特色的功能,在这里,这个功能就是绘制自身的内容。任何继承自View类的子类都可以传递show函数,就是所说的里氏替换。

依赖倒置原则(DIP)

  1. 高层模块不应该依赖底层模块,两者都应该依赖其抽象
  2. 抽象不应该依赖细节,细节应该依赖抽象

依赖倒置原则在Java语言中的表现就是:模块间的依赖通过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系是通过接口或者抽象类产生的
抽象可以是接口或者抽象类的形式,我们看代码:

public interface Driveable {
    void drive();
}

class Bike implements Driveable{
    @Override
    public void drive() {
        // TODO Auto-generated method stub
        System.out.println("Bike drive.");
    }
}

class Car implements Driveable{
    @Override
    public void drive() {
        // TODO Auto-generated method stub
        System.out.println("Car drive.");
    }
}

Driveable 是接口,所以它是抽象,而 Bike 和 Car 实现了接口,它们被称为具体。
在平常的开发中,我们大概都会这样编码。

public class Person {
    private Bike mBike;
    public Person() {
        mBike = new Bike();
    }
    public void chumen() {
        System.out.println("出门了");
        mBike.drive();
    }
}

我们创建了一个 Person 类,它拥有一台自行车,出门的时候就骑自行车。

public class Test1 {
    public static void main(String[] args) {
        Person person = new Person();
        person.chumen();
    }
}

执行结果如下:

出门了
Bike drive.

不过,自行车适应很短的距离。如果,我要出门逛街呢?自行车就不大合适了。于是就要改成汽车。

public class Person {
    private Bike mBike;
    private Car mCar;
    public Person() {
        //mBike = new Bike();
        mCar = new Car();
    }
    public void chumen() {
        System.out.println("出门了");
        //mBike.drive();
        mCar.drive();
    }
}

我们需要修改 Person 这个类的代码。
不过,如果我要到北京去,那么汽车也不合适了。

class Train implements Driveable{
    @Override
    public void drive() {
        System.out.println("Train drive.");
    }
}

package com.frank.test;
public class Person {
    private Bike mBike;
    private Car mCar;
    private Train mTrain;
    public Person() {
        //mBike = new Bike();
        //mCar = new Car();
        mTrain = new Train();
    }
    public void chumen() {
        System.out.println("出门了");
        //mBike.drive();
        //mCar.drive();
        mTrain.drive();
    }
}

我们添加了 Train 这个最新的实现类,然后再次修改了 Person 这个类。
有没有一种方法能让 Person 的变动少一点呢?因为这是最基础的演示代码,如果工程大了,代码复杂了,Person 面对需求变动时改动的地方会更多。
而依赖倒置原则正好适用于解决这类情况。
下面,我们尝试运用依赖倒置原则对代码进行改造。
首先是上层模块和底层模块的拆分。
按照决策能力高低或者重要性划分,Person 属于上层模块,Bike、Car 和 Train 属于底层模块。
上层模块不应该依赖于底层模块。
但是

public class Person {
    private Bike mBike;
    private Car mCar;
    private Train mTrain;
    public Person() {
        //mBike = new Bike();
        //mCar = new Car();
        mTrain = new Train();
    }
}

Person 这个类显然是依赖于 Bike 和 Car。Person 类中 chumen() 的能力完全依赖于属性 Bike 或者 Car 对象,也就是说 Person 把自己的能力依赖在 Bike 和 Car 身上。
上层和底层都应该依赖于抽象。
我们的代码中,Person 没有依赖抽象,所以我们得引进抽象。
而底层的抽象是什么,是 Driveable 这个接口。

public class Person {
//  private Bike mBike;
//  private Car mCar;
//  private Train mTrain;
    private Driveable mDriveable;
    public Person() {
        //mBike = new Bike();
        //mCar = new Car();
        //mTrain = new Train();
        mDriveable = new Train();
    }
    public void chumen() {
        System.out.println("出门了");
        //mBike.drive();
        //mCar.drive();
        //mTrain.drive();
        mDriveable.drive();
    }
}

执行结果如下:

出门了
Train drive.

现在,Person 类中 chumen() 这个方法依赖于 Driveable 接口的抽象,它没有限定自己出行的可能性,任何 Car、Bike 或者是 Train 都可以的。
到这一步,我们可以说是符合了上层不依赖于底层,依赖于抽象的准则了。
那么,抽象不应该依赖于细节,细节应该依赖于抽象又是什么意思呢?
以上面为例,Driveable 是抽象,它代表一种行为,而 Bike、Car、Train 都是实现细节。
Person 需要的是 Driveable,需要的是交通工具,但不是说交通工具一定是 Bike、Car、Train。未来也可能是 AirPlane。

class AirPlane implements Driveable{
    @Override
    public void drive() {
        // TODO Auto-generated method stub
        System.out.println("AirPlane fly.");
    }
}

那么一个 Person,它下次出门改成飞机可以吗?当然可以的。因为依赖倒置的缘由,Person 展现出了极度的可扩展性。

接口隔离原则(ISP)

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

在说接口隔离原则之前,我们先说一个没有使用该原则的例子,然后通过前后对比,来看看有什么不同之处。大家一听到“美女”这个字眼,会想到什么呢?别激动啊,我没啥意思,只是想给美女来定一个通用的标准:面貌,身材与气质,一般来说,长得好看的,身材不错的,气质出众的都可以称为美女。假如我现在是一个星探,下面我要设计一个找美女的类图:
美女类图
定义了一个IPettyGirl接口,声明所有的美女都应该有goodLooking,niceFigure,greatTemperament。还定义了一个抽象类AbstractSearcher,其作用就是搜索美女并显示其信息。下面是接口的定义与实现:

//美女接口
public interface IPettyGirl {
    //要有姣好的面孔
    public void goodLooking();
    //要有好身材
    public void niceFigure();
    //要有气质
    public void greatTemperament();
}

//接口的实现类
public class PettyGirl implements IPettyGirl {
    private String name;
    public PettyGirl(String  name){
       this.name= name;
}
//脸蛋漂亮
public void goodLooking() {
     System.out.println(this.name + "---脸蛋很漂亮!");
}
//气质要好
public void greatTemperament() {
      System.out.println(this.name + "---气质非常好!");
}
//身材要好
public void niceFigure() {
      System.out.println(this.name + "---身材非常棒!");
   }
}

美女有了,就需要星探出马找美女了:

//星探的抽象
public abstract class AbstractSearcher {
    protected IPettyGirl pettyGirl;
    public AbstractSearcher(IPettyGirl pettyGirl){
        this.pettyGirl = pettyGirl;
   }
     //搜索美女, 列出美女信息
    public abstract void show();
}

//实现类
public class Searcher extends AbstractSearcher{
     public Searcher(IPettyGirl pettyGirl){
         super(pettyGirl);
   }
    //展示美女的信息
    public void show(){
        System.out.println("--------美女的信息如下: ---------------");
         //展示面容
        super.pettyGirl.goodLooking();
         //展示身材
        super.pettyGirl.niceFigure();
        //展示气质
        super.pettyGirl.greatTemperament();
      }
}

//下面是客户端代码
public class Client {
      //搜索并展示美女信息
      public static void main(String[] args) {
      //定义一个美女
      IPettyGirl xiaoMei = new PettyGirl("小美");
      AbstractSearcher searcher = new Searcher(yanYan);
      searcher.show();
    }
}

OK,找美女的过程开发完毕,总体来说还是不错的,因为只要按照我们设计的原则来,那么找到的都是美女。但是这样的设计是最优的吗?现在考虑这样一种情况,由于人们审美的提高,或许颜值不一定是我们关注的主要因素,也许某个女生虽然颜值不是太高,但是气质很好,也可以把她称为美女。也有可能某些人喜欢身材匀称的,有的人觉得骨感一点好。也就是是说,美女的定义是可以宽泛话的,并非一成不变的。就如同我们的设计,必须符合我们定好的原则那才是美女,显然这是说不通的。所以上面的设计是有问题的,显然IPrettyGirl这个接口过于庞大了,根据接口隔离原则,星探AbstractSearcher应该依赖于具有部分特质的女孩子,但上面的设计却把这些特质都封装起来,放到一个接口中,这样就造成了封装过度,不容易扩展。

现在我们找到了问题的原因,那就该接口隔离原则上场了。把原IPrettyGirl接口拆分为两个接口,一种是外形美女IGoodBodyGirl(相貌一流,身材不错,但气质可能差点),另一种是气质美女IGreatTemperamentGirl(也许外形条件不出众,但是谈吐优雅得体,气质很好)。下面是设计类图:
设计类图

public interface IGoodBodyGirl {
     //要有姣好的面孔
     public void goodLooking();
     //要有好身材
     public void niceFigure();
}

public interface IGreatTemperamentGirl {
    //要有气质
    public void greatTemperament();
}

public class PettyGirl implements IGoodBodyGirl,IGreatTemperamentGirl {
     private String name;
     public PettyGirl(String _name){
        this.name=_name;
  }
    //脸蛋漂亮
    public void goodLooking() {
        System.out.println(this.name + "---脸蛋很漂亮!");
   }
    //气质要好
    public void greatTemperament() {
       System.out.println(this.name + "---气质非常好!");
   }
   //身材要好
   public void niceFigure() {
        System.out.println(this.name + "---身材非常棒!");
   }
}

OK,现在经过重新设计,程序变得更加灵活,这就是接口隔离原则的强大之处。

ISP的几个使用原则

  • 根据接口隔离原则拆分接口时,首先必须满足单一职责原则:有些设计原则之间就可能出现冲突,就如同单一职责原则和接口隔离原则,一个考虑的是接口的职责的单一性,一个考虑的是方法设计的专业性(尽可能的少),必然是会出现冲突。在出现冲突时,尽量以单一职责为主,当然这也要考虑具体的情况。
  • 提高高内聚: 提高接口,类,模块的处理能力,减少对外的交互。比如你给杀手提交了一个订单,要求他在一周之内杀一个人,一周后杀手完成了任务,这种不讲条件完成任务的表现就是高内聚。具体来说就是:要求在接口中尽量少公布public方法,接口是对外的承诺,承诺越少对系统的开发越有利,变更的风险就越小,也有利于降低成本。
  • 定制服务
  • 接口设计要有限度

迪米特原则(LOD)

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

迪米特法则又叫最少知道原则,最早是在1987年由美国Northeastern University的Ian Holland提出。通俗的来讲,就是一个类对自己依赖的类知道的越少越好。也就是说,对于被依赖的类来说,无论逻辑多么复杂,都尽量地的将逻辑封装在类的内部,对外除了提供的public方法,不对外泄漏任何信息。迪米特法则还有一个更简单的定义:只与直接的朋友通信。首先来解释一下什么是直接的朋友:每个对象都会与其他对象有耦合关系,只要两个对象之间有耦合关系,我们就说这两个对象之间是朋友关系。耦合的方式很多,依赖、关联、组合、聚合等。其中,我们称出现成员变量、方法参数、方法返回值中的类为直接的朋友,而出现在局部变量中的类则不是直接的朋友。也就是说,陌生的类最好不要作为局部变量的形式出现在类的内部

举一个例子:有一个集团公司,下属单位有分公司和直属部门,现在要求打印出所有下属单位的员工ID。先来看一下违反迪米特法则的设计。

 //总公司员工
class Employee{
	private String id;
	public void setId(String id){
		this.id = id;
	}
	public String getId(){
		return id;
	}
}

//分公司员工
class SubEmployee{
	private String id;
	public void setId(String id){
		this.id = id;
	}
	public String getId(){
		return id;
	}
}

class SubCompanyManager{
	public List<SubEmployee> getAllEmployee(){
		List<SubEmployee> list = new ArrayList<SubEmployee>();
		for(int i=0; i<100; i++){
			SubEmployee emp = new SubEmployee();
			//为分公司人员按顺序分配一个ID
			emp.setId("分公司"+i);
			list.add(emp);
		}
		return list;
	}
}

class CompanyManager{
	public List<Employee> getAllEmployee(){
		List<Employee> list = new ArrayList<Employee>();
		for(int i=0; i<30; i++){
			Employee emp = new Employee();
			//为总公司人员按顺序分配一个ID
			emp.setId("总公司"+i);
			list.add(emp);
		}
		return list;
	}
	public void printAllEmployee(SubCompanyManager sub){
		List<SubEmployee> list1 = sub.getAllEmployee();
		for(SubEmployee e:list1){
			System.out.println(e.getId());
		}
		List<Employee> list2 = this.getAllEmployee();
		for(Employee e:list2){
			System.out.println(e.getId());
		}
	}
}

调用代码:

public class Client{
	public static void main(String[] args){
		CompanyManager e = new CompanyManager();
		e.printAllEmployee(new SubCompanyManager());
	}
}

现在这个设计的主要问题出在CompanyManager中,根据迪米特法则,只与直接的朋友发生通信,而SubEmployee类并不是CompanyManager类的直接朋友(以局部变量出现的耦合不属于直接朋友),从逻辑上讲总公司只与他的分公司耦合就行了,与分公司的员工并没有任何联系,这样设计显然是增加了不必要的耦合。按照迪米特法则,应该避免类中出现这样非直接朋友关系的耦合。修改后的代码如下:

class SubCompanyManager{
	public List<SubEmployee> getAllEmployee(){
		List<SubEmployee> list = new ArrayList<SubEmployee>();
		for(int i=0; i<100; i++){
			SubEmployee emp = new SubEmployee();
			//为分公司人员按顺序分配一个ID
			emp.setId("分公司"+i);
			list.add(emp);
		}
		return list;
	}
	public void printEmployee(){
		List<SubEmployee> list = this.getAllEmployee();
		for(SubEmployee e:list){
			System.out.println(e.getId());
		}
	}
}

class CompanyManager{
	public List<Employee> getAllEmployee(){
		List<Employee> list = new ArrayList<Employee>();
		for(int i=0; i<30; i++){
			Employee emp = new Employee();
			//为总公司人员按顺序分配一个ID
			emp.setId("总公司"+i);
			list.add(emp);
		}
		return list;
	}
	
	public void printAllEmployee(SubCompanyManager sub){
		sub.printEmployee();
		List<Employee> list2 = this.getAllEmployee();
		for(Employee e:list2){
			System.out.println(e.getId());
		}
	}
}

修改后,为分公司增加了打印人员ID的方法,总公司直接调用来打印,从而避免了与分公司的员工发生耦合。

迪米特法则的初衷是降低类之间的耦合,由于每个类都减少了不必要的依赖,因此的确可以降低耦合关系。但是凡事都有度,虽然可以避免与非直接的类通信,但是要通信,必然会通过一个“中介”来发生联系,例如本例中,总公司就是通过分公司这个“中介”来与分公司的员工发生联系的。过分的使用迪米特原则,会产生大量这样的中介和传递类,导致系统复杂度变大。所以在采用迪米特法则时要反复权衡,既做到结构清晰,又要高内聚低耦合。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值