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 ... 了,自己想想吧.....都不敢想了!那就来优化一下,先看张类图

图1.1

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

/// <summary>
/// 现在来了只比较大的鸟,叫鸵鸟,继承了鸟类
/// </summary>
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,我可以飞

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


图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
{
    /// <summary>
    /// 鸟有自己可以飞的方法
    /// </summary>
    public void Fly()
    {
        Console.WriteLine($"我是{this.Name},我可以飞");
    }
}

public class Ostrich : Animal
{
    //do nothing
}

public class Sparrow : Bird
{
    //do nothing
}

public class Peacock : Bird
{
    /// <summary>
    /// 孔雀可以开屏
    /// </summary>
    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

图3.1 依赖倒置原则的类图

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

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

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

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

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

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

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

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

    /// <summary>
    /// 学员学习开车,只依赖了抽象(ICar接口)
    /// </summary>
    /// <param name="car"></param>
    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();
    }
}

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容

  • 转载自 设计模式六大原则[http://www.uml.org.cn/sjms/201211023.asp#3] ...
    厨子阅读 1,088评论 2 5
  • 设计模式六大原则(1):单一职责原则 定义:不要存在多于一个导致类变更的原因。通俗的说,即一个类只负责一项职责。 ...
    Jabir_Zhang阅读 640评论 0 3
  • 转载标注声明:http://www.uml.org.cn/sjms/201211023.asp 目录:[设计模式六...
    Bloo_m阅读 706评论 0 7
  • 设计模式六大原则 设计模式六大原则(1):单一职责原则 定义:不要存在多于一个导致类变更的原因。通俗的说,即一个类...
    viva158阅读 763评论 0 1
  • 周末的时候要去打针,因为在外地上学,人生地不熟的,在加上自己是个严重路痴,就拉了一个好朋友陪我去医院。写这篇文章的...
    心蕊啊阅读 364评论 0 2