zoukankan      html  css  js  c++  java
  • 面向对象设计模式七大设计原则

    本来想自己写的,发现一个eleven老师的学生 加菲猫,已经写得很详细!那我将她的内容复制过来。然后在上面的内容上增写点内容。

    参照文章链接:https://www.cnblogs.com/loverwangshan/p/10211973.html

    ------------------------------------------------------加菲猫---------------------------------------------------------------

    【单一职责原则】【里式替换原则】【迪米特法则】【依赖倒置原则】【接口隔离原则】【开闭原则】【聚合/合成复用原则】

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

    设计模式七大原则:面向对象语言开发过程中,推荐的一些指导性原则,这些是没有明确的招数的,而且也经常被忽视或者违背!

    一:单一职责原则(Single Responsibility Principle)

    单一职责原则就是一个类只负责一件事儿,面向对象语言开发,类就是一个最基本的单位,单一职责的原则就是封装的粒度,主要关注的单个类实现的功能

    比如我们下面的例子

    /// <summary>
    /// 封装
    /// 动物类
    /// 简单意味着稳定
    /// </summary>
    public class Animal
    {
        private string _Name = null;
        public Animal(string name)
        {
            this._Name = name;
        }
       
        //应该拆分了
        public void Action()
        {
            if (this._Name.Equals(""))
                Console.WriteLine($"{this._Name} flying");
            else if (this._Name.Equals(""))
                Console.WriteLine($"{this._Name} walking");
            else if (this._Name.Equals(""))
                Console.WriteLine($"{this._Name} Swimming");
            else if (this._Name.Equals("蚯蚓"))
                Console.WriteLine($"{this._Name} Crawling");
        }
    }

    我们声明一个动物,但是每个动物的action是不一样的,顺着我们的思维模式,我们会在action中增加对应的if来判断,不同的动物有不同的动作,

    类似于这样的,在一个方法中写分支判断,然后执行不同的逻辑,这就违背了单一职责原则,但是其实我们想要的功能完全能实现,如果种类比较少且不变的情况下,我们完全可以这样操作,但是如果种类比较多且经常容易发生改变,那我们这样写就有很大的隐患,因为其中的改变有可能会影响到其它的。

    我们可以对其进行改变,比如我们可以先创建一个基类

     public abstract class AbstractAnimal
        {
            protected string _Name = null;
            public AbstractAnimal(string name)
            {
                this._Name = name;
            }
         public abstract void Action();
        }

    然后可以创建不同动物的类来继承于这个基类

    public class Fish : AbstractAnimal
    {
        public Fish() : base("")
        {
        }
    
      public override void Action()
        {
            Console.WriteLine($"{base._Name} swimming");
        }
    }
    public class Chicken : AbstractAnimal
    {
      public Chicken() : base("")
        {
        }
    
        public override void Action()
        {
            Console.WriteLine($"{base._Name} flying");
        }
    }

    代码调用前后对比

    改写前:

      Animal animal = new Animal("");//呼吸空气
                    animal.Action();
    
     Animal animal = new Animal("");//呼吸空气
                    animal.Action();

    改写后:

    AbstractAnimal animal = new Chicken();               
                    animal.Action();
    
    
    
    AbstractAnimal animal = new Fish();
                    animal.Action();

    类似于这样的,然后在自己的类中实现自己的方法,一个类只负责自己的事情,且都比较单一,简单意味着稳定,意味着强大,这就是所谓的单一职责原则,那么究竟什么时候回使用单一职责原则呢?如果类型复杂,方法多,这样建议使用单一职责原则!

    那么使用单一职责原则也有自己的弊端,具体分为以下两个方面

    1:代码量的增加(拆分开类的代码明显比之前增加)

    2:使用成本就是所谓的理解成本增高(调用者要晓得不同的类)

    具体的单一原则分为以下五种

    1:方法级别的单一职责原则:一个方法只负责一件事儿(职责分拆小方法,分支逻辑分拆)
    2:类级别的单一职责原则:一个类只负责一件事儿
    3:类库级别的单一职责原则:一个类库应该职责清晰
    4:项目级别的单一职责原则:一个项目应该职责清晰(客户端/管理后台/后台服务/定时任务/分布式引擎)
    5:系统级别的单一职责原则:为通用功能拆分系统(IP定位/日志/在线统计)

    二: 里氏替换原则(Liskov Substitution Principle)

    任何使用基类的地方,都可以透明的使用其子类,这主要是指 继承+透明(安全,不会出现行为不一致)

    继承:子类拥有父类的一切属性和行为,任何父类出现的地方,都可以用子类来代替,主要是因为:

    1:父类有的,子类是必须有的(私有不继承);如果出现了子类没有的东西,那么就应该断掉继承;、

    2:子类可以有自己的属性和行为,但是子类出现的地方,父类不一定能代替

    3:父类实现的东西,子类就不要再写了,(就是不要new隐藏),如果想修改父类的行为,通过abstract/virtual

    举个例子:

    复制代码
     1 public class People
     2 {
     3     public int Id { get; set; }
     4     public string Name { get; set; }
     5 
     7     public void Traditional()
     8     {
     9         Console.WriteLine("仁义礼智信 温良恭俭让 ");
    10     }
    11 }
    12 
    13 public class Chinese : People
    14 {
    15     public string Kuaizi { get; set; }
    16     public void SayHi()
    17     {
    18         Console.WriteLine("早上好,吃了吗?");
    19     }
    20 
    21 }
    22 
    23 public class Hubei : Chinese
    24 {
    25     public string Majiang { get; set; }
    26     public new void SayHi()
    27     {
    28         Console.WriteLine("早上好,过早了么?");
    29     }
    30 }
    复制代码

    调用的时候:

    复制代码
    {
        Chinese people = new Chinese();
        people.Traditional();
        people.SayHi();
    }
    {
        Chinese people = new Hubei();
        people.Traditional();
        people.SayHi();
    
    }
    {
        var people = new Hubei();
        people.Traditional();
        people.SayHi();
    }
    复制代码

    上面需要注意的是:如果是普通的方法,以左边为主,就是左边是什么类,就调用谁的普通方法(编译时决定),如果是abstract或者virtual则是以右边为主(运行时决定),所以:父类有的方法,子类就不要再写了,(就是不要new隐藏),如果想修改父类的方法,通过abstract/virtual来标识!

    三:迪米特法则

    也叫最少知道原则,就是:一个对象应该对其他对象保持最少的了解,只与直接的朋友通信。

    他主要的职责就是关注类与类之间的交互,降低类与类之间的耦合,尽量避免依赖更多的类型

    举例说明:

    有一个学生类,班级类,学校类

    /// <summary>
    /// 学生
    /// </summary>
    public class Student
    {
        public int Id { get; set; }
        public string StudentName { get; set; }
        public int Height { private get; set; }
    
        public int Salay;
    
        public void ManageStudent()
        {
            Console.WriteLine(" {0}Manage {1} ", this.GetType().Name, this.StudentName);
        }
    }

     /// <summary>
     /// 班级
     /// </summary>
     public class Class
     {
         public int Id { get; set; }
         public string ClassName { get; set; }
    
         public List<Student> StudentList { get; set; }
    
    
         public void ManageClass()
         {
             Console.WriteLine(" {0}Manage {1} ", this.GetType().Name, this.ClassName);
             foreach (Student s in this.StudentList)
             {
                 s.ManageStudent();
             }
         }
     }
     1 /// <summary>
     2 /// 学校
     3 /// </summary>
     4 public class School
     5 {
     6     public int Id { get; set; }
     7     public string SchoolName { get; set; }
     8     public List<Class> ClassList { get; set; }
     9 
    10     public void Manage()
    11     {
    12         Console.WriteLine("Manage {0}", this.GetType().Name);
    13         foreach (Class c in this.ClassList)
    14         {
    15             //遵循了迪米特,school直接跟自己的朋友classList通讯,而不是跟自己的朋友的朋友通讯
    16             c.ManageClass();
    17 
    18             #region 违背了迪米特法则(跟自己的朋友的朋友通讯)
    19             //List<Student> studentList = c.StudentList;
    20             //foreach (Student s in studentList)
    21             //{
    22             //    Console.WriteLine(" {0}Manage {1} ", s.GetType().Name, s.StudentName);
    23             //}
    24             #endregion
    25 
    26         }
    27     }

    现在的关系是:一个学校有多个班级,每个班级有多个学生,现在学校想要管理学生,学校可以直接跟班级通讯,然后班级跟学生通讯,这就是所谓的只与直接朋友通讯,然后避免依赖更多类型(这个类型不包含:基类库BCL--框架内置)

    其实类与类之间的关系可以总结为:

    1:纵向:继承≈实现(最密切)

    2:横向:聚合> 组合> 关联> 依赖(出现在方法内部)

    依赖别人更少,也让别人了解更少,比如我们项目中经常中用到的一些访问修饰符:

    Private:私有
    Protected:子类才能获取到,子类才能访问到
    Internal :当前dll才能看见
    Public:公开得
    Protected internal 叠加要么是子类, 要么是相同类库

    其实项目中能体现迪米特法则的地方比如:三层架构(UI-BLL-DAL),还有我们习惯创建的中间层(UI-中间层--(调用不同的业务逻辑进行组合)),另外还有门面模式

     另外需要注意的是:单一职责法则只关注单类的功能;迪米特法则关注的是类与类之间的联系

     四:依赖倒置原则(Dependence Inversion Principle)

    依赖倒置原则:高层模块不应该依赖于低层模块,二者应该通过抽象依赖

    那何为高层何为底层,一般来说使用者为高层,被调用者为低层,具体还是举例说明,我们先创建一个手机:

    public abstract class AbstractPhone
     {
         public int Id { get; set; }
         public string Branch { get; set; }
         public abstract void Call();
         public abstract void Text();
     }
    
    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);
        }
    }

    接着我们创建一个学生,然后学生玩手机:

    public class Student
    {
        public int Id { get; set; }
        public string Name { get; set; }
    
        /// <summary>
        /// 依赖细节  高层就依赖了底层
        /// </summary>
        /// <param name="phone"></param>
        public void PlayiPhone(iPhone phone)
        {
            Console.WriteLine("这里是{0}", this.Name);
            phone.Call();
            phone.Text();
        }
    
        public void PlayLumia(Lumia phone)
        {
            Console.WriteLine("这里是{0}", this.Name);
            phone.Call();
            phone.Text();
        }
    
        public void PlayHonor(Honor phone)
        {
            Console.WriteLine("这里是{0}", this.Name);
            phone.Call();
            phone.Text();
        }
    }

    然后上面的学生就是高层,而手机就是低层,然后学生玩手机不应该直接直接写PlayiPhone,PlayLumia,PlayHonor 等,显然这是依赖的细节,如果这样写,以后增加一个手机,则要在学生类中增加了一个play方法,这样就会使学生类反复修改而不稳定,所以我们一般会定义一个基类为AbstractPhone,然后把各个型号通用的手机属性和功能都写在基类中,然后在学生中增加一个Play的方法如下:

    public void Play(AbstractPhone phone)
    {
        Console.WriteLine("这里是{0}", this.Name);
        phone.Call();
        phone.Text();
    }

    这样的话以后增加手机的话只要继承AbstractPhone,则在外部都可以直接调用。因为父类出现的地方都可以用子类代替。

    然后有人还说这个可以直接使用泛型来写如下:

    public void PlayT<T>(T phone) where T : AbstractPhone
    {
        Console.WriteLine("这里是{0}", this.Name);
        phone.Call();
        phone.Text();
    }

    这个T使用基类来进行约束,其实就等同于用父类参数类型!

    调用的方法如下:

    {
         iPhone phone = new iPhone();
         student.PlayiPhone(phone);
         student.PlayT(phone);
         student.Play(phone);
     }
     {
         Lumia phone = new Lumia();
         student.PlayLumia(phone);
         student.PlayT(phone);
         student.Play(phone);
     }
     {
         Honor phone = new Honor();
         student.PlayHonor(phone);
         student.PlayT(phone);
         student.Play(phone);
     }

    然后这样写的好处主要分为以下两点:

    1 :一个方法满足不同类型的参数

    2:还支持扩展,只要是实现了这个抽象,不用修改Student,稳定的同时又多了扩展

    这就是所谓的面向抽象编程,但是面向抽象编程只适合于通用功能,如果你想要在子类中有特殊的操作,比如想要在iphone手机新增一个其它手机没有的方法A,然后play方法还传入AbstractPhone这个参数,这样是么有办法访问到A,这就是非通用的,就不应该面向抽象

    那何为面向抽象,以及面向抽象的好处?

    面向抽象就是:只要抽象不变,高层就不变!

    面向抽象的好处:

    面向对象语言开发,就是类与类之间进行交互,如果高层直接依赖低层的细节,细节是多变的,那么低层的变化就导致上层的变化;如果层数多了,底层的修改会直接水波效应传递到最上层,一点细微的改动都会导致整个系统从下往上的修改!

    而面向抽象,如果高层和低层没有直接依赖,而是依赖于抽象,抽象一般是稳定的,那低层细节的变化扩展就不会影响到高层,这样就能支持层内部的横向扩展,不会影响其他地方,这样的程序架构就是稳定的

    以后很多的设计模式都会跟抽象有关,比如我们经常说的控制反转IOC就是一个很好的例子!依赖倒置原则(理论基础)---IOC控制反转(实践封装)---DI依赖注入(实现IOC的手段)!

    五: 接口隔离原则(Interface Segregation Principle)

     未遵循接口隔离原则的设计

    这个图的意思是:类A依赖接口I中的方法1、方法2、方法3,类B是对类A依赖的实现。
    类C依赖接口I中的方法1、方法4、方法5,类D是对类C依赖的实现。
    对于类B和类D来说,虽然他们都存在着用不到的方法(也就是图中红色字体标记的方法),但由于实现了接口I,所以也必须要实现这些用不到的方法。

    遵循接口隔离原则的设计

    接口隔离原则则是:客户端不应该依赖它不需要的接口, 一个类对另一个类的依赖应该建立在最小的接口上;

    这个原则跟自己实际工作有很大的关系,总结下来可以通过以下几点来定义接口:

    1: 既不能是大而全,会强迫实现没有的东西,也会依赖自己不需要的东西
    2 :也不能一个方法一个接口,这样面向抽象也没有意义的
    按照功能的密不可分来定义接口,
    而且应该是动态的,随着业务发展会有变化的,但是在设计的时候,要留好提前量,避免抽象的变化
    没有标准答案,随着业务和产品来调整的

    3 :接口合并 Map--定位/搜索/导航 这种属于固定步骤,业务细节,尽量的内聚,在接口也不要暴露太多业务细节(就是密切相连的可以放在一个接口来实现,而不是分多个接口实现功能)

    六:开闭原则(Open Closed Principle)

    “开-闭 ” 原则讲的是:一个软件实体应当对扩展开放, 对修改关闭。 这一原则最早由 Bertrand Meyer [MEYER88]提出, 英文原文是:Software entities should be open for extension, but closed for modification.

    这个原则说的是, 在设计一个模块的时候, 应当使这个模块可以在不被修改的前提下被扩展。 换言之, 应当可以在不必修改源代码的情况下改变这个模块的行为。

    所有的软件系统都有一个共同的性质, 即对它们的需求都会随时间的推移而发生变化。 在软件系统面临新的需求时, 系统的设计必须是稳定的。 满足 “开-闭” 原则的设计可以给一个软件系统两个无可比拟的优越性:

    通过扩展已有的软件系统, 可以提供新的行为, 以满足对软件的新需求, 使变化中的软件系统有一定的适应性和灵活性。
    已有的软件模块,特别是最重要的抽象层模块不能再修改, 这就使变化中的软件系统有一定的稳定性和延续性。
    具有这两个优点的软件系统是一个在高层次上实现了复用的系统, 也是一个易于维护 的系统。

     

    七 .合成/聚合复用原则(Composite/Aggregate Reuse Principle,CARP)


    合成/聚合复用原则经常又叫做合成复用原则,它的设计原则是:要尽量使用合成/聚合,尽量不要使用继承。也就是说,我们要优先考虑使用合成、聚合来实现功能,在使用合成、聚合无法实现的情况下,才考虑使用继承来实现。

    其实这里最重要的地方就是区分“has-a”和“is-a”的区别。
    相对于合成和聚合,继承的缺点在于:父类的方法全部暴露给子类。父类如果发生变化,子类也得发生变化;聚合的复用的时候就对另外的类依赖的比较的少。

     
  • 相关阅读:
    锋利的jQuery书中推荐的几款插件
    60款很酷的 jQuery 幻灯片演示和下载
    关于viewport
    几个Jquery对话框插件
    从小白白到大白白之初识区块链(如何理解区块链)
    简单的MVC与SQL Server Express LocalDB
    java的MVC与C#
    企业微信登陆方式
    整洁的测试遵循的规则
    Android活动的生命周期
  • 原文地址:https://www.cnblogs.com/for-easy-fast/p/12396398.html
Copyright © 2011-2022 走看看