面向对象设计原则

1.单一职责原则:

一个类最好只负责一件事,只有一个引起它变化的原因。

违背单一职责原则

  • 在一个方法中写分支判断,分别取执行不同的逻辑,功能虽然可以实现,但如果修改需求,就会变得很不稳定

遵循单一职责原则:

  • 拆分:父类 + 各个不同的实现类

举例:

class Animal
{
    public void breathe(string animal)
    {
        Debug.Log(animal + "呼吸空气");
    }
}

public class test : MonoBehaviour
{
    Animal animal = new Animal();
    void Start()
    {
        animal.breathe("牛");
        animal.breathe("羊");
        animal.breathe("猪");
    }
}

在这里插入图片描述


现在当需求变动,并不是所有的动物都呼吸空气的,比如鱼就是呼吸水的。
修改时如果遵循单一职责原则,需要将Animal类细分为陆生动物类Terrestrial,水生动物Aquatic

class Terrestrial
{
    public void breathe(string animal)
    {
        Debug.Log(animal + "呼吸空气");
    }
}

class Aquatic
{
    public void breathe(string animal)
    {
        Debug.Log(animal + "呼吸水");
    }
}

public class test : MonoBehaviour
{
    void Start()
    {
        Terrestrial terrestrial = new Terrestrial();
        terrestrial.breathe("牛");
        terrestrial.breathe("羊");
        terrestrial.breathe("猪");

        Aquatic aquatic = new Aquatic();
        aquatic.breathe("鱼");
    }
}

在这里插入图片描述


但是我们会发现如果这样修改花销是很大的,除了将原来的类分解之外,还需要修改客户端。
而直接修改类Animal来达成目的虽然违背了单一职责原则,但花销却小的多,代码如下:

class Animal
{
    public void breathe(string animal)
    {
        if ("鱼" == animal)
        {
            Debug.Log((animal + "呼吸水"));
        }
        else
        {
            Debug.Log((animal + "呼吸空气"));
        }
    }
}

public class test : MonoBehaviour
{
    void Start()
    {
        Animal animal = new Animal();
        animal.breathe("牛");
        animal.breathe("羊");
        animal.breathe("猪");
        animal.breathe("鱼");
    }
}

可以看到,这种修改方式要简单的多。但是却存在着隐患:有一天需要将鱼分为呼吸淡水的鱼和呼吸海水的鱼,则又需要修改Animal类的breathe方法。
这种修改方式直接在代码级别上违背了单一职责原则,虽然修改起来最简单,但隐患却是最大的。


class Animal
{
    public void breathe(string animal)
    {
        Debug.Log(animal + "呼吸空气");
    }

    public void breathe2(string animal)
    {
        Debug.Log(animal + "呼吸水");
    }
}
public class test : MonoBehaviour
{
    void Start()
    {
        Animal animal = new Animal();
        animal.breathe("牛");
        animal.breathe("羊");
        animal.breathe("猪");
        animal.breathe2("鱼");
    }
}

可以看到,这种修改方式没有改动原来的方法,而是在类中新加了一个方法,这样虽然也违背了单一职责原则,但在方法级别上却是符合单一职责原则的,因为它并没有动原来方法的代码。这三种方式各有优缺点。

衡量使用:

  • 逻辑足够简单,可以在代码级别上违反单一职责原则。
  • 类中方法足够少,可以在方法级别违反单一职责原则。
  • 类相对稳定,扩展变化少,可以在考虑在类一级别违反单一职责原则。
  • 如果不同职责,总一起变化,一定要遵循单一职责原则进行拆分。

单一职责原则优点

  • 可以降低类的复杂度,一个类只负责一项职责,其逻辑肯定要比负责多项职责简单的多;
  • 提高类的可读性,提高系统的可维护性;
  • 变更引起的风险降低,变更是必然的,如果单一职责原则遵守的好,当修改一个功能时,可以显著降低对其他功能的影响。

2.里氏替换原则:

子类可以扩展父类的功能,但不能改变父类原有的功能。原则上子类对象是可以赋值给父类对象的,也可以说子类可以替换父类,并且可以出现在父类能够出现的任何地方。反过来父类对象不能替换子类。

eg1:

子类可以替换父类并且可以出现在父类能够出现的任何地方。

class Person
{
    public void Speak()
    {
        Debug.Log("人在说话");
    }
}

class ZhangSan : Person
{
    public void Speak()
    {
        Debug.Log("张三说话");
    }
}

 void Start()
    {
        //Person p1 = new Person();
        //p1.Speak();//打印:“人在说话”
        //ZhangSan z = new ZhangSan();
        //z.Speak();//打印:“张三说话”

        //里氏替换
        Person p1 = new ZhangSan();
        p1.Speak();

    }

