C#设计模式:六大原则(上)

  面向对象设计原则,是一种指导思想,在程序设计过程中,要尽量的去遵守这些原则,用于解决面向对象设计中的可维护性,可复用性以及可扩展性。常用的,就是我们日常所说的6大原则,分别是:单一职责(SRP)、里氏替换原则(LSP)、依赖倒置原则(DIP)、接口隔离原则(ISP)、迪米特法则(LOD)、开闭原则(OCP)。下面就来分别说说这些原则:

一、 单一职责(Single Reponsibility Principle,SRP)

一个类只负责一项职责。换种说法,就一个类而言,应该只有一个引起它变化的原因。

  这个原则最简单,也是备受争仪,较难运用的一个原则。这个和类的职责有关,主观性比较强,没有一个量化的标准,开发设计人员对职责怎么定义,以及怎么划分类的职责,和每个人的分析设计思想及相关实践经验,都有比较大的关系。对于一个类而言,不能承担太多的职责,职责过多,就会耦合在一起,一个职责变化,会影响其它职责的运作。过多的耦合,还会影响其复用性。对于软件系统而言,小到类的方法,接口的定义,大到模块,类库,也都是一样的。单一职责的指导思想,就是为了实现高内聚,低耦合

下面来看一个简单的例子:以一个建筑工为例,这个人比较厉害,泥瓦工,木工,油漆工都能做,代码如下:

public class Builder
{
    public void Work()
    {
        Console.WriteLine("我开始做泥瓦工的活了");
        Console.WriteLine("我开始做木工的活了");
        Console.WriteLine("我开始做油漆工的活了");
    }
}

  这个代码简单的不能再简单了,一看就懂。不管做啥,都整到一个方法里面,就是个大杂烩,这里只是显示,没什么逻辑,如果逻辑多的话,只是判断的话,你就要各种 if else ... 了,自己想想吧.....都不敢想了!那就来优化一下,先看张类图

C#设计模式:六大原则(上)_第1张图片
图1.1

  一般情况下,都会想到这种方式,分成三个不同的方法来处理,代码很简单,这里就不贴出来了,但这样同样的有问题,一个人(类)做这么多事情,你不会很“累”吗?说白点,就是职责太多,即要做泥瓦的活,又要做木工的活,如果哪天赶工,临时来个只做木工的或其它工种的,你就要改类,木工的代码也没法复用,这就违背了单一职责。因此,需要对类进行拆分,使其满足单一职责,重构后如图1.2:


C#设计模式:六大原则(上)_第2张图片
图1.2 职责分明的建筑工人类图

  各做各的,互不影响,就如同现在建筑工人分工,做什么都很明确。引入到软件设计里面,类的复杂性降低了,可读性也同时提高了,最重要的是职责划分也明确了。当然,也就更容易维护了。
代码如下

public interface IBuilder
{
    void Work();
}

public class TilerBuilder : IBuilder
{
    public void Work()
    {
        Console.WriteLine("我是泥瓦工,开始工作了");
    }
}

public class WoodBuilder : IBuilder
{
    public void Work()
    {
        Console.WriteLine("我是木工,开始工作了");
    }
}

public class PaintBuilder : IBuilder
{
    public void Work()
    {
        Console.WriteLine("我是泥瓦工,开始工作了");
    }
}

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

所有使用基类的地方,都可以使用其子类来代替,而且行为不会有任务变化

  面向对象语言的继承是项很牛的设计,普通类间父子继承,抽象类以及接口,它们之间的相互关联与纠缠,看似复杂,实则给我们带来很多好处:代码共享,减少创建类的工作量,提高了代码的复用性;提高了代码的可扩展性与项目的开放性,实现父类方法后,子类可任意扩展,想想一些框架的扩展接口不都是通过继承来完成的么。里氏替换原则就是为良好的继承定义了一个规范。主要如下:

  1. 子类必须完全实现父类的属性和方法,如果子类不拥有父类的全部属性或者行为,不能强行继承,要断掉继承。
  2. 子类可以拥有父类没有的属性或者方法,子类出现的地方,父类不能代替。

一直在纠结举个什么例子,还是拿鸟来说事吧,通俗易懂。先看个反例,鸟类都需要吃东西,都需要喝水,还可以飞,代码如下:

public class Bird
{
    public string Name => this.GetType().Name;

    public void Eat()
    {
        Console.WriteLine($"我是{this.Name},我需要吃东西");
    }

    public void Drink()
    {
        Console.WriteLine($"我是{this.Name},我需要喝水");
    }

    public void Fly()
    {
        Console.WriteLine($"我是{this.Name},我可以飞");
    }
}

/// 
/// 现在来了只比较大的鸟,叫鸵鸟,继承了鸟类
/// 
public class Ostrich : Bird
{
    //Do nothing
}

调用一下

class Program
{
    static void Main(string[] args)
    {
        try
        {
            Bird bird = new Ostrich();
            bird.Eat();
            bird.Drink();
            bird.Fly();
        }
        catch (Exception e)
        {
            Console.WriteLine(e.Message);
        }
        Console.Read();
    }
}

运行结果:
我是Ostrich,我需要吃东西
我是Ostrich,我需要喝水
我是Ostrich,我可以飞

  是不是出问题了,鸵鸟显然是不能飞的,也继承了鸟类,这就违背了里氏替换原则。鸵鸟虽然是鸟类,可不能飞,比较特珠,就需要断掉继承。鸵鸟这是说话了,我不能飞,我也要吃和喝啊,怎么办?那你都不属于动物吗,我们来使用里氏替换原则重构一下:


C#设计模式:六大原则(上)_第3张图片
图2.1 重构后的类图

  类图不复杂,很容易理解,抽出一个共同的基类动物,然后继承各自的功能,互不影响,根据需求还可以有自己的方法,孔雀可以开屏了。
  到这里,你是不是看出点什么问题了,如果父类有什么改动或需要去除一个方法什么的,这就麻烦了,这就是里氏替换的一个缺陷了:继承是侵入式的,代码灵活性受到限制,增强了耦合性。
代码如下:

public class Animal
{
    public string Name => this.GetType().Name;

    public void Eat()
    {
        Console.WriteLine($"我是{this.Name},我需要吃东西");
    }

    public void Drink()
    {
        Console.WriteLine($"我是{this.Name},我需要喝水");
    }
}

public class Bird : Animal
{
    /// 
    /// 鸟有自己可以飞的方法
    /// 
    public void Fly()
    {
        Console.WriteLine($"我是{this.Name},我可以飞");
    }
}

public class Ostrich : Animal
{
    //do nothing
}

public class Sparrow : Bird
{
    //do nothing
}

public class Peacock : Bird
{
    /// 
    /// 孔雀可以开屏
    /// 
    public void Open()
    {
        Console.WriteLine($"我是{this.Name},我要开屏了,我不是老孔雀");
    }
}

调用如下

class Program
{
    static void Main(string[] args)
    {
        try
        {
            {
                Bird bird = new Sparrow();
                bird.Fly();  //可以飞
            }

            {
                //Bird bird = new Peacock(); //子类出现的地方父类不能代替
                Peacock bird = new Peacock();
                bird.Fly();
                bird.Open();
            }
        }
        catch (Exception e)
        {
            Console.WriteLine(e.Message);
        }
        Console.Read();
    }
}

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

高层模块不应该依赖低层模块,两者都应该依赖其抽象,不要依赖细节

  在C#中,抽象就是指接口或者抽象类,两者都不能直接进行实例化;细节就是实现类,就是实现了接口或继承了抽象类而产生的类就是实现类,可以直接被实例化。所谓的高层与低层,每个逻辑实现都是由原始逻辑组成,原始逻辑就属于低层模块,像我们常说的三层架构,业务逻辑层相对数据层,数据层就属于低层模块,业务逻辑层就属于高层模块,是相对来说的。依赖倒置原则就是程序逻辑在传递参数或关联关系时,尽量引用高层次的抽象,不使用具体的类,即是使用接口或抽象类来引用参数,声明变量以及处理方法返回值等。这样就要求具体的类就尽量不要有多余的方法,否则就调用不到。说简单点,就是“面向接口编程”。
  现在学车很流行,驾校也很多(学习的车是真心的破旧),我当时都是些老捷达,皇冠之类的,根据依赖倒置的原则,我们来实现下这个过程,如图3.1

C#设计模式:六大原则(上)_第4张图片
图3.1 依赖倒置原则的类图

一个学生的抽象类,一个汽车的接口,分别定义了各自的职能,具体代码如下:

public interface ICar
{
    /// 
    /// 汽车是可以开动的
    /// 
    void Run();
}

public class Jetta : ICar
{
    public void Run()
    {
        Console.WriteLine("捷达车开动起来了...");
    }
}

public class Crown : ICar
{
    public void Run()
    {
        Console.WriteLine("皇冠车开动起来了...");
    }
}

/// 
/// 用的抽象方法,考虑学员会有共性的内容
/// 
public abstract class BaseStudent
{
    public string Name { get; set; }

    /// 
    /// 给个构造函数,用来初始化名子
    /// 
    /// 
    protected BaseStudent(string name)
    {
        this.Name = name;
    }

    /// 
    /// 学员要学习开车
    /// 这里用的是虚方法,实现可确定的基本操作
    /// 由于每个学员学习过程可能不同,可进行重写操作
    /// 
    /// 
    public virtual void LearnDrive(ICar car)
    {
        Console.WriteLine($"{this.Name}开始学车了");
        car.Run();
    }
}

public class Student : BaseStudent
{
    public Student(string name) : base(name)
    {
    }

    /// 
    /// 学员学习开车,只依赖了抽象(ICar接口)
    /// 
    /// 
    public override void LearnDrive(ICar car)
    {
        //加入自己的内容
        Console.WriteLine($"{this.Name}有些紧张,调整了下情绪");
        base.LearnDrive(car);
    }
}

在我们的场景中,代码如下所示

class Program
{
    static void Main(string[] args)
    {
        try
        {
            //张三开皇冠车都是依赖上层抽象
            //不同的学员开不同的车,就很容易处理了...
            BaseStudent student = new Student("张三");
            ICar car = new Jetta();
            student.LearnDrive(car);

            //张三开皇冠车
            ICar crown = new Crown();
            student.LearnDrive(crown);
        }
        catch (Exception e)
        {
            Console.WriteLine(e.Message);
        }
        Console.Read();
    }
}

  运行结果,就不贴出来了。这里注意到了没有,是不是有些地方很熟悉,这不就是里氏替换原则吗?其实,它们之间是相辅相成的,里氏替换是基础,依赖倒置是方法和手段。刚开始了解设计模式时,我就被这两个原则之间整的有点迷惑了,在代码设计的过程中,它们基本上都是同时出现的。

你可能感兴趣的:(C#设计模式:六大原则(上))