浅谈面向对象的六大设计原则

作者: s_在路上 | 来源:发表于2018-10-27 12:02 被阅读46次
    image.png

    原则一、单一职责原则(Single Responsibility Principle,简称SRP )

    核心思想:不要存在多于一个导致类变更的原因,通俗的说,即一个类只负责一个功能。如果一个类里面添加了多个功能,当其中有一个功能发生了变化,就会有多种原因引起这个类的变更,从而导致这个类的维护变得困难。在真实的开发中,不仅仅是类,函数也要遵循单一职责原则。即:一个函数负责一个功能。如果一个函数里面有不同的功能,则需要将不同的功能的函数分离出去。

    优点:如果一个类或者一个函数的功能划分清晰,只负责某一项具体的功能,不但可以降低类或者函数的复杂度,而且还可以提高代码的可读性、应用程序的可维护性。

    例如,需求上指出用一个类描述食肉和食草动物:

    //================== Animal.h ==================
    
    @interface Animal : NSObject
    
    - (void)eatWithAnimalName:(NSString *)animalName;
    
    @end
    

    运行结果:

    2018-10-27 17:55:25.775317+0800 DesignPatterns[54087:24701786] 狼 吃肉
    2018-10-27 17:55:25.775689+0800 DesignPatterns[54087:24701786] 豹 吃肉
    2018-10-27 17:55:25.775721+0800 DesignPatterns[54087:24701786] 虎 吃肉
    

    上线后,发现问题了,并不是所有的动物都是吃肉的,比如羊就是吃草的。修改时如果遵循单一职责原则,需要将 Animal 类细分为食草动物类 Herbivore,食肉动物 Carnivore,代码如下:

    //================== Herbivore.h ==================
    @interface Herbivore : Animal
    
    @end
    
    @implementation Herbivore
    
    - (void)eatWithAnimalName:(NSString *)animalName {
        NSLog(@"%@ 吃草", animalName);
    }
    
    @end
    
    //================== Carnivore.h ==================
    @interface Carnivore : Animal
    
    @end
    
    @implementation Carnivore
    
    - (void)eatWithAnimalName:(NSString *)animalName {
        NSLog(@"%@ 吃肉", animalName);
    }
    
    @end
    
    //================== main 函数 ==================
    Animal *carnivore = [Carnivore new];
    [carnivore eatWithAnimalName:@"狼"];
    [carnivore eatWithAnimalName:@"豹"];
    [carnivore eatWithAnimalName:@"虎"];
    NSLog(@"\n");
    Animal *herbivore = [Herbivore new];
    [herbivore eatWithAnimalName:@"羊"];
    

    在子类里面重写父类的 eatWithAnimalName 函数,运行结果:

    2018-10-27 18:04:49.189722+0800 DesignPatterns[54422:24725132] 狼 吃肉
    2018-10-27 18:04:49.190450+0800 DesignPatterns[54422:24725132] 豹 吃肉
    2018-10-27 18:04:49.190482+0800 DesignPatterns[54422:24725132] 虎 吃肉
    2018-10-27 18:04:49.190498+0800 DesignPatterns[54422:24725132] 
    2018-10-27 18:04:49.190530+0800 DesignPatterns[54422:24725132] 羊 吃草
    

    这样一来,不仅仅在此次新需求中满足了单一职责原则,以后如果还要增加食肉动物和食草动物的其他功能,就可以直接在这两个类里面添加即可。但是,有一点,修改花销是很大的,除了将原来的类分解之外,还需要修改 main 函数 。而直接修改类 Animal 来达成目的虽然违背了单一职责原则,但花销却小的多,代码如下:

    //================== Animal.h ==================
    
    @interface Animal : NSObject
    
    - (void)eatWithAnimalName:(NSString *)animalName;
    
    @end
    
    @implementation Animal
    
    - (void)eatWithAnimalName:(NSString *)animalName {
        if ([@"羊" isEqualToString:animalName]) {
            NSLog(@"%@ 吃草", animalName);
        } else {
            NSLog(@"%@ 吃肉", animalName);
        }
    }
    
    @end
    
    //================== main 函数 ==================
    
    Animal *animal = [Animal new];
    [animal eatWithAnimalName:@"狼"];
    [animal eatWithAnimalName:@"豹"];
    [animal eatWithAnimalName:@"虎"];
    [animal eatWithAnimalName:@"羊"];
    
    

    运行结果:

    2018-10-27 18:16:10.910397+0800 DesignPatterns[54677:24751636] 狼 吃肉
    2018-10-27 18:16:10.911105+0800 DesignPatterns[54677:24751636] 豹 吃肉
    2018-10-27 18:16:10.911138+0800 DesignPatterns[54677:24751636] 虎 吃肉
    2018-10-27 18:16:10.911160+0800 DesignPatterns[54677:24751636] 羊 吃草
    

    可以看到,这种修改方式要简单的多。
    但是却存在着隐患:有一天需求上增加牛和马也需要吃草,则又需要修改 Animal 类的 eatWithAnimalName 函数,而对原有代码的修改会对调用狼、豹和虎吃肉等功能带来风险,也许某一天你会发现运行结果变为虎也吃草了。这种修改方式直接在代码级别上违背了单一职责原则,虽然修改起来最简单,但隐患却是最大的。还有一种修改方式:

    //================== Animal.h ==================
    
    @interface Animal : NSObject
    
    /**
     *  吃草
     */
    - (void)eatGrassWithAnimalName:(NSString *)animalName;
    
    /**
     *  吃肉
     */
    - (void)eatMeatWithAnimalName:(NSString *)animalName;
    
    @end
    
    @implementation Animal
    
    - (void)eatGrassWithAnimalName:(NSString *)animalName {
        NSLog(@"%@ 吃草", animalName);
    }
    
    - (void)eatMeatWithAnimalName:(NSString *)animalName {
        NSLog(@"%@ 吃肉", animalName);
    }
    
    @end
    
    //================== main 函数 ==================
    
    Animal *animal = [Animal new];
    [animal eatMeatWithAnimalName:@"狼"];
    [animal eatMeatWithAnimalName:@"豹"];
    [animal eatMeatWithAnimalName:@"虎"];
    [animal eatGrassWithAnimalName:@"羊"];
    

    运行结果:

    2018-10-27 18:31:30.321473+0800 DesignPatterns[55048:24787008] 狼 吃肉
    2018-10-27 18:31:30.321884+0800 DesignPatterns[55048:24787008] 豹 吃肉
    2018-10-27 18:31:30.321922+0800 DesignPatterns[55048:24787008] 虎 吃肉
    2018-10-27 18:31:30.321939+0800 DesignPatterns[55048:24787008] 羊 吃草
    

    通过运行结果可以看到,这种修改方式没有改动原来的函数,而是在类中新加了一个函数,这样虽然也违背了类单一职责原则,但在函数级别上却是符合单一职责原则的,因为它并没有动原来函数的代码。

    在实际的开发应用中,有很多复杂的场景,怎么设计一个类或者一个函数,让应用程序更加灵活,是更多程序员们值得思考的,需要结合特定的需求场景,有可能有些类里面有很多的功能,但是切记不要将不属于这个类本身的功能也强加进来,这样不仅带来不必要的维护成本,也违反了单一职责的设计原则

    原则二、里氏替换原则(Liskov Substitution Principle,简称LSP)

    定义:如果对一个类型为 T1 的对象 o1,都有类型为 T2 的对象 o2,使得以 T1 定义的所有程序 P 在所有的对象 o1 都替换成 o2 时,程序 P 的行为没有发生变化,那么类型 T2 是类型 T1 的子类型。有点拗口,通俗点讲,只要父类能出现的地方子类就可以出现,而且替换为子类也不会产生任何错误或异常,使用者不需要知道是父类还是子类。但是,反过来就不行了,有子类出现的地方,父类未必就能适应

    面向对象的语言的三大特点是继承、封装、多态,里氏替换原则就是依赖于继承、多态这两大特性。当使用继承时,遵循里氏替换原则。子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法,也尽量不要重载父类的方法

    比如,需要完成一个两数相加的功能:

    //================== A.h ==================
    
    @interface A : NSObject
    
    /**
     加法
    
     @param a
     @param b
     @return 相加之后的和
     */
    - (NSInteger)addition:(NSInteger)a b:(NSInteger)b;
    
    @end
    
    //================== main 函数 ==================
    
    A *a = [[A alloc] init];
    NSLog(@"100+50=%ld", [a addition:100 b:50]);
    NSLog(@"100+80=%ld", [a addition:100 b:80]);
    

    运行结果如下,

    2018-11-01 22:53:23.549358+0800 DesignPatterns[18063:363232] 100+50=150
    2018-11-01 22:53:23.549586+0800 DesignPatterns[18063:363232] 100+80=180
    

    接着,需求上需要增加一个新的功能,完成两数相加,然后再与 100 求差,由类 B 来负责。即类 B 需要完成两个功能:

    • 两数相减。
    • 两数相加,然后再加 100

    由于类 A 已经实现了加法功能,所以 B 继承 A 之后,只需要完成减法功能就可以了,但是在类 B 中不小心重写了父类 A 的减法功能,如下:

    //================== B.h ==================
    
    @interface B : A
    
    /**
     加法
     
     @param a
     @param b
     @return 相加之后的和
     */
    - (NSInteger)addition:(NSInteger)a b:(NSInteger)b;
    
    
    /**
     减法
     
     @param a
     @param b
     @return 相加之后的和
     */
    - (NSInteger)subtraction:(NSInteger)a b:(NSInteger)b;
    
    @end
    
    //================== main 函数 ==================
    
    B *b = [[B alloc] init];
    NSInteger sub = [b addition:100 b:50];
    NSInteger difference = [b subtraction:sub b:100];
    NSLog(@"100+50=%ld", sub);
    NSLog(@"100+100+50=%ld", difference);
    

    运行结果如下,

    2018-11-01 23:15:06.530080+0800 DesignPatterns[18363:375940] 100+50=5000
    2018-11-01 23:15:06.530758+0800 DesignPatterns[18363:375940] 100+100+50=4900
    

    发现原本运行正常的相减功能发生了错误,原因就是类 B 在给方法起名时无意中重写了父类的方法,造成所有运行相减功能的代码全部调用了类 B 重写后的方法,造成原本运行正常的功能出现了错误。如果按照“里氏替换原则”,只要父类能出现的地方子类就可以出现,而且替换为子类也不会产生任何错误或异常,使用者不需要知道是父类还是子类,是不成立的。

    在平时的日常开发中,通常会通过重写父类的方法来完成新的功能,这样写起来虽然简单,但是整个继承体系的可复用性会比较差,特别是运用多态比较频繁时,程序运行出错的几率非常大。

    原则三、依赖倒置原则(Dependence Inversion Principle,简称DIP)

    它指的是:模块间的依赖通过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系是通过接口或抽象类产生的。即依赖抽象,而不依赖具体的实现。

    原则四、接口隔离原则(Interface Segregation Principle,简称ISP)

    它的定义是:客户端不应该依赖它不需要的接口。

    它的目的是:解开系统的耦合,从而容易重构更改。

    原则五、迪米特法则(Law of Demeter,简称LOD)

    它的定义是:一个对象应该对其他对象有最少的了解

    通俗的说,一个类应该对自己需要耦合或调用的类知道的越少越好,类的内部如何实现与调用者或依赖者没关系。

    原则六、开闭原则(Open Close Principle,简称OCP)

    在应用程序开发中,一个如类、模块和函数应该对应扩展是开放的,对于修改是封闭的。
    核心思想:尽量通过扩展应用程序中的类、模块和函数来解决不同的需求场景,而不是通过直接修改已有的类、模块和函数。

    这个意思就是说,当一个类实现了一个功能的时候,如果想要改变这个功能不是去修改代码,而是通过扩展的方式去实现。实现该类提供的接口函数,然后注入到该类中,通过这种函数去实现功能的改变。

    相关文章

      网友评论

        本文标题:浅谈面向对象的六大设计原则

        本文链接:https://www.haomeiwen.com/subject/mbfdtqtx.html