设计模式原则

参考:https://zhuanlan.zhihu.com/p/54147707

分类

  • 开放封闭原则——自己相关的属性封装在自己类里,对外暴露功能
  • 职能单一性原则——一个类只负责一个功能领域的相应职责,一个类只负责做一类事
  • 里氏替换原则——所有引用基类的地方都能透明地使用子类代替
  • 迪米特原则——一个类对于其他类知道的越少越好,就是说一个对象应当对其他对象有尽可能少关联,降低对其他类的依赖。
  • 接口隔离原则——接口尽量细化,功能单一,同时接口中的方法尽量少
  • 依赖倒置原则——依赖抽象,而不是依赖细节

相关介绍

单一职责原则

  • 一个类只负责一个功能领域的相应职责,一个类只负责做一类事

适用场景:

  1. 当一个类里需要多个类型分支判断,影响类的稳定性的时候,需要拆分成多个类

    此处Animal就该单独作为一个类,然后新增Dog,Cat的类继承Animal

        class Animal
        {
            public string Type { get; set; }
            public void Eat()
            {
                if (Type == "Cat")
                {
                    Console.WriteLine("吃鱼");
                }
                else if (Type == "Dig")
                {
                    Console.WriteLine("吃骨头");
                }
            }
            public void dothing()
            {
                if (Type == "Cat")
                {
                    Console.WriteLine("抓老鼠");
                }
                else if (Type == "Dig")
                {
                    Console.WriteLine("看家护院");
                }
            }
        }
    
  2. 类里涉及的职责涉及太多层面,整个类很臃肿,很难理解这个类负责哪些职责

    比如银行客户端,里面有货币转换、存钱、取钱等等操作;整个类显然太庞杂了。我们可以把里面的业务提取出来,比如提取一个货币转换类专门负责各种货币转换,我们可以抽取一个利率维护类,专门负责管理不同的业务的利率换算;抽取一个业务类,负责存钱、取钱。。。

    class BankClient
    {
        public double TransLateRMBToDollars(int RMBCount)
        {
            Console.WriteLine("人民币转美元");
            //double count = func(RMBCount)
            return 0;
        }
        public double TransLateDollarsToRMB(int RMBCount)
        {
            Console.WriteLine("美元转人民币");
            //double count = func(RMBCount)
            return 0;
        }
        //存钱
        public void StorageMoney(double money)
        {
    
        }
        //取钱
        public void GetMoney(double money)
        {
    
        }
    
    }
    

可违背场景

  1. 如果类型足够简单,可以在类级别去违背单一职责

    比如上例里业务就只有猫和狗两种类型,就可以写进一个类里

  2. 如果方法足够简单,可以在方法级别去违背单一职责

    比如余额查询,余额查询涉及利息计算、账目显示;如果利息计算比较简单,只有一个利率和年限就可计算,这一步就可以写在在余额查询方法里。

    如果利息计算比较复杂(比如要用到个人存款年限、是否为金卡用户、信誉等级等数据进行复杂计算),则应该把利息计算抽取单独定义成一个方法

相关建议

  1. 如果类型复杂了,方法逻辑多了,建议遵循单一职责原则
  2. 一个方法,不超过50行(编码建议)
  3. 一个功能类,不超过300行(编码建议)

优缺点

优点:
  1. 降低类的复杂度,类的职责单一,逻辑简单清晰
  2. 提高类的可读性,提高了类的稳定性(变逻辑拆分为新增)。
  3. 降低了与其他类的耦合性,因为功能单一,修改对其他功能的影响变小
缺点:
  1. 拆多了会更零碎,不好管理,使用成本高。

拓展

方法级别的单一职责原则:一个方法只做好一件事儿—职责拆分成小方法、接受输入-业务计算-数据

操作-日志、分支逻辑拆分--源码:父类方法DoS(写校验)-DoSCore(核心业务)

类级别的单一/WebCore

项目级别的单一职责原则:项目的职责要清晰—Client、Manager、职责原则:一个类只做好一件事

