欢迎来到骑白马的菜鸟的博客

设计模式就该这么学:要走心才能遵循设计模式五大原则(第二篇)

 

摘要:设计模式是基于一定问题解决规律产生,自然也会有些公用的东西,这个公用的东西就是我今天要讲的设计模式的五大原则,当然原则只是战略层面的指导,没有代码能完全遵守着五大原则,要根据实际(zou)情况(xin)合理取舍

  本文作为《设计模式就该这么学》系列文章的第二篇,今天我们来聊一下设计模式中的五大原则!

  《设计模式就该这么学》系列文章: 

设计模式就该这么学:为什么要学设计模式?(开篇漫谈)

设计模式就该这么学:要走心才能遵循设计模式五大原则(第二篇)

设计模式就该这么学:以微信订阅号来讲观察者模式(第三篇)

观察者模式实际应用:监听线程,意外退出线程后自动重启

一、到底是哪五大原则

  设计模式(面向对象)五大原则可以简称为SOLID,SOLID是面向对象设计和编程(OOD&OOP)中几个重要编码原则(Programming Priciple)的首字母缩写,它目的就是为了写出可复用、可扩展、高内聚、低耦合的代码。

当然原则只是战略层面的指导,没有代码能完全遵守着五大原则,要根据实际(zou)情况(xin)合理取舍。

下面我们来看看SOLID的具体描述:

 

SRP The Single Responsibility Principle  单一责任原则
OCP The Open Closed Principle  开放封闭原则
LSP The Liskov Substitution Principle 里氏替换原则
DIP The Dependency Inversion Principle 依赖倒置原则
ISP The Interface Segregation Principle 接口分离原则

1、单一职责

  一个类只做一种类型责任,当这个类需要承当其他类型的责任的时候,就需要分解这个类。不过在现实开发中,这个原则是最不可能遵守的,因为每个人对一个类的哪些功能算是同一类型的职责判断都不相同。

2、开放封闭原则

  软件实体应该是可扩展,而不可修改的。也就是说,你写完一个类,要想添加功能,不能修改原有类,而是想办法扩展该类。即对扩展开放,对修改关闭。

3、里氏替换原则

  当一个子类的实例应该能够替换任何其超类的实例时,它们之间才具有is-A关系。也就是说接口或父类出现的地方,实现接口的类或子类可以代入,这主要依赖于多态和继承。

4、 接口分离原则

  (1)不能强迫用户去依赖那些他们不使用的接口。换句话说,使用多个专门的接口比使用单一的总接口总要好。

  (2)不要提供一个大的接口包括所有功能,应该根据功能把这些接口分割,减少依赖。

5、依赖倒置原则

  (1)高层模块不应该依赖于低层模块,二者都应该依赖于抽象

  (2)抽象不应该依赖于细节,细节应该依赖于抽象

二、举个例子来介绍下五大原则

   以动物爬树来讲单一原则,用一个类描述这个场景

class Animal {
    public void breathe(string animal)
    {
        System.out.println(animal+"爬树");
    }
}
class Program
{
    static void Main(string[] args)
    {
        Animal animal = new Animal();
        animal.breathe("猫");
        animal.breathe("蛇");
    }
}

我们发现不是所有动物都会爬树的,比如鸟就不会爬树,根据单一职责原则,我们将Animal类细分为爬树动物类和飞翔动物类,如下所示: 

   class SpeelAnimal
    {
        public void pashu(string animal)
        {
            System.out.println(animal+"爬树");
        }
    }
    class FlyAnimal
    {
        public void fly(string animal)
        {
        	System.out.println(animal + "飞翔");
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            SpeelAnimal speelAnimal = new SpeelAnimal();
            speelAnimal.pashu("蛇");
            FlyAnimal flyAnimal = new FlyAnimal();
            flyAnimal.breathe("麻雀");
        }
    }

 我们发现这样修改的花销很大,既要将原来的类分解,又要修改客户端。而直接修改Animal类虽然违背了单一职责原则,但花销小的多,如下所示

