• 六大设计原则详解


    前言

    面向对象有人分为五大原则,分别为单一职责原则、开放封闭原则、依赖倒置原则、接口隔离原则、里氏替换原则。

    也有人分为六大原则,分别为单一职责原则、开放封闭原则、依赖倒置原则、接口隔离原则、里氏替换原则、迪米特法则。

    一、迪米特法则

    1)概念

    其法则又叫最少知道法则。从名字上就能知道,类与类之间应该是最少知道的,应当加多一个类来辅助类与类之间的联系。

    其概念是一个软件实体应当尽可能少的与其他实体发生相互作用,每一个软件单位对其他的单位都只有最少的知识。

    2)说道理

    为什么会有迪米特法则呢?

    就是因为其法则有用嘛,这不是废话哦。当然,怎么有用,要针对我们的软件面向对象的设计。

    例如,通常设计上,一个类与一个类的关联是很大的。如果类A依赖类B;如果类A发生改变,那么最后的结果是类B可能要过一过代码有没有发生改变,甚至类B要跟着改变。所以,按照迪米特法则来说,建立一个中间类,类MAB,来处理类A与类B的关联。这就降低了类A与类B的耦合度了。

    但是也不能说建立太多的‘中间类’,因为会造成代码的冗余。

    二、里氏替换原则

    1)概念

    其概念是子类对象能够替换其基类对象被使用。

    听上面的概念好像很简单,不就是父类实现的方法就能被子类实现,父类在外部的调用,替换成子类也可以嘛。

    这么理解就错了,这里的概念虽然说得简单,但是其实并不是这样理解的。

    那么我们说说里氏替换原则的真正概念吧。(原则)

    2)原则

    • 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法
    • 子类可以增加自己持有的方法
    • 当子类覆盖或实现父类的方法时,方法的前置条件(参数)要比父类方法的参数更宽松
    • 当子类的方法实现父类的抽象方法时,方法的后置条件(返回值)要比父类更严格

    从上面可以看出,当我们继承父类时,不要覆盖父类的已经实现好的方法(抽象方法),只能覆未实现好的方法(非抽象方法)。

    为什么不要覆盖父类已经实现的方法呢?因为如果覆盖了已经实现的方法后,子-父类会变得很耦合。

    虽然继承是面向对象的3大特征之一,但是如果用得不好(替换父类已经实现的方法),会使程序变得复杂,变得更加耦合。

    三、接口隔离原则

    1)概念

    客户端不应该依赖它不需要的接口。一个类对另一个类的依赖应该建立在最小的接口上。

    怎么理解呢?通俗一点就是说接口尽量细分,把不需要的方法尽量写在2个不同的接口上。

    假如我有一个接口Interface1,有5个方法。其中Class1想实现第1-3个方法,Class2想实现第3-5个方法。

    那么我只有一个Interface1,如果达到上面的要求,Class1与Class2只能实现Interface1的5个方法,如下:

    image

    那么就变得很臃肿了。有什么解决办法呢?接口分离原则很好地解决了以上方法。

    我们把Interface1分离3个接口,然后Class1与Class2分别继承对应的接口,如下:

    image

    这样,很好的消除了冗余。

    2)深入了解

    接口隔离原则的含义是:建立单一接口,不要建立庞大臃肿的接口,尽量细化接口,接口中的方法尽量少。也就是说,我们要为各个类建立专用的接口,而不要试图去建立一个很庞大的接口供所有依赖它的类去调用。本文例子中,将一个庞大的接口变更为3个专用的接口所采用的就是接口隔离原则。在程序设计中,依赖几个专用的接口要比依赖一个综合的接口更灵活。接口是设计时对外部设定的“契约”,通过分散定义多个接口,可以预防外来变更的扩散,提高系统的灵活性和可维护性。

    说到这里,很多人会觉的接口隔离原则跟之前的单一职责原则很相似,其实不然。其一,单一职责原则原注重的是职责;而接口隔离原则注重对接口依赖的隔离。其二,单一职责原则主要是约束类,其次才是接口和方法,它针对的是程序中的实现和细节;而接口隔离原则主要约束接口接口,主要针对抽象,针对程序整体框架的构建。

    采用接口隔离原则对接口进行约束时,要注意以下几点:

    • 接口尽量小,但是要有限度。对接口进行细化可以提高程序设计灵活性是不挣的事实,但是如果过小,则会造成接口数量过多,使设计复杂化。所以一定要适度。
    • 为依赖接口的类定制服务,只暴露给调用的类它需要的方法,它不需要的方法则隐藏起来。只有专注地为一个模块提供定制服务,才能建立最小的依赖关系。
    • 提高内聚,减少对外交互。使接口用最少的方法去完成最多的事情。

    运用接口隔离原则,一定要适度,接口设计的过大或过小都不好。设计接口的时候,只有多花些时间去思考和筹划,才能准确地实践这一原则。

    3)生活中例子

    比如,我们身边的手机,有打电话、支付、摄影、聊天、玩游戏功能。

    我们可以定义一个接口IPhoneOperation来说明这些功能。这样臃不臃肿,要看业务逻辑。

    我们现在定一个业务逻辑,把手机功能分成业余娱乐、生活支付、工作电话。可以拆分接口分别为:

    image

    拆分各种接口后,在业务层面我们更加容易地了解各自的业务功能实现;在代码层面我们更加的高内聚,低耦合。

    四、依赖倒置原则

    1)概念

    a.高层模块不应该依赖于底层模块,两者应该依赖于其抽象。

    b.抽象不应该依赖具体实现,具体实现应该依赖抽象。

    上面2点是依赖倒置原则的概念,也是核心。主要是说模块之间不要依赖具体实现,依赖接口或抽象。

    其实依赖倒置原则的核心思想是面向接口编程。

    2)例子

    依赖倒置原则其实也没什么可以说的,下面我们来举个例子吧。

    比如,学生看语文书。我们就实现这两个类,一个是学生,一个是语文书。学生依赖于语文书,所以语文书是学生的一个属性。

    UML图如下:

    image

    类如下。

    首先是学生Student类:

    public class Student
        {
    
            public void DoSomething(YuWenBook book)
            {
                book.LookYuWenBook();
            }
    
        }

    然后是语文书类YuWenBook类:

    public class YuWenBook
        {
    
            public void LookYuWenBook()
            {
                Console.WriteLine("看语文书");
            }
    
        }

    最后,我们开始调用:

    class Program
        {
            static void Main(string[] args)
            {
                YuWenBook book = new YuWenBook();
                Student student = new Student();
                student.DoSomething(book);
    
            }
        }

    以上程序都没有错,new一个语文的对象,然后new一个学生对象,最后把语文对象赋值到学生的book中。完成了学生看书。

    但是,这是不好扩展的,抛离了面向对象的低耦合,高内聚的思想。因为,如果,我们要添加学生看英语书,那怎么添加呢?

    首先,在Student类中添加方法DoSomething(EnglishBook book); 然后再实现EnghlishBook类的LookEnglishBook方法。

    最后调用,变成:

    static void Main(string[] args)
            {
                YuWenBook book = new YuWenBook();
                Student student = new Student();
                student.DoSomething(book);
    
                //学生看英语书
                EnglishBook englishBook = new EnglishBook();
                student.DoSomething(englishBook);
    
            }

    一看,就知道不好扩展。因为已经修改了Student类中代码(增加了方法)。怎么才算好扩展呢,那么要聊起与开闭原则有关系了。对修改关闭,对扩展开放。所以我们尽量的不要修改Student类中的代码。所以,我们增加一个接口,叫IBook:

    public interface IBook
        {
    
            void LookBook();
    
        }

    然后,英语、语文各自继承IBook:

        public class EnglishBook: IBook
        {
            public void LookBook()
            {
                Console.WriteLine("看英语书");
            }
    
        }
    
        public class YuWenBook:IBook
        {
    
            public void LookBook()
            {
                Console.WriteLine("看语文书");
            }
    
        }

    再修改一下Student的方法,接收IBook参数,变成:

    public class Student
        {
    
            public void DoSomething(IBook book)
            {
                book.LookBook();
            }
    
        }

    最后,看我们是怎么调用的:

    static void Main(string[] args)
            {
                IBook book = new YuWenBook();
                Student student = new Student();
                student.DoSomething(book);
    
                //学生看英语书
                book = new EnglishBook();
                student.DoSomething(book);
    
            }

    看看,是不是可以很方面的扩展? 为什么这样说呢?

    因为当我们再添加一个学生看数学书的时候,我们只需要添加一个数学书类来继承IBook,然后实现LookBook方法。最后我们用IBook对象来调用数学书类就可以了。可以看出,Student不用做任何修改,IBook也是。只需做相应的实现与调用即可。

    3)小结

    到此,介绍与例子到此结束了,我们可以看到面向对象的设计是多么的广、深。从一些小小的例子、甚至身边发生的事,都可以联想到面向对象的设计。要掌握面向对象,首先要掌握其原则。

    五、开闭原则

    1)概念

    官方说法是 软件实体(模块、类、函数等)应该可以扩展,但是不可以修改。也就是说软件对扩展开放,对修改关闭。

    需要说明的是,对修改关闭不是说软件设计不能做修改,只是尽量不要做不必要的修改。怎么才能做到呢?那就是有相应的扩展性。

    其实,软件有相应的扩展性是好处,但是不能说每个地方都有扩展。反而造成了代码的臃肿。所以这里的扩展与修改关闭是有限制的。

    开闭原则,可以说是其他五大原则的实现,也是面向对象设计的终极目标。我们也可以说成开闭原则是其他原则的核心。

    说了这些概念性的东西,似懂非懂。我们可以试试从下面的例子来看看是否更加上心。

    2)深入理解

    开闭原则怎么能更深入的理解呢?还是说说我们身边的例子吧。

    比如我们平常喝水用的一次性纸杯。平常人只是用来装水。喝完水就扔了。这就是这个纸杯的生命周期。纸杯这一生只完成了它的一个功能:装水。纸杯此时就很封闭了,没有什么扩展性。

    此时,我看到身边有一支花苗,我想要拿回家种。但是没有容器呀? 啊?旁边不是有一个纸杯吗,可以用此纸杯来种这朵花苗。

    纸杯有了它的另外一个扩展性,就是种花苗。

    纸杯不仅有装水、种花苗的用途,以后还可以有装小垃圾、冲茶、回收等功能。对于以后这些功能,我们要想到他们的扩张性。

    在纸杯只有一个装水的功能的时候,我们只写一个纸杯功能类,说纸杯能装水。但是以后有扩展呢?这一方面我们要预先判断。预先判断它以后可能会根据需求的变动而扩展。对于纸杯本来的装水功能,不能说不能修改,此功能只能在此函数、类中修改。这就是开闭原则的核心。

    所以,纸杯在开闭原则所体现的是:尽量少修改,未来可能扩展的模块、类做好预算的判断。如果要修改,只能在此函数此类修改,不能牵涉到其他地方。

    下面,我们用UML类图来直观地说明一下纸杯的设计吧。

    当纸杯只有一个功能,装水时。有一个纸杯操作接口,有一个纸杯操作实现类。

    image

    当我们要添加一个功能 种花苗时,我们不也是加一个方法吗?如下:

    image

    当添加N个方法时,不也是在纸杯的操作接口上面添加N个方法吗?

    我们想一想,此时已经背离了我们的开闭原则。因为每添加一个方法,都要在操作类上面做修改。所以,我们按照开闭原则,开做了以下合理的设计:

    image

    从上面可以看出,我们把纸杯的操作类,统一写成一个接口,每个不同的操作继承此接口来完成各自操作。我们还开到多了一个类,叫客户端类,其实此类也不难理解。也就是要最终操作纸杯的类。

    六、单一职责原则

    1)概念

    按照官方说明,单一职责原则是指 应该有且只有一个一个原因引起类的变更。

    通俗一点来说,一个类应该只做一类事情;一个类应该只负责一个功能。

    单一职责原则是程序设计高内聚、低耦合的引申。

    2)浮想联偏

    作者接触单一职责原则的时候,以为很简单。单一职责嘛,一个类一个方法,不就是它的终极目标吗?

    的确,这属于‘终极目标’但是此终极目标不现实。因为,一个类一个方法的确是高内聚、低耦合。但是这显然显得代码很臃肿了,维护更加不便,在中大型的项目更加如此。如果你的项目足够简单,类足够少,也可以这么来做。

    所以,这里要强调一点的是,单一职责原则是一个类处理一类事情,也只有一类事情影响到这个类。并不是一个类处理一个方法。

    3)开始理解

    我们来举个例子。作者眼前有一支黑色笔,拿这支笔来做例子吧。

    笔有它属性、被动的行为。属性有:黑色、手感好、笔尖0.5。行为有:写字、画画、扎人。当然,这里的扎人只是个例子,千万不要来真的了 >. <

    按照一般的做法,我们都把笔的属性与行为都放在一个类来做。单一职责现在就起作用了,我们要分开成属性与行为。

    来上UML图:

    首先按照一般的设计:

    image

    一般一个接口、一个类来处理笔的各种事情,上面的图充分解析了这种说法,也是平常人设计的类。

    下面,我们按照单一职责原则,来设计笔的属性、行为来隔离开,如下:

    image

    从上图看出,我们把行为以及属性动作分离开。

    举了这个例子,我们看出一件事情,可以分离开多件事情的处理,从而提高了软件设计的高内聚、低耦合。

    但是,我们从上面的例子,可以在分一下笔的行为。因为画画、写字是笔在纸上做的动作,而扎人是笔在人上做的动作。

    更通俗地说,就是画画这个动作要发生改变的话,扎人这个动作应该不会受影响,所以扎人应该放到另外一个类中做处理。

    下面我们可以更深一层地理解一下这个笔的动作可以分解成2个。

    4)更深一层地理解

    按照上面说的,我们可以把笔的动作分成两种,一种在纸上做的,一种是在人上做的。

    为什么可以分开这两种,因为上面说了,一种在纸上做,一种在人上做。在纸上做只影响在纸张的类,在人上做只影响在人生的类。互不干扰。

    我们上一下UML图吧:

    image

    到此结束了,下面有空再修改与补充。

    3)其他例子

    开闭原则其实在大话设计模式中说得非常好,让人通俗易懂。

    它举了一个例子,我觉得说得非常好。是加减乘除法的例子。

    开始需求是做一个加法的操作。后来继续加入减法、乘法、除法。

    开始我们想加法以后可能会做一个需求变更:加入其它的算法法则。所以我们要有一个预判性,这个预判性会导致我们项目以后的扩展性,也会导致如果需求发生变更,程序修改的难易程度。

    所以,我们要做一个算法法则的操作类,加减乘除法都继承此操作接口。再加一个算法法则的客户端类类操作此算法。

    我们来上一下大话设计模式中的图:

    image

    4)总结

    开闭原则是我们面向对象设计的目标,我们灵活地运用好此目标也不是易事。所以开闭原则要深入的理解,才能做好面向对象的编程,才能做一个好的软件。

  • 相关阅读:
    REUSE_ALV_GRID_DISPLAY_LVC I_CALLBACK_HTML_TOP_OF_PAGE
    查找数组中最大值java
    jvm 调优
    jvm 内存
    树形遍历文件夹
    程序创建一个ArrayList,添加1到10的10个随机数,删除大于5的数 java
    字符串反序排序 并带有空格输出 java
    摆动排序
    免密登陆
    springboot UEditor集成
  • 原文地址:https://www.cnblogs.com/ljdong7/p/12014531.html
Copyright © 2020-2023  润新知