六大设计原则(C#)

为何要有设计原则,我以为一张图片就能够解释这一切

1、单一职责原则(SRP)

对于一个类而言,应该只有一个发生变化的缘由。(单一职责不只仅是指类)编程

若是一个模块须要修改,它确定是有缘由的,除此缘由以外,若是遇到了其余状况,还须要对此模块作出修改的话,那么就说这个模块就兼具多个职责。举个栗子:ide

此时咱们有个动物类Animal,有个Move()会移动的方法函数

public class Animal
{
    //动物移动的方法
    public void Move(String name)
    {
        Console.WriteLine($"动物{name}跑");
    }
}
class Program
{
    static void Main(string[] args)
    {
        Animal a = new Animal();
        a.Move("");
        Console.ReadKey();
    }
}

此时若是传入一个鱼进去就不太合适了,由于鱼是不会跑只会游的ui

a.Move("");

此时咱们须要兼顾两个职责,第一个就是普通动物移动的方法,第二个就是鱼类的移动方法。咱们修改一下,让这一切变得合理一些spa

第一种设计

public class Animal
{
    //动物移动的方法
    public void Move(String name)
    {
        if (name == "") {
            Console.WriteLine($"动物{name}跑");
        }
        else if (name==""){
            Console.WriteLine($"动物{name}游");
        }
        
    }
}

这种的话其实就是让Move方法兼具普通动物和鱼类移动两个职责(若是你的设计之初就是让Move知足全部动物的移动,此时Move方法仍是兼具一个职责)code

第二种对象

public class Animal
{
    //普通动物移动的方法
    public void RunMove(String name)
    {
        Console.WriteLine($"动物{name}跑");
    }
    //鱼类移动的方法
    public void FishMove(String name)
    {
        Console.WriteLine($"动物{name}游");
    }
}

此时RunMove和FishMove方法的职责是单一的,只管普通动物和鱼类的移动,可是Animal类确是兼具了普通动物和鱼类移动两个职责(若是你的设计之初就是让Animal类知足全部动物的移动,此时Animal仍是兼具一个职责)blog

第三种继承

public class RunAnimal
{
    //普通动物移动的方法
    public void Move(String name)
    {
        Console.WriteLine($"动物{name}跑");
    }
}
public class FishAnimal
{
    //鱼类移动的方法
    public void Move(String name)
    {
        Console.WriteLine($"动物{name}游");
    }
}

class Program
{
    static void Main(string[] args)
    {
        RunAnimal a = new RunAnimal();
        a.Move("");
        FishAnimal f = new FishAnimal();
        f.Move("");
        Console.ReadKey();
    }
}

此时的话RunAnimal类、FishAnimal类和Move方法的职责都是单一的,只作一件事。就拿RunAnimal的Move方法来讲,只有普通动物的移动须要作出改变了,才会对Move方法作出修改。

单一职责原则的优势就是高内聚,使得模块看起来有目的性,结构简单,修改当前模块对于其余模块的影响很低。缺点就是若是过分的单一,过分的细分,就会产生出不少模块,无形之中增长了系统的复杂程度。

2、开闭原则(OCP)

软件中的对象(类,模块,函数等等)应该对于扩展时开放的,可是对于修改是封闭的

一个模块写好了,可是若是还想要修改功能,不要对模块自己进行修改,可能会引发很大的连锁反应,破坏现有的程序,应该经过扩展来进行实现。经过扩展来实现的前提,就是一开始把模块抽象出来,而抽象出来的东西要可以预测到足够多的可能,由于一旦肯定后,该抽象就不能在发生改变。举个栗子:

如今有个Dog类,Food食物类,还有个Feed类 ,根据传入食物喂养Dog类动物

public class Dog
{
    public void eat(DogFood f)
    {
        Console.WriteLine("狗吃" + f.Value);
    }
}
public class Feed
{
    //开始喂食
    public void StartFeed(List<Dog> d, DogFood f)
    {
        foreach (Dog item in d)
        {
            item.eat(f);
        }
    }
}

public class DogFood
{
    public String Value
    {
        get
        {
            return "狗粮";
        }
    }
}

若是有一天,咱们引入了新的种类Tiger老虎类,和新的食物Meat肉类,此时要修改Feed喂食类了,这就违反了开闭原则,只作扩展不作修改,若是要让Feed类符合开闭原则,咱们须要对Dog类和Food类作出一个抽象,抽象出Eat和Food抽象类或者接口,这里我就抽象出两个接口IEat和IFood:

//全部须要进食的动物都要实现此接口
public interface IEat
{
    //此时食物也应该使用接口而不是具体类来接收
    //不然只能接收单一的食物,增长食物的话仍是须要修改
    void eat(IFood food);
}
//全部食物须要实现此接口
public interface IFood
{
    String Value { get; }
}

此时,IEat和IFood是被固定死了,不作修改,这就须要设计之初可以预测到足够多的可能,若是须要添加新的功能(动物或食物),只须要实现对应的接口就好了。

修改原有Dog类和DogFood类实现对应的接口:

public class Dog:IEat
{

    public void eat(IFood food)
    {
        Console.WriteLine("狗吃" + food.Value);
    }
}
public class DogFood:IFood
{

    public String Value
    {
        get
        {
            return "狗粮";
        }
    }
}

修改Feed喂食类,使用接口来接收和使用,使其知足开闭原则:

public class Feed
{
    //使用接口接收,后续能够传入实现该接口的子类,由于用到了协变,就须要使用IEnumerable来接受
    public void StartFeed(IEnumerable<IEat> d, IFood f)
    {
        foreach (IEat item in d)
        {
            item.eat(f);
        }
    }
}

这样的话,若是要添加新的功能,就不须要对Feed进行修改,而是添加新的类:

public class Tiger : IEat
{
    public void eat(IFood food)
    {
        Console.WriteLine("老虎吃" + food.Value);
    }
}

public class Meat : IFood
{
    public string Value
    {
        get
        {
            return "";
        }
    }
}

调用:

static void Main(string[] args)
{
    //喂食
    Feed f = new Feed();
    //
    List<Dog> dog = new List<Dog>(){
        new Dog(),
        new Dog()
    };
    //狗的食物
    DogFood df = new DogFood();
    //开始喂食
    f.StartFeed(dog,df);

    //老虎
    List<Tiger> t = new List<Tiger>() {
        new Tiger()
    };
    //
    Meat m = new Meat();
    //开始喂食
    f.StartFeed(t,m);


    Console.ReadKey();
}

 遵循开闭原则的好处是扩展模块时,无需对已有的模块进行修改,只须要添加新的模块就行,避免了程序修改所形成的风险,抽象化就是开闭原则的关键。

3、依赖倒置原则(DIP)

依赖倒置原则主程序要依赖于抽象接口,不要依赖于具体实现。高层模块不该该依赖底层模块,两个都应该以来抽象。抽象不该该依赖细节,细节应该依赖抽象。

依赖:依赖其实就是耦合性,若是A类依赖B类,那么当B类消失或修改时,对A类会有很大的影响,能够说是A类的存在彻底就是为B类服务,就是说A类依赖B类。

高层模块不该该依赖底层模块,两个都应该依赖抽象:在上一个例子中,做为高层模块Feed喂食类依赖底层模块Dog类和DogFood类(高层和底层就是调用时的关系,由于Feed调用Dog,因此Feed是高层模块),这样的话Feed喂食类只能给Dog类吃DogFood,若是引进了其余动物,Feed类此时是没法完成喂食工做的。后来对Feed类、Dog类和DogFood类作出了修改,让Dog类和DogFood类分别依赖IEat和IFood接口,使Feed类依赖于IEat和IFood接口,这样的话就使得高层模块(Feed)和底层模块(Dog、DogFood)都是依赖于接口。

抽象不该该依赖细节,细节应该依赖抽象:抽象就是抽象类或者接口,细节就是实现抽象类或接口的具体类,这句话的意思其实就是,抽象类或者接口不该该依赖具体的实现类,而应该让具体的实现类去依赖抽象类或者接口。

遵循依赖倒置原则的好处就是下降了模块和模块之间的耦合性,下降了修改模块后对程序所形成的风险。

4、里氏替换原则(LSP)

一个程序中若是使用的是一个父类,那么该程序必定适用于其子类,并且程序察觉不出父类和子类对象的区别。也就是说在程序中,把父类都替换成它的子类,程序的行为没有任何变化。

其实里氏替换原则是很容易理解的,若是想知足里氏替换原则,子类继承父类时,能够有本身的特色,能够重写父类的方法,可是父类中有的方法,子类必须是彻底能够实现的。开闭原则中的例子就是符合里氏替换原则,而关于里氏替换原则的反例也有不少,例如:正方形不是长方形、玩具枪不能杀人、鸵鸟不会飞,这里就拿企鹅不会飞来举个反例:

Birds鸟类、Sparrow麻雀类,全部的鸟类都具备一个飞行时间

public abstract class Birds
{
    //全部鸟类都应该具备飞行速度
    public abstract double FlySpeed();
}

//麻雀类
public class Sparrow : Birds
{
    public override double FlySpeed()
    {
        return 10.5;
    }
}

此时咱们添加一个Penguin企鹅类,由于企鹅也是鸟,因此也应该继承自Birds鸟类

//企鹅
public class Penguin: Birds
{
    //实现飞的方法
    public override double FlySpeed()
    {
        return 0;
    }
}

可是因为Penguin并不会飞,因此飞行速度为0,可是也实现了FlySpeed方法,编译也没有报错啊,可是若是此时有一个Fly方法须要根据鸟类的飞行速度来计算飞行300米所须要的时间

public static double Fly(Birds b)
{
    return 300 / b.FlySpeed();
}

那么,将Penguin企鹅类放入时,则会报出异常,由于300/0是不符合逻辑的,也就不知足里氏替换原则,由于此时做为父类Birds,若是传入子类Penguin,程序就会出错。

不知足里氏替换原则的根本仍是Penguin企鹅类并无彻底继承Birds鸟类,由于实现不了FlySpeed方法,因此此时解决方案有两种,

一种就是在Fly方法中进行判断:

public static double Fly(Birds b)
{
    //若是传入的类型为鸵鸟,默认返回0
    if (b.GetType().Equals(typeof(Penguin)))
    {
        return 0;
    }
    else {
        return 300 / b.FlySpeed();
    }
    
}

这样的话就会违反开闭原则,并且更改代码会很是麻烦,后续添加功能还需修改。

第二种就是Penguin企鹅类不继承Birds鸟类,由于此时企鹅类继承鸟类在此案例中就是强行继承,虽然现实世界中企鹅也是鸟,可是在编程世界中就行不通了。

总结下来实现里氏替换原则的根本就是,不要强行继承,若是继承就要彻底实现。

5、接口隔离原则(ISP)

客户端不该该依赖它不须要的接口;一个类对另外一个类的依赖应该创建在最小的接口上

知足接口隔离原则的前提就是,接口不要设计的太过庞大,什么叫庞大呢?好比一个动物接口就很是庞大,由于若是细分的话,就能够分不少种类的动物,此时动物接口就须要考虑率足够多的状况来保证动物接口后续不被修改,那么一开始设计时,就能够将动物接口根据具体的需求(例如动物是否会飞和游泳)细分为水里游的动物、天上飞的动物和地上跑的动物,若是仍是过于庞大,就再细分。

就好比开闭原则中的IEat接口,就知足了接口隔离原则,该接口只负责吃的接口,全部须要吃的动物均可以实现该接口,而Feed喂食类依赖IEat接口,此时IEat接口也是最小接口。举个反例:

此时Feed喂食类不在依赖于IEat接口,而是依赖于IAnimal接口,全部动物(Dog、Tiger)都实现IAnimal接口

public interface IAnimal
{
    //全部动物都会吃
    void eat(IFood food);
    //全部动物都会呼吸
    void breathe();
    //全部动物都会移动
    void move();
    //........动物的功能确定不止这么多
}


public class Tiger : IAnimal
{
    public void breathe()
    {
        Console.WriteLine("老虎会呼吸");
    }

    public void eat(IFood food)
    {
        Console.WriteLine("老虎吃" + food.Value);
    }

    public void move()
    {
        Console.WriteLine("老虎会跑");
    }
}

public class Dog : IAnimal
{
    public void breathe()
    {
        Console.WriteLine("狗会呼吸");
    }

    public void eat(IFood food)
    {
        Console.WriteLine("狗吃" + food.Value);
    }

    public void move()
    {
        Console.WriteLine("狗会跑");
    }
}
View Code

那么此时让Feed喂食类依赖IAnimal

public class Feed
{
    //使用接口接收,后续能够传入实现该接口的子类
    public void StartFeed(IEnumerable<IAnimal> d, IFood f)
    {
        foreach (IAnimal item in d)
        {
            item.eat(f);
        }
    }
}

这样的话就违反了接口隔离原则,由于Feed喂食类只须要调用对象的eat方法,动物的其余的方法都是不调用的,可是却依赖了IAnimal接口,这样的话就显得很臃肿,并且若是之后不传入动物,该工厂也负责喂养机器人吃电池,是否是依赖IAnimal就不合适了(若是非要让机器人实现IAnimal接口是可行的,不过这太不合理了)。

可是Feed若是依赖IEat接口,那么只要能吃东西就能够实现IEat接口,只要能吃东西就能够传入Feed喂食类喂养,此时Feed类依赖的IEat接口为最小接口。当一个类对另外一个类的依赖创建在最小接口上时,该类基本上负责调用此接口中的全部内容,不须要接口中有多余的方法。

6、迪米特法则(LoD)(最少知识原则(LKP))

一个对象应当对其余对象有尽量少的了解,不要和陌生人说话。

首先来讲一下什么叫“陌生人”,首先咱们有个类A,A自己确定是A的朋友,A的属性一样也是A的朋友,若是A的方法须要参数是B类型的,那么B也是A的朋友,还有A类中直接创造的对象也是A类的朋友,其余类对于A类来讲就是“陌生人”。若是想要知足迪米特法则,就要尽量少的写public方法和变量,不须要让别的对象知道的方法或者字段就不要公开。

其实迪米特法则的目的也是为了减小模块间的依赖,下降模块间的耦合性,这样才能提升代码的复用率。举个栗子:

动物园中有不少动物,而管理员须要天天记录动物的数量

动物和动物园

//动物
public class Animal
{

}

//动物园 
public class Zoo
{
    public List<Animal> animals = new List<Animal>();
}

管理员

//管理员
public class ZooMan
{
    //根据动物园检查动物的数量
    public void CheckCount(Zoo z)
    {
        //获取全部的动物
        List<Animal> animals = z.animals;
        //获取全部动物的数量
        int count = animals.Count;
        //输出
        Console.WriteLine(count);
    }
}

ZooMan管理员与Animal动物类并无直接的朋友关系,可是却发生了依赖关系,这样的设计显然违反了迪米特法则。咱们应该对此程序进行修改,让动物园不对外开放animals属性,将计算动物全部数量的方法交由Zoo动物园来完成,对外提供GetAnimalCount方法获取数量,使得ZooMan管理员与Animal不产生依赖关系,修改以下:

//动物园 
public class Zoo
{
    //私有全部的动物,只有当前动物园能够访问
    private List<Animal> animals = new List<Animal>();
    //可是对外提供获取全部动物数量的方法
    public int GetAnimalCount()
    {
        return animals.Count;
    }
}

管理员获取数量只须要 int count = z.GetAnimalCount();就能够作到了。

迪米特法则的虽然能够直接避免ZooMan管理员与Animal动物类产生依赖,可是却须要Zoo动物园对外提供一个GetAnimalCount方法,若是盲目的追求迪米特法则时,就会产生很对相似于GetAnimalCount这样的“中间”方法或模块,来传递间接的调用,有可能形成模块间通信效率下降,不容易协调。

其实这么多原则,遵循与不遵循对于实现特定的功能没有丝毫影响,但程序不多是一成不变的,重要的是后续须要修改或者添加新的模块时,你的程序可否作到“拥抱变化” ?

 

 若是有错误或者疑问欢迎留言

相关文章
相关标签/搜索