在这里插入图片描述
结论:当创建一个子类对象是,声明的是父类对象,则只能调用父类方法。

eg2:

我们先隐藏子类中的方法,然后声明子类,去调用方法。

class ZhangSan : Person
{
    //public void Speak()
    //{
    //    Debug.Log("张三说话");
    //}
}
  void Start()
    {
        ZhangSan z = new ZhangSan();
        z.Speak();
    }

在这里插入图片描述

打印信息仍然是“人在说话”,所以:**声明的是子类对象,这先看子类对象中是否有该方法,没有则在父类中寻找,有则调用。
因为C#中的继承属于强继承,子类拥有父类向下公开的所有特征。
如果子类出现了不应该有的职责那么应该断掉继承;重新建立一个父类,包含子类应该有的职责。

eg3:

Person p1 = new Person();//子类隐式转换为父类
ZhangSan z = (ZhangSan)p1;//父类强制转换为子类

子类转换为父类是隐式转换,父类转换为子类必须强转。

里氏替换原则总结:

  • 父类有的,子类必须要有。否则断掉继承。
  • 子类可以有自己的属性和行为。
  • 父类已经实现的,子类不要再写。如果想修改父类行为,通过abstract/virtual关键字声明。

3.迪米特法则(知道最少原则)

一个对象应该对其他对象保持最少了解。

程序就是类和类之间交互堆积出来的。类与类之间的关系如下:

  • 纵向:继承≈实现(最密切)
  • 横向:聚合>组合>关联>依赖(出现在方法内部)

简单的定义:只与直接的朋友通信。
直接的朋友:每个对象都会与其他对象有耦合关系,只要两个对象之间有耦合关系,我们就说这两个对象之间是朋友关系。
出现成员变量、方法参数、方法返回值中的类我们称为直接的朋友。出现在局部变量中的类,则不是直接朋友:
陌生的类最好不要作为局部变量的形式出现在类的内部。

eg:违背迪米特法则
class Student
{
    public int ID { get; set; }//学号
}
class Class
{
    public List<Student> StudentList { get; set; }
}

class School
{
   public List<Class> ClassList{get;set;}

   public void Manager()
   {
       foreach (Class c in ClassList)
       {
           foreach (Student s in c.StudentList)
           {
               Debug.Log(s.Id+"校服裤腿我改定了!")}
       }
   }
}

我们看到,School类中直接朋友只要Class类,但却与类中不知道的Student类进行通信。这样就违背了迪米特法则。
类与类之间关系越密切,耦合度就越大。遵循迪米特法则的目的就是尽量降低类与类之间的耦合。
eg:遵循迪米特法则

class Student
{
    public int ID { get; set; }//学号
    public void Speak()
    {
        Debug.Log(this.ID + "校服裤腿我改定了!");
    }
}

class Class
{
    public List<Student> StudentList { get; set; }
   public  void Manager()
    {
        foreach (Student item in StudentList)
        {
            item.Speak();
        }
    }
}

class School
{
   public List<Class> ClassList{get;set;}

   public void Manager()
   {
       foreach (Class item in ClassList)
       {
           item.Manager();
       }
   }
}

4.依赖倒置原则

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

  • 无论是创建型模式、结构型模式还是行为型模式,归根结底都是寻找软件中可能存在的“变化”。然后利用抽象的方式对变化进行封装,由于抽象没有实现,就代表了无限可能性。使得其扩展成为了可能。

举例:

//读物 
public interface IRead { void Read(); }
//读者
public interface IReader{ void Read(IRead read); }

//文学经典类
public class LiteraryClassic : IRead
{
    public void Read()
    {
        Debug.Log("阅读文学经典,滋润内心心灵!");
    }
}

//小说类
public class Novel : IRead
{
    public void Read()
    {
        Debug.Log("阅读小说,放松一下!");
    }
}
//小明同学
public class XiaoMingSchoolmate : MonoBehaviour,IReader
{
    
    public void Read(IRead read)
    {
        read.Read();
    }

    void Start()
    {
        LiteraryClassic lc = new LiteraryClassic();
        Novel n = new Novel();

        Read(lc); 
        Read(n);
    }
}

在这里插入图片描述


