[译]软件开发SOLID原则:原理解析与代码示例

原文:
https://itnext.io/solid-principles-explanation-and-examples-715b975dcad4


SOLID是面向对象编程领域的5个重要的设计原则的简称。 这5个设计原则是由Robert C. Martin (Uncle Bob)在他2000年发表的论文 Design Principles and Design Patterns中首先提到的。然而事实上,SOLID这个简称是后来由Michael Feathers整理确定的。
引入这些设计原则的目的是使得软件设计更易于理解,便于维护和扩展。作为软件工程师,这5条原则是必须理解的。
在本文中,我会讲解这5大设计原则,通过代码示例说明为何违反了SOLID原则,以及如何修改以符合这些原则。
代码示例会采用C#,但对其他OOP语言也是适用的。

S — 单一职责原则

在程序设计中,单一职责原则指出,每一个模块或类应该承担整个软件中一块独立的功能。
生活中你可能听说过这样一句话:“只做一件事,但要把它做好”。这句话就是阐述了单一职责原则。
Robert C. Martin的文章Principles of Object Oriented Design中,他把职责定义为“改变的理由”,总结起来就是,要修改一个模块或者类有且只有一个理由。
以下面的代码为例,看看它是如何违反单一职责原则的:

class User
{
    void CreatePost(Database db, string postMessage)
    {
        try
        {
            db.Add(postMessage);
        }
        catch (Exception ex)
        {
            db.LogError("An error occured: ", ex.ToString());
            File.WriteAllText("\LocalErrors.txt", ex.ToString());
        }
    }
}

我们可以看到,CreatePost()这个方法的职责过多,它既负责创建邮件,又完成了记录错误到数据库和本地文件,违反了单一职责原则。
我们尝试修改:

class Post
{
    private ErrorLogger errorLogger = new ErrorLogger();

    void CreatePost(Database db, string postMessage)
    {
        try
        {
            db.Add(postMessage);
        }
        catch (Exception ex)
        {
            errorLogger.log(ex.ToString())
        }
    }
}

class ErrorLogger
{
    void log(string error)
    {
      db.LogError("An error occured: ", error);
      File.WriteAllText("\LocalErrors.txt", error);
    }
}

通过抽象出处理错误日志这个独立功能,就不会违反单一职责原则了。现在我们有两个类,各自有独立的职责互不干涉,一个负责创建邮件,一个负责记录错误日志。

O - 开闭原则

开闭原则指的是:软件实体(类、模块、方法等)应该对扩展开放,对修改封闭。 如果你对面向对象编程有基本的理解,你应该知道“多态”这个概念。通过使用继承和实现接口,可以实现多态性,满足不同的需求,进而可以确保我们的代码是符合开闭原则的。
以上论述可能不易理解,以下通过代码示例说明:

class Post
{
    void CreatePost(Database db, string postMessage)
    {
        if (postMessage.StartsWith("#"))
        {
            db.AddAsTag(postMessage);
        }
        else
        {
            db.Add(postMessage);
        }
    }
}

在上面的代码片段里,我们需要实现,如果邮件是以“#”开头的,需要做特殊处理。
但是上面的实现违反的开闭原则,因为根据首字母,这段代码会有不同的行为。试想如果下次需要特别处理以“@”开头的邮件,那么,我们需要修改这个类,在方法里再加一段else if
为了让它符合开闭原则,我们只需要简单地使用继承。

class Post
{
    void CreatePost(Database db, string postMessage)
    {
        db.Add(postMessage);
    }
}

class TagPost : Post
{
    override void CreatePost(Database db, string postMessage)
    {
        db.AddAsTag(postMessage);
    }
}

通过使用继承,可以很方便的扩展Post对象的行为,只需要重写CreatePost()方法。
判断首字母是否是“#”的逻辑可以放到其他地方(可能是上一层),因此,如果我们需要调整首字母判断的逻辑,我们只需要在上层做修改,不会影响底层的具体实现。

L - 里氏代换原则

这条原则对于初次接触的人可能是最难理解的。
里氏代换原则说的是:如果ST的子类,那么使用T类型对象的地方都可以替换为S类型的对象。
可以用数学公式表达:
假设 ϕ(x) 表示xT类型的对象。
如果 y是类型S的对象,ST的子类型, 那么ϕ(y) 一定是成立的。
简单来说就是,在程序中用子类对象替换父类对象是不会改变程序正确性的。
看下面的例子为何违反了里氏代换原则的:

class Post
{
    void CreatePost(Database db, string postMessage)
    {
        db.Add(postMessage);
    }
}

class TagPost : Post
{
    override void CreatePost(Database db, string postMessage)
    {
        db.AddAsTag(postMessage);
    }
}

class MentionPost : Post
{
    void CreateMentionPost(Database db, string postMessage)
    {
        string user = postMessage.parseUser();

        db.NotifyUser(user);
        db.OverrideExistingMention(user, postMessage);
        base.CreatePost(db, postMessage);
    }
}

class PostHandler
{
    private database = new Database();

    void HandleNewPosts() {
        List<string> newPosts = database.getUnhandledPostsMessages();

        foreach (string postMessage in newPosts)
        {
            Post post;

            if (postMessage.StartsWith("#"))
            {
                post = new TagPost();
            }
            else if (postMessage.StartsWith("@"))
            {
                post = new MentionPost();
            }
            else {
                post = new Post();
            }

            post.CreatePost(database, postMessage);
        }
    }
}

可以看到,对于子类MentionPost对象调用方法CreatePost(),不会如预想的那样,通知用户,并且覆盖已有的提醒信息。
由于在MentionPost类中,没有重写CreatePost()方法,所有调用CreatePost()会在继承体系里向上寻找,执行父类中的CreatePost()方法。
修改如下:

class MentionPost : Post
{
    override void CreatePost(Database db, string postMessage)
    {
        string user = postMessage.parseUser();

        NotifyUser(user);
        OverrideExistingMention(user, postMessage)
        base.CreatePost(db, postMessage);
    }

    private void NotifyUser(string user)
    {
        db.NotifyUser(user);
    }

    private void OverrideExistingMention(string user, string postMessage)
    {
        db.OverrideExistingMention(_user, postMessage);
    }
}

我们重构MentionPost这个类,重写CreatePost()这个方法,就不会违背里氏代换原则了。
这个例子很简单,能够很容易看出违背了该原则,但是这种情况可以通过多种方式表现出来,不一定能很容易识别。

如果觉得我的文章对您有用,请在支付宝公益平台找个项目捐点钱。 @sxzhou Feb 21, 2020

奉献爱心