原文:
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 - 里氏代换原则
这条原则对于初次接触的人可能是最难理解的。
里氏代换原则说的是:如果S
是T
的子类,那么使用T
类型对象的地方都可以替换为S
类型的对象。
可以用数学公式表达:
假设 ϕ(x)
表示x
是T
类型的对象。
如果 y
是类型S
的对象,S
是T
的子类型, 那么ϕ(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()
这个方法,就不会违背里氏代换原则了。
这个例子很简单,能够很容易看出违背了该原则,但是这种情况可以通过多种方式表现出来,不一定能很容易识别。