如何遵循依赖倒置原则?
  • 每个类尽量都要有接口或抽象类,或者抽象类和接口都有: 依赖倒置原则的基本要求,有抽象才能依赖倒置
  • 变量尽量不要持有具体类的引用。
  • 不要让类派生自具体类。
  • 不要覆盖基类中已实现的方法。(里式替换原则)
  • 结合里式替换原则来使用。结合里式替换原则和依赖倒置原则我们可以得出一个通俗的规则,接口负责定义public属性和方法,并且声明与其他对象的依赖关系,抽象类负责公共构造部分的实现,实现类准确的实现业务逻辑,同时在适当的时候对父类进行细化。

5.接口隔离原则

使用多个专门接口取代一个统一接口

在这里插入图片描述

  • 未遵循接口隔离原则
//接口
interface I
{
    void method1();
    void method2();
    void method3();
    void method4();
    void method5();
}

class A
{
    public void depend1(I i)
    {
        i.method1();
    }
    public void depend2(I i)
    {
        i.method2();
    }
    public void depend3(I i)
    {
        i.method3();
    }
}

class B : I
{
    public void method1()
    {
        Debug.Log("类B实现接口I的方法1");
    }
    public void method2()
    {
        Debug.Log("类B实现接口I的方法2");
    }
    public void method3()
    {
        Debug.Log("类B实现接口I的方法3");
    }
    //对于类B来说,method4和method5不是必需的,但是由于接口A中有这两个方法,
    //所以在实现过程中即使这两个方法的方法体为空,也要将这两个没有作用的方法进行实现。
    public void method4() { }
    public void method5() { }
}

class C
{
    public void depend1(I i)
    {
        i.method1();
    }
    public void depend2(I i)
    {
        i.method4();
    }
    public void depend3(I i)
    {
        i.method5();
    }
}

class D : I
{
    public void method1()
    {
        Debug.Log("类D实现接口I的方法1");
    }
    //对于类D来说,method2和method3不是必需的,但是由于接口A中有这两个方法,
    //所以在实现过程中即使这两个方法的方法体为空,也要将这两个没有作用的方法进行实现。
    public void method2() { }
    public void method3() { }

    public void method4()
    {
        Debug.Log("类D实现接口I的方法4");
    }
    public void method5()
    {
        Debug.Log("类D实现接口I的方法5");
    }
}
public class test : MonoBehaviour
{
    void Start()
    {
        A a = new A();
        a.depend1(new B());
        a.depend2(new B());
        a.depend3(new B());

        C c = new C();
        c.depend1(new D());
        c.depend2(new D());
        c.depend3(new D());
    }
}

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

  • 遵循接口隔离原则
interface I1
{
    void method1();
}

interface I2
{
    void method2();
    void method3();
}

interface I3
{
    void method4();
    void method5();
}

class A
{
    public void depend1(I1 i)
    {
        i.method1();
    }
    public void depend2(I2 i)
    {
        i.method2();
    }
    public void depend3(I2 i)
    {
        i.method3();
    }
}

class B : I1, I2
{
    public void method1()
    {
        Debug.Log("类B实现接口I1的方法1");
    }
    public void method2()
    {
        Debug.Log("类B实现接口I2的方法2");
    }
    public void method3()
    {
        Debug.Log("类B实现接口I2的方法3");
    }
}

class C
{
    public void depend1(I1 i)
    {
        i.method1();
    }
    public void depend2(I3 i)
    {
        i.method4();
    }
    public void depend3(I3 i)
    {
        i.method5();
    }
}

class D : I1, I3
{
    public void method1()
    {
        Debug.Log("类D实现接口I1的方法1");
    }
    public void method4()
    {
        Debug.Log("类D实现接口I3的方法4");
    }
    public void method5()
    {
        Debug.Log("类D实现接口I3的方法5");
    }
}
public class test : MonoBehaviour
{
    void Start()
    {
        A a = new A();
        a.depend1(new B());
        a.depend2(new B());
        a.depend3(new B());

        C c = new C();
        c.depend1(new D());
        c.depend2(new D());
        c.depend3(new D());
    }
}
采用接口隔离原则对接口进行约束时注意:
  1. 接口尽量小,但是要有限度。对接口进行细化可以提高程序设计灵活性是不挣的事实,但是如果过小,则会造成接口数量过多,使设计复杂化。所以一定要适度。
  2. 为依赖接口的类定制服务,只暴露给调用的类它需要的方法,它不需要的方法则隐藏起来。只有专注地为一个模块提供定制服务,才能建立最小的依赖关系。
  3. 提高内聚,减少对外交互。使接口用最少的方法去完成最多的事情。
  4. 运用接口隔离原则,一定要适度,接口设计的过大或过小都不好。设计接口的时候,只有多花些时间去思考和筹划,才能准确地实践这一原则。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值