class Animal
{
    public void action(string animal)
    {
        if ("麻雀".Equals(animal))
        {
            System.out.println(animal + "飞翔");
        }
        else {
            System.out.println(animal + "攀爬");
        }
    }
}
class Program
{
    static void Main(string[] args)
    {
        Animal animal = new Animal();
        animal.action("蛇");
        animal.action("麻雀");
    }
}

 可以看到,这样代码量少了很多,可还有一个问题,鸡会打鸣,而蛇不会,有一天需要增加鸡打鸣的方法,会发现又要直接修改上面的方法,这种修改直接在代码级别违背了单一职责原则,虽然修改起来最简单,但隐患最大。还有一种修改方式:

 Animal
    {
        public void pashu(string animal)
        {
            System.out.println(animal+"爬树");
        }

        public void fly(string animal)
        {
        	System.out.println(animal + "飞翔");
        }
        
        public call()
       {
                System.out.println(animal + "鸣叫");
        }
    
    }
    class Program
    {
        static void Main(string[] args)
        {
            Animal animal = new Animal();
            animal.pashu("蛇");        
            animal.call("麻雀");
        }
    }    

 这种修改方式没有改动原来的方法,而是在类中新加了一个方法,这样虽然违背了单一职责原则,但在方法级别上却是符合单一职责原则的。那么在实际编程中,采用哪一种呢?我的原则是,只有逻辑足够简单,才可以在代码级违反单一职责原则;只有类中方法数量足够少,才可以在方法级别违反单一职责原则。 

 遵循单一职责的优点:
1)降低类的复杂度,一个类只负责一项职责。
2)提高类的可读性,可维护性
3)降低变更引起的风险。

开放封闭原则这个没什么好说的,再来说说里氏替换原则,它其实有两种定义,

第一种定义:如果对每一个类型为S的对象o1,都有类型为T的对象o2,使得以T定义的所有程序P在所有的对象o1都代换为o2,程序P的行为没有发生变化,那么类型S是类型T的子类型。

第二种定义:所有引用基类的地方必须透明的使用其子类的对象。第二种定义明确的说,只要父类能出现的地方子类也可以出现,而且替换为子类不会产生任何错误或异常,但是反过来就不行,有子类出现的地方,父类未必就能适应。由定义可知,在使用继承时,会遵循里氏替换原则,集成的好处自不必多说,代码共享,重用性高,但是在子类中尽量不要重写和重载父类的方法。为什么呢?

  • 继承是侵入性的,只要继承,就必须拥有父类的所有方法和属性
  • 降低了代码的灵活性,子类必须拥有父类的属性和方法,让子类有了一些约束
  • 增加了耦合性,当父类的常量,变量和方法被修改了,需要考虑子类的修改,这种修改可能带来非常糟糕的结果,要重构大量的代码。

   接下来以一个两数相减的小李子说明一下:

class A{
    public int func1(int a,int b){
        return a-b;
    }
}
public class Client{
    public static void main(string[] args){
        A a=new A();
        System.out.println("100-50="+a.func1(100,50));
        System.out.println("100-80="+a.func1(100,80));
    }
}

 运行结果:

100-50=50
100-80=20

后来,我们需要增加一个新的功能:完成两数相加,然后再与100求和,由类B来负责。

Class B extends A{
    public int func1(int a,int b){
        return a+b;
    }
    public int func2(int a,int b){
        return func1(a,b)+100;
    }
}
public class Client{
    public static void main(string[] args){
        B a=new B();
        System.out.println("100-50="+b.func1(100,50));
        System.out.println("100-80="+b.func1(100,80));
        System.out.println("100+20+100="+b.func2(100,20));
    }
}

运行结果:

100-50=150
100-80=180
100+20+100=220

我们发现原来运行正常的相减功能发生了错误,这就是因为重写A类中的方法导致。

所以如果非要重写父类的方法,通用的做法是:原来的父类和子类都继承一个更通俗的基类,原有的继承关系去掉,采用依赖,聚合,组合等关系代替

接下来说说依赖导致原则,它的定义是:

  • 高层模块不应该依赖低层模块,两者都应该依赖抽象
  • 抽象不应该依赖细节
  • 细节应该依赖抽象

