在面向对象编程中,SOLID是5个重要的设计原则的缩写。首先是由著名的软件大师Robert C.Martin (Bob 大叔)在Design Principles and Design Patterns 中提出, 后来Michael Feathers 用SOLID来概括这五大原则。
SOLID原则使得软件设计更加容易理解、灵活和可维护。作为一名软件工程师,这5个原则我们必须知道。
本文,我将涵盖这些原则,并举例说明怎样是违背了原则,以及如何进行纠正来符合SOLID原则。
在程序设计中,单一责任原则指的是每个模块或者类应该只有一个职责。
你可能听过这样一句谚语“做一件事并把它做好”,这指的就是单一责任原则。
在文章《Principles of Object Oriented Design》中,Bob 大叔定义责任为“改变的原因”。并认为有一个且仅有一个原因使得类或模块发生改变。
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); } }
通过把错误日志功能抽象出来,我们不再违背单一责任原则。
现在有2个类,每个类都有一个责任;创建邮件和记录一个错误日志。
在程序设计中,开闭原则指的是软件对象(类,模块,函数等等)应该对扩展开放,对修改关闭。
如果你熟悉OOP,那么对于多态应该不陌生。通过继承或接口实现,使得一个抽象类具有多个子类,就可以确保代码是符合开闭原则的。
这听起来有点困惑,所以接下来举个例子,你就会非常清楚我在说什么。
class Post { void CreatePost(Database db, string postMessage) { if (postMessage.StartsWith("#")) { db.AddAsTag(postMessage); } else { db.Add(postMessage); } } }
在这个代码段中,每当邮件是用字符“#“开头,我们都需要做一些指定。然而,当有不同的字符开头,代码会有不同的行为,这违背了开闭原则。
比如,如果我们以后想用“@”开头,我们必须在CreatePost()方法中增加一个‘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); } }
通过使用继承,重写*CreatePost()*方法来创建邮件的扩展行为变得更加简单。
现在,判断第一个字符“#”可以在软件其它地方处理。更酷的事情是,如果我们想改变postMessage的判断方式,可以不影响基类的行为。
这个原则可能是第一次介绍时最难理解的一个。
在程序设计中,里氏替换原则指的是如果 S 是T 的子类,那么T 的实例可以用 S 的实例取代。
更一般的表述是,在不改变程序正确性的前提下,派生类对象可以在程序中代替其基类对象。
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); } } }
由于没有覆写,CreatePost()方法在子类MentionPost中不会起到应有的作用。
修改后如下:
... 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类,就能满足可替换性。
这只是一个不违背里氏替换原则的简单例子。然而,在实际使用过程中,这种情形可以用多种方式实现并且不易识别出来。
这个原则理解起来很简单,实际上,如果你习惯于使用接口,很大概率上会用到这个原则。
在程序设计中,接口隔离原则指的是客户不应被迫使用对其而言无用的方法或功能。
简单来讲,不要在已有接口上增加新的方法来实现新的功能。相反的,可以创建新的接口,如果有必要,可以让你的类实现多个接口。
interface IPost { void CreatePost(); } interface IPostNew { void CreatePost(); void ReadPost(); }
在上述代码示例中,假设我已经有了一个IPost 接口,包含CreatePost()方法;后来,我增加了一个新方法 ReadPost(),修改了这个接口,变成IPostNew 接口,这违背了接口隔离原则。修改如下:
interface IPostCreate { void CreatePost(); } interface IPostRead { void ReadPost(); }
一旦任何类需要实现这2个方法,就将同时实现这2个接口。
最后,我们来看一下D,最后一个设计原则。
在程序设计中,依赖倒置原则用于解耦软件中的模块。这个原则表述如下:
为了遵循这一原则,我们需要使用一种设计模式称为依赖注入,典型的,依赖注入通过类的构造函数作为输入参数。
class Post { private ErrorLogger errorLogger = new ErrorLogger(); void CreatePost(Database db, string postMessage) { try { db.Add(postMessage); } catch (Exception ex) { errorLogger.log(ex.ToString()) } } }
观察到我们在Post 类中创建了ErrorLogger 实例,如果我们想使用不同的日志,我们需要修改Post类,这违背了依赖倒置原则。修改如下:
class Post { private Logger _logger; public Post(Logger injectedLogger) { _logger = injectedLogger; } void CreatePost(Database db, string postMessage) { try { db.Add(postMessage); } catch (Exception ex) { _logger.log(ex.ToString()); } } }
通过使用依赖注入,我们不再依赖Post类来定义指定类型的日志。
OK,介绍完这么多,也大致理解了这几个原则。这些原则有区别,同时彼此间也有着联系。
【2】Single Responsibility Principle in C++
【4】Open Closed Principle in C++
【6】Liskov’s Substitution Principle in C++
【8】Interface Segregation Principle in C++
【10】Dependency Inversion Principle in C++
That’s it!If you have any questions or feedback, please feel free to comment below.
-EOF-