儿—源码:scheme-schemeprovider-handler

类库级别的单一职责原则:一个类库做好一件事儿—DAL/BLL/Core/PayCoreBackgroudJob

系统级别的单一职责原则:通用系统拆分---IP库/日志中心/在线统计

里氏替换原则

定义一:存在两个不同类(T1T2)的对象o1,o2,若一个程序里的所有对象o1都能使用o2代替,对程序地行为没有任何影响,则T2T1的子类

定义二:所有引用基类的地方都能透明地使用子类代替。(透明代表对程序没有任何影响

这是与继承地区别,因为继承存在重写,如果存在重写,这样替换肯定会有影响的

插播:抽象方法(abstract)、虚方法(virtual)及接口(interface) - 海岸线summer - 博客园 (cnblogs.com)

插播:is 与 as 的区别

 is : 相当于判断,A is B  A是不是B或者A是不是B的子类?

 as :先判断,在转换。(它比传统的强制转换相对来说要安全一点,因为传统的强制转换, 一旦转换失败的话,程序就会崩溃,那么使用as关键字,如果转换不成功,就转换成空类型)

潜在规则:

  • 尽量将公共属性、字段、变量及不涉及重写的方法抽象到父类
  • 避免出现父类有,子类里没有的情况
  • 父类实现的东西,子类就不要再写了

迪米特法则

迪米特法则(Law of Demeter)又叫作最少知识原则(The Least Knowledge Principle)

原则:

  1. 一个类对于其他类知道的越少越好,就是说一个对象应当对其他对象有尽可能少关联,降低对其他类的依赖。
  2. 如果需要建立依赖,尽量把关联建立在第三方功能实现类里,不要直接建立联系

目的:

  1. 迪米特法则的初衷在于降低类之间的耦合。避免一个类的更改影响太多其他类。
  2. 由于耦合度降低,从而提高了类的可复用率和系统的扩展性。
  3. 由于每个类尽量减少对其他类的依赖,但是因为要实现功能类的相互调用是很常见的,为了降低更改影响同时实现功能,这个时候创建第三方功能类去间接建立联系。如果某一个类有变化,只需要改动这个类和关联类。

过度使用缺陷:

  • 过度使用迪米特法则会使系统产生大量的中介类,从而增加系统的复杂性,使模块之间的通信效率降低。

结论:

在采用迪米特法则时需要反复权衡,确保高内聚和低耦合的同时,保证系统的结构清晰。

迪米特法则不希望类之间建立直接的联系。

具体应用

参照:.NET(C#) 设计模式六大原则 迪米特法则-CJavaPy

错误例子:
    public class LODPatternError
    {
        static void Main(string[] args)
        {
            Phone phone = new Phone();
            phone.read("三国演义");
        }
    }
    class Phone{
        App app = new App();
        
        public void read(string title)
        {
            Book book = new Book(title);
            app.readBook(book);
        }

    }
    class App
    {
        //app里的书单
        public List<Book> books = new List<Book>();
        public void readBook(Book book)
        {
            if (books.Find(s=>s.Title == book.Title) == null)
            {
                DownloadBook(book);
            }
            Console.WriteLine($"开始读{book.Title}");
        }
        public void DownloadBook(Book book)
        {
            Console.WriteLine($"开始下载{book.Title}");
            books.Add(book);
        }
    }
    class Book
    {
        public string Title { get; set; }
        public Book(string  title)
        {
            Title = title;
        }
    }

三个类:手机——App——书籍

正常的设计是:手机里面有阅读软件,阅读软件里面有书籍

看书的流程是:

  1. 我想看XXX书(告诉手机我想看书的标题)
  2. 我进去app根据书籍名称找这本书,没有就在app里下载
  3. 然后在app里阅读这本书

App相当于手机和书籍类的一个第三方中介类;手机不应该直接和书籍类建立联系。

手机的变化不应该对书籍有影响;书籍的变化不应该对手机有影响。显然在当前手机类的阅读方法里就有书籍实例的创建。就是说如果书籍的创建方式变化,我还需要修改手机类里的这个方法,这显然是不合理的。

正确例子:

   internal class LODPattern
    {
        static void Main(string[] args)
        {
            Phone phone = new Phone();
            phone.read("三国意义");
        }
    }
    class Phone
    {
        App app = new App();
        public void read(string title)
        {
            app.readBook(title);
        }

    }
    class App
    {
        //app里的书单
        public List<Book> books = new List<Book>();
        public void readBook(string bookTitle)
        {
            Book book = books.Find(s=>s.Title == bookTitle);
            if (book == null)
            {
                DownloadBook(bookTitle);
            }
            Console.WriteLine($"开始读{book.Title}");
        }
        public void DownloadBook(string title)
        {
            Console.WriteLine($"开始下载title");
            Console.WriteLine($"下载完成");
            books.Add(new Book(title));
        }

    }
    class Book
    {
        public string Title { get; set; }
        public string Conyent { get; set; }
        public string Author { get; set; }

        public Book(string title)
        {
            Title = title;
        }
    }

依赖倒置原则(Dependence Inversion Principle)

定义

设计代码结构时,高层模块不应该依赖低层模块,二者都应该依赖其抽象。

简单说:依赖抽象,而不是依赖细节

抽象——抽象类/接口——包含没有实现的

细节:具体的类——所有的元素都是确定的

要有【面向接口】编程的思维

目的

  • 减少类与类之间的耦合性,提高系统的稳定性
  • 提高代码的可读性和可维护性
  • 够降低修改程序所造成的风险

实践

  1. 变量的表面类型尽量是接口或者是抽象类

  2. 任何类都不应该从具体类派生

  3. 尽量不要覆写基类的方法

  4. 结合里氏替换原则使用

错误例子:

    public class Student
    {
        public string Name { get; set; }
        //玩Iphone手机
        public void PlayIPhone(IPhone phone)
        {
            Console.WriteLine($"这里是{this.Name} {nameof(phone)}");
            phone.Call();
            phone.Text();
        }
        //玩小米手机
        public void PlayMiPhone(MiPhone phone)
        {
            Console.WriteLine($"这里是{this.Name} {nameof(phone)}");
            phone.Call();
            phone.Text();
        }
        //那其他手机呢
    }
   public class IPhone
    {
        public string Name { get; set; }
        public  void Call()
        {
            Console.WriteLine("User {0} Call", this.GetType().Name);
        }
        public  void Text()
        {
            Console.WriteLine("User {0} Call", this.GetType().Name);
        }
    }
    public class MiPhone
    {
        public string Name { get; set; }
        public void Call()
        {
            Console.WriteLine("User {0} Call", this.GetType().Name);
        }
        public void Text()
        {
            Console.WriteLine("User {0} Call", this.GetType().Name);
        }
    }

这里定义几个类:学生、IPhone手机、小米手机

学生类里玩手机PlayPhone这个方法依赖于具体的手机类型,比如玩PlayIPhonePlayMiPhone

会诞生几个问题:

  1. 如果换了华为荣耀手机,是不是还要在Student类里添加一个PlayHonnorPhone?有一种新的手机机型出现就要修改一次Student类,这合理吗?

    从学生玩手机可以看出学生Student是依赖手机这个类型,但是学生一般也就是用手机打电话、上网等,换一个品牌、配置其实实际使用的还是这些功能。也就是说学生依赖的是一个可以打电话、上网、看视频的东西,这是不是就是一个抽象的手机概念

  2. 就如第一条所说,手机实际上就是打电话、上网等功能,除了操作系统、芯片这些少数区别,大多数手机的大部分功能都是一样的,有必要新出一个手机类型,我就把手机的属性或者功能重复写一遍吗?

正确的例子:

   public class Student
    {
        public string Name { get; set; }
        //玩手机
        public void PlayPhone(AbstractPhone phone)
        {
            Console.WriteLine($"这里是{this.Name} {nameof(phone)}");
            phone.Call();
            phone.Text();
        }
    }
    public abstract class AbstractPhone
    {
        public string Name { get; set; }
        public abstract void Call();

        public abstract void Text();
    }
    public class MiPhone: AbstractPhone
    {
        public override void Call()
        {
            Console.WriteLine("User {0} Call", this.GetType().Name);
        }
        public override void Text()
        {
            Console.WriteLine("User {0} Call", this.GetType().Name);
        }
    }
    public class IPhone : AbstractPhone
    {
        public override void Call()
        {
            Console.WriteLine("User {0} Call", this.GetType().Name);
        }
        public override void Text()
        {
            Console.WriteLine("User {0} Call", this.GetType().Name);
        }
    }

这样手机种类的增加不会影响学生类Student的变化,而且抽象手机AbstractPhone发送变化的情况比具体的手机变化的频率小太多。所以Student类很稳定。

总结:

  • 层级多了,依赖细节,一个改动会影响一连串---水波效应

  • 依赖抽象,类的改动影响就不会扩散

  • 相对于细节的多变性,抽象的东西要稳定的多

  • 抽象指的是接口或者抽象类,细节就是具体的实现类。

    使用接口或者抽象类的目的是制定好规范和契约,而不去涉及任何具体的操作,把展现细节的任务交给他们的实现类去

实践建议

  1. 低层模块尽量都要有抽象类或接口,或者两者都有
  2. 变量的声明类型尽量是抽象类或接口(比如玩手机的参数变量)
  3. 使用继承时遵循里氏替换原则(避免埋雷)

依赖倒置原则的核心就是要我们面向抽象/接口编程

理解了面向抽象/接口编程,也就理解了依赖倒置。

现实应用

  1. 各种工厂的面向抽象---80%的设计模式都跟面向抽象有关
  2. 现代化开发,IOC已内置—无处不在的IOC---就得有抽象

接口隔离原则(Interface Segregation Principle)

定义

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

使用规则

  1. 接口尽量小,但是要有限度。对接口进行细化可以提高程序设计灵活性是事实,但

    是如果过小,则会造成接口数量过多,使设计复杂化,所以一定要适度。

  2. 接口细节要屏蔽(一个功能接口可能有一个串行化的动作,比如获取食物的接口,接口实现里需要

    备料——烹饪——上菜等诸多步骤但没必要体现细节)

  3. 通过接口继承来组合接口

例子:

有以下几个类,对应功能如下

手机:打电话 发短信 看电影 上网 玩游戏 拍照 拍视频 导航 支付

平板: 看电影 上网 玩游戏 拍照 拍视频

电视: 看电影 上网 玩游戏

相机: 拍照 拍视频

  • 将每个功能拆分成一个接口——接口太多不利管理书写
  • 将这些功能统一成一个接口——没有的功能也得实现(比如电视没有支付功能)这样也不合理(违反了定义第一条)
  • 接口组合及合并——通过功能的统一性(比如拍照、拍视频基本上是成对存在)、及统一的目的性(比如提供食物:可以将备料—烹饪—上菜接口组合成一个接口)等逻辑通过接口继承的方式去组合成新的接口

接口隔离与单一职责的区别:

  • 单一职责原则原注重的是职责;而接口隔离原则注重对接口依赖的隔离

  • 单一职责原则主要是约束类,其次才是接口和方法,它针对的是程序中的实现和

    细节;而接口隔离原则主要约束接口,主要针对抽象,针对程序整体框架的构建

开闭原则(Open Closed Principle)

定义

一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。

修改:修改现有代码(类-方法)

扩展:增加代码(类-方法)

为什么要尽量遵循开闭原则

面向对象语言是一种静态语言,最害怕变化,会波及很多东西,需要全面测试。

如果有变化,最好的方式就是利用新增去替代修改实现新功能,能够保障原有的功能稳定可靠

其实其他几个原则也是为了更好实现开闭原则

实现方式

  1. 新增一个类实现新的方法
  2. 新增一个类继承原有的父类,再在类里定义新的功能