依赖倒置原则可以说是这里面最难用的一个原则了,那为什么我们还要要遵循依赖倒置原则?接下来以一个看视频的例子说明下问题,张三喜欢看douyin小视频

//Bibili类
public class Bibili类{
    //阅读文学经典
    public void watch(){
       System.out.println("看douyin视频");
    }
}

 再来看张三类

//张三类
public class Zhangsan{
    //阅读文学经典
    public void watch(douyin bili){
        bili.watch();
    }
}

场景类

public class Client{
   public static void main(Strings[] args){
      Zhangsan zhangsan = new Zhangsan();
      douyin  bili = new douyin ();
      //zhangsan看douyin视频
      zhangsan.watch(bili);
   }

}

张三看了一段时间的小视频时候,又想看电视机剧《白夜追凶》,发现这个只在优酷才能看,于是实现一个优酷类

public class Youku{
    //看电视剧
    public void watch(){
       System.out.println("看白夜追凶");
    }
}

现在我们再来看代码,发现张三类的read方法只与Bilibi类是强依赖,紧耦合关系,张三竟然阅读不了小说类。这与现实明显的是不符合的,代码设计的是有问题的。那么问题在那里呢?
我们看张三类,此类是一个高层模块,并且是一个细节实现类,此类依赖的是一个Bilibi类,而Bilibi类也是一个细节实现类。这是不是就与我们说的依赖倒置原则相违背呢?依赖倒置原则是说我们的高层模块,实现类,细节类都应该是依赖与抽象,依赖与接口和抽象类。
为了解决张三看电影的问题,我们根据依赖倒置原则先抽象一个阅读者接口,下面是完整的uml类图:

观看者接口

public interface Watcher{
   //阅读
   public void watch(Watcher watch){
       watch.watch();
   }

}

视频接口

public interface Video{
   //被观看
   public void video();
}

再定义douyin类和Youku类

//douyin类
public class douyin implements Watcher{
    public void watcher(){
       System.out.println("看douyin小视频");
    }
}
//Youku类
public class Youku implements Watcher{
    public void watcher(){
       System.out.println("看白夜追凶");
    }
}

然后实现张三类

//张三类
public class Zhasan implements Video{
    //观看
    public void watch(Video video){
        video.watch();
    }
}

然后再让张三来看douyin视频和优酷电视剧《白夜追凶》

public class Client{
   public static void main(Strings[] args){
      Watcher zhansan = new Zhansan();
      Video douyin = new douyin();
      //张三看douyin小视频
      zhangsan.watch(douyin);

      Video youku = new Youku();
      //张三看douyin小视频
      zhangsan.watch(youku);
   }

}

至此,张三是可以看douyin小视频,又可以看优酷电视剧《白夜追凶》了,目的达到了。

为什么依赖抽象的接口可以适应变化的需求?这就要从接口的本质来说,接口就是把一些公司的方法和属性声明,然后具体的业务逻辑是可以在实现接口的具体类中实现的。所以我们当依赖对象是接口时,就可以适应所有的实现此接口的具体类变化。由此也可以看出依赖倒置原则用好可以减少类间的耦合性,提高系统的稳定,降低并行开发引起的风险,提高代码的可读性和可维护性。

 

至此,设计模式的五大原则介绍完毕,接下来一篇文章我会以微信订阅号来讲观察者模式,同时会讲到实际应用的一个例子!

 

 学习本就是一个不断模仿、练习、再到最后面自己原创的过程。

虽然可能从来不能写出超越网上通类型同主题博文,但为什么还是要写?
于自己而言,博文主要是自己总结。假设自己有观众,毕竟讲是最好的学(见下图)。

于读者而言,笔者能在这个过程get到知识点,那就是双赢了。
当然由于笔者能力有限,或许文中存在描述不正确,欢迎指正、补充!
感谢您的阅读。如果本文对您有用,那么请点赞鼓励。

posted @ 2018-04-22 21:11  骑白马的菜鸟  阅读(996)  评论(1编辑  收藏  举报