里氏替换原则

这是SOLID原则这一系列的第四篇文章,主要来描述里氏替换(LSP)原则。LSP指定所有引用基类的地方必须能透明地使用其子类的对象。
  里氏替换原则(LSP)适用于继承的层级结构。它指定你应该设计你的类,以便于客户端依赖关系可以替换为子类,而这一切客户端不知道。因此,所有子类必须以与其基类相同的方式运行。子类的具体功能可能不同,但必须符合基类的预期行为。要成为一个真正的行为子类型,子类不仅必须实现基类的方法和属性,而且还符合其隐含的行为。这必须遵从若干规则。
  第一条规则是在基类的方法的参数和子类之间的匹配参数之间应该存在矛盾。这意味着子类中的参数必须与基类中的参数相同,或者限制性较小。类似地,基类中的方法返回值和其子类之间必须存在协方差。 这指定子类的返回类型必须与基类返回类型相同或更为限制。
  下一条规则关注点是前驱条件和后置条件。类的前置条件是在采取行动之前必须到位的规则。例如,在调用从数据库读取的方法之前,您可能需要满足数据库连接打开的前置条件。后置条件描述进程完成后对象的状态。例如,假定数据库连接在执行SQL语句后关闭。LSP指出,基类的前提条件不能通过子类加强,并且子类中的后置条件不能被削弱。
  接下来考虑LSP的不变量?一个不变量描述了一个进程的条件,该进程在进程开始之前是真实的,并且之后保持为真。例如,一个类可能包含一个从一个文件读取文本的方法。如果该方法处理文件的打开和关闭,则不变量可能是该文件在调用之前或之后未打开。为了符合LSP,基类的不变量不能被子类改变。
  下一个规则是历史约束。根据其性质,子类包括其超类的所有方法和属性。他们还可以增加其他成员。历史约束说新的或修改的成员不应该以基类不允许的方式修改对象的状态。例如,如果基类表示具有固定大小的对象,则子类不应允许修改此大小。
  LSP最后一个规则指定一个子类不应该抛出不被基类抛出的异常,除非它们是基类可能抛出的异常的子类型。
  上述规则不能由编译器控制或被面向对象编程语言所限制。相反,您必须仔细考虑类层次结构的设计和将来可能被子类化的类型。如果不这样做,就有可能创建破坏规则并在依赖于它们的类型中创建错误的子类。不符合LSP的一个常见场景是当客户端类检查其依赖关系的类型时。这可能是通过读取人为描述其类型的对象的属性或通过使用反射来获得该类型。通常,switch语句将用于根据依赖关系的类型执行不同的动作。这种额外的复杂性也违反了开放/封闭原则(OCP),因为客户端类将需要被修改,因为引入了进一步的子类。

代码示例

为了演示LSP的应用,我们可以考虑违反它的代码,并解释如何重构类遵循原则。 以下代码显示了几个类的概要:

public class Project
{
    public Collection<ProjectFile> ProjectFiles { get; set; }
 
    public void LoadAllFiles()
    {
        foreach (ProjectFile file in ProjectFiles)
        {
            file.LoadFileData();
        }
    }
 
    public void SaveAllFiles()
    {
        foreach (ProjectFile file in ProjectFiles)
        {
            if (file as ReadOnlyFile == null)
                file.SaveFileData();
        }
    }
}
 
 
public class ProjectFile
{
    public string FilePath { get; set; }
 
    public byte[] FileData { get; set; }
 
    public void LoadFileData()
    {
        // Retrieve FileData from disk
    }
 
    public virtual void SaveFileData()
    {
        // Write FileData to disk
    }
}
 
 
public class ReadOnlyFile : ProjectFile
{
    public override void SaveFileData()
    {
        throw new InvalidOperationException();
    }
}

第一个类代表一个包含多个文件的项目类。包括两种方法,用于加载每个项目文件的文件数据,并将所有文件保存到磁盘。 第二类描述一个项目文件。 这具有文件名的属性和加载文件数据的字节数组。 两种方法允许加载或保存文件数据。
  以后可能会将第三类添加到其他两个类中,也许当创建一个新的需求时,某些项目文件将是只读的。 ReadOnlyFile类从ProjectFile继承其功能。 但是,由于只读文件无法保存,所以SaveFileData方法已被覆盖,从而抛出无效的操作异常。
  ReadOnlyFile类以多种方式违反了LSP。 虽然基类的所有成员都已实现,但客户端无法替代ProjectFile对象的ReadOnlyFile对象。 这在SaveFileData方法中是清楚的,它引入了一个不能被基类抛出的异常。 接下来,基类中的SaveFileData方法的后置条件是文件已在磁盘上更新。 子类不是这样。 最后一个问题可以在Project类的SaveAllFiles方法中看到。 在这里,程序员添加了一个if语句,以确保在尝试保存文件之前该文件不是只读的。 这样就违反了LSP和OCP,因为必须修改Project类以允许检测到新的ProjectFile子类。

代码重构

有多种方式可以重构代码以符合LSP。 一个如下所示。这里的Project类已被修改为包含两个collection代替之前的一个collection。 一个collection包含项目中的所有文件,另一个只包含对可写文件的引用。LoadAllFiles方法加载所有文件数据到AllFiles collection中。由于WriteableFiles集合中的文件将是相同引用的子集,所以数据将通过这些参数可见。SaveAllFiles方法已被替换为只保存可写文件的方法。
  ProjectFile类现在只包含一种加载文件数据的方法。这个方法对于只读文件和可写文件都是具有的。WriteableFile类继承自ProjectFile,并添加了一个保存文件的方法。层次结构的这种颠倒意味着代码现在符合LSP。
  下面的是重构后的代码:

public class Project
{
    public Collection<ProjectFile> AllFiles { get; set; }
    public Collection<WriteableFile> WriteableFiles { get; set; }
 
    public void LoadAllFiles()
    {
        foreach (ProjectFile file in AllFiles)
        {
            file.LoadFileData();
        }
    }
 
    public void SaveAllWriteableFiles()
    {
        foreach (WriteableFile file in WriteableFiles)
        {
            file.SaveFileData();
        }
    }
}
 
 
public class ProjectFile
{
    public string FilePath { get; set; }
 
    public byte[] FileData { get; set; }
 
    public void LoadFileData()
    {
        // Retrieve FileData from disk
    }
}
 
 
public class WriteableFile : ProjectFile
{
    public void SaveFileData()
    {
        // Write FileData to disk
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,718评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,683评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,207评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,755评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,862评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,050评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,136评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,882评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,330评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,651评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,789评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,477评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,135评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,864评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,099评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,598评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,697评论 2 351

推荐阅读更多精彩内容