OOP 是为人设计的(CPU 不关心):通过接口实现抽象的力量
学习利用接口的力量来设计你的面向对象代码,这样你就可以轻松地测试交互并创建更具可扩展性、更易于维护的代码。(另见简单实现的工厂模式)
- 下载 OODesign_LINQPad.zip (LINQPad 源文件) - 627 B
- 下载 OODesign_v001.zip (VStudio 2013 C# 带 Git 仓库) - 24.7 KB
引言
扩展你的 OOP 设计知识的挑战
在不学习是否有更好的代码设计方式的情况下,自然会非常擅长自己所做的事情。
即使你决定深入学习一种新的设计模式,你也可能会发现很难将这种模式应用到你编写的实际代码中。即使你发现你可以在实际代码中使用这种模式,你也可能会发现整个应用程序都需要重新设计才能实现这种模式——因为进行更改会改变你的代码中对象的交互。
通常,这会导致我们回到最初的想法,即我们已经很擅长我们所做的事情,代码也足够好了。毕竟,它已经在生产环境中运行,并且很少给你带来麻烦。
缩小范围,真实代码,边学边运行
这就是为什么我这篇文章的思路是有一个缩小的范围,让你能够实际看到真正做有价值的事情的真实代码,你可以立即应用于解决方案。你还可以一边学习一边运行代码,这样你就可以看到设计模式是如何实际工作的。
如何轻松地看到代码运行?
我建议你从 http://LINQPad.net (在新窗口/标签页中打开) 下载免费的 LINQPad 工具。
这个工具允许你编写 C# 代码并运行它,而无需处理创建项目、WinForms 以及所有其他要求。相反,有了这个工具,你可以设计类,编写一些代码来使用它们,然后运行代码并获得控制台输出(在工具底部)。看起来是这样的:
这就是我们将如何进行本文的。我将为你提供最小化的、可运行的代码,让你运行它,然后我们将讨论它并稍作修改。
设计模式是抽象概念
设计模式难以讨论的原因之一是它们有时非常抽象,以至于难以看到它们在现实世界中的好处。例子往往过于复杂和混乱,使人难以专注于我们试图完成的事情。
这就是为什么我想让我的例子非常简单,但又适用于实际解决方案。
正如我所说,我想让我的代码在进展时能够运行。
我建议我们使用一个 IPersistable
接口,它支持一个名为 Save()
的 public
方法。它看起来会是这样的:
interface IPersistable {
bool Save();
}
这个接口是从哪里来的?
我首先要说明的是,这个接口是我自己编的。没错,你可能会在其他地方找到一个名为 IPersistable
的接口,但这个是我自己编的。下一个合乎逻辑的问题是:你是从哪里得到这个想法的,它会是一个好的接口?
为什么这是一个有效的接口?
它之所以有效,仅仅是因为接口由一个或多个命名为动作的行为组成。在这种情况下,我希望我的实现类(我们稍后会看到)包含一个名为 Save()
的方法,该方法返回一个 bool
。正如你所看到的,接口根本不包含任何实现——接口中没有任何代码能做任何事情。它有什么价值呢?
未来代码的契约
目前,只需将其视为对未来代码的陈述。例如,开发者 Sam 可能会说:“我希望我的类将数据保存到文件中。”然后 Jane 可能会插嘴说:“嗯,我希望我的类保存到数据库。”
你有没有注意到,这两个开发者使用的自然语言中都包含“保存”这个词?这是他们希望他们的类遵守的未来契约。这是谈论他们想要的类的抽象方式。
实现方式不同
然而,每种实现的具体方式将大不相同,因为一种是写入文件,另一种是写入数据库。它们将需要不同的配置信息,例如文件版本的路径和文件名,以及数据库连接和表名,以供数据库实现使用。
三个实现类,使我们的代码更真实
让我们创建三个实现 IPersistable
接口的类,这会让事情更清楚一些。此外,你还可以运行代码,并看到更多关于它如何工作以及它可能如何帮助你的示例。
class FileSaver : IPersistable{
public bool Save(){
Console.WriteLine("I'm saving into a FILE.");
return true;
}
}
class DatabaseSaver : IPersistable{
public bool Save(){
Console.WriteLine("I'm saving into a DATABASE.");
return true;
}
}
class TcpSaver : IPersistable{
public bool Save(){
Console.WriteLine("I'm saving into a WEB LOCATION.");
return true;
}
}
现在我们有了一些实际的实现,这会令很多开发者兴奋,因为他们会看到这些类实际上做了什么。
如果你将所有代码复制到 LINQPad 并添加一个 main
方法,你将在 LINQPad 的控制台输出区域看到一些结果。
这是你可以复制并尝试在 LINQPad 中使用的完整代码列表
void Main()
{
FileSaver fs = new FileSaver();
fs.Save();
DatabaseSaver ds = new DatabaseSaver();
ds.Save();
}
interface IPersistable {
bool Save();
}
class FileSaver : IPersistable{
public bool Save(){
Console.WriteLine("I'm saving into a FILE.");
return true;
}
}
class DatabaseSaver : IPersistable{
public bool Save(){
Console.WriteLine("I'm saving into a DATABASE.");
return true;
}
}
class TcpSaver : IPersistable{
public bool Save(){
Console.WriteLine("I'm saving into a WEB LOCATION.");
return true;
}
}
如果你在 LINQPad 中运行这段代码,你会看到它的输出如下:
没那么令人惊叹(或者说令人惊叹?)
“好吧,”你想,“了不起。”这有什么意义呢?嗯, at this point it's a bit difficult to see but the big deal is that you know that your implementations are guaranteed to fulfill the Save()
contract that was set up by the interface. (此时很难看出,但重要的是你知道你的实现保证履行了接口设定的 Save()
契约。)
履行契约有什么意义?
你可能没有注意到,但现在你已经让所有类都实现了接口,这些类都已经被转换成了相同的类型(IPersistable
)。这有什么意义呢?它很重要,因为现在你可以将它们分组。为什么能分组很重要?这很重要,因为现在你不需要像在以前的 LINQPad 示例的 Main()
方法中那样将它们创建为单独的类型。
分组相似的对象及其优势
由于我们现在知道所有对象都将履行特定的契约,因此我们可以将它们分组并调用相同的代码,因为我们知道该方法将可用。
现在,我们可以将我们的 Main()
方法代码修改成如下:
void Main()
{
List<IPersistable> allItems = new List<IPersistable>();
allItems.Add(new FileSaver());
allItems.Add(new DatabaseSaver());
allItems.Add(new TcpSaver());
foreach (IPersistable ip in allItems)
{
ip.Save();
}
}
请告诉我,你的眼睛是不是亮了?
如果你理解了我们在代码中所做的,你就会灵光一闪,眼睛都会亮起来。
理解这个简单的概念是理解 OOP 中一切的关键。
这是代码在 LINQPad 中运行时快照:
无需知道对象是什么
你不再需要知道你的对象是什么才能让它做某事。运行时系统将知道它是什么。
只需要知道对象实现了哪些行为
现在你只需要知道它实现了哪些行为,以便在你的代码中专门调用它们。
这有什么帮助?
它允许你开始在更高级别上设计你的代码,这样你就可以开始编写代码,而无需编写所有实现就可以尝试你的对象的交互。
对象的交互?什么?
如果你要总结 OOP 代码,你可以说它只是:
引用创建各种对象类型,它们相互交互以产生预期的结果。
你到底想跟我说什么鬼话?听起来像一堆废话。
好吧,考虑一下这个问题。
写代码的主要原因
写代码的主要目的是创建解决问题的软件。
OOP 是为人类设计的
编写 OOP 代码的主要目的是设计有组织的、能够解决问题的代码。OOP 仅仅是一种组织代码的方式。计算机本身(处理器)根本不知道或不在乎 OOP。OOP 是为人类设计的。
我们编写代码来解决问题。我们以 OOP 的方式编写代码是为了组织复杂的解决方案,使其更易于维护和扩展。OOP 代码应更清晰地(并非完美地)向从未见过该代码的维护开发人员传达其意图(目的)。
OOP 通过代码摘要赋予沟通能力
OOP 可以提供一个很好的代码摘要,以便其他开发人员可以参与到项目中。当人们谈论代码时,他们谈论的是代码的功能。他们谈论代码的行为。当你创建接口时,你就为行为创建了契约。现在,当我们谈论我刚刚创建的代码时,我们可以轻松地说,IPersistable
帮助你保存数据。如果一个新开发人员过来问:“嘿,这个对象做什么?”他可以看看,看到它实现了 IPersistable
,并看到 public
接口是 Save()
方法,就知道它保存数据。
接下来,当新开发人员决定他想让他新类能够将自己保存到数据库时,他可以环顾四周,看到有一个 DatabaseSaver
类实现了 IPersistable
接口。
小型系统和独自编码的开发人员的疑虑
这正是许多独自编码或从未在团队中工作过(即使是少数人)的开发人员对 OOP 持怀疑态度的原因。我理解他们的疑虑。我的意思是 OOP 在某些情况下是对系统和开发人员的开销。
一次专注于一件事
但是,当事情变得极其复杂时,OOP 可以真正帮助开发人员一次专注于一件事。它通过封装(数据隐藏)来实现这一点,因为每个对象都应该专注于做一件事(SOLID 的单一职责原则),并且只向客户端公开其主要操作的 public
成员。
引用正确实现的 OOP 可以帮助你管理复杂性。
另一个使用接口的具体示例
好了,我们再进一步,同时教你**工厂模式**。假设你希望有其他东西来构建你需要完成工作所需的对象。你为什么要这样做?因为你将允许用户在配置文件中配置他保存数据的位置,这样他的数据就可以在运行时保存在适当的位置。想一想这有多强大。用户可以在运行时轻松地让软件知道将数据保存到数据库而不是文件中。
非常简单的工厂模式实现,以清晰为目的
请记住,为了清晰起见,我将工厂模式的第一个版本保持得非常简单。
这是你需要在 LINQPad 中添加的所有代码来创建工厂模式。
class SaverFactory{
String type;
public SaverFactory(String type){
this.type = type;
}
public IPersistable CreateSaver(){
switch (type){
case "FileSaver" :
{
return new FileSaver();
}
case "DatabaseSaver" :
{
return new DatabaseSaver();
}
case "TcpSaver" :
{
return new TcpSaver();
}
default :
return null;
}
}
}
简单的力量
这段代码非常简单,但功能非常强大。请注意,你可以通过传入一个表示类型的 String
来构造工厂。构造工厂如下:SaverFactory("FileSaver")
。
接下来,请注意 CreateSaver()
方法只是根据 String
进行切换,并调用我们要实例化的实现类的相应构造函数。
需要关注的重点
请注意,CreateSaver()
方法返回一个 IPersistable
类型的对象。这就是力量的来源。
你不需要知道返回的是哪种具体的实现类型。你只关心它保证支持的行为。在这种情况下是 Save()
。
使用 SaverFactory 生成我们的类
现在,当我们想生成一个 FileSaver
时,我们可以这样做:
IPersistable ip = SaverFactory("FileSaver").CreateSaver();
然而,这个小例子可能不足以让你清楚地看到它的强大之处。
让我们修改主方法中的驱动代码来使用 Factory
。我包含了完整的代码列表,以便你可以将其复制到 LINQPad 中运行。
void Main()
{
// imagine numerous config lines were read from DB
List<String> fakeConfig = new List<String>();
fakeConfig.Add("FileSaver"); fakeConfig.Add("DatabaseSaver");
fakeConfig.Add("TcpSaver");
List<IPersistable> allItems = new List<IPersistable>();
foreach (String s in fakeConfig)
{
IPersistable ip = new SaverFactory(s).CreateSaver();
if (ip != null)
allItems.Add(ip);
}
foreach (IPersistable ip in allItems)
{
ip.Save();
}
}
interface IPersistable {
bool Save();
}
class FileSaver : IPersistable{
public bool Save(){
Console.WriteLine("I'm saving into a FILE.");
return true;
}
}
class DatabaseSaver : IPersistable{
public bool Save(){
Console.WriteLine("I'm saving into a DATABASE.");
return true;
}
}
class TcpSaver : IPersistable{
public bool Save(){
Console.WriteLine("I'm saving into a WEB LOCATION.");
return true;
}
}
class SaverFactory{
String type;
public SaverFactory(String type){
this.type = type;
}
public IPersistable CreateSaver(){
switch (type){
case "FileSaver" :
{
return new FileSaver();
}
case "DatabaseSaver" :
{
return new DatabaseSaver();
}
case "TcpSaver" :
{
return new TcpSaver();
}
default :
return null;
}
}
}
代码量不小
代码量不小,但请注意它的组织是多么清晰。请注意,你现在可以遍历这些通用对象,知道它们将满足你最初的契约。
关注主方法
另外,请注意,你可以专注于 Main()
方法,并轻松确定正在发生的事情。这是非常可读的代码。
想象一下读取配置信息
你必须想象代码正在从数据库或配置文件中读取各种配置信息。这些配置信息还将包含用户想要在相关配置中实现的类型的名称(FileSaver
、DatabaseSaver
等)。这可能是因为用户希望将数据保存到数据库或其他地方。
可扩展,而不会破坏代码
然而,它也是相当可扩展的。我们可以轻松地添加 IPersistable
的新实现,甚至扩展我们每个实现类**而不会破坏任何东西**。甚至不需要触碰任何现有代码。这意味着无需重新测试以前完成和测试过的代码。应该有更多的灵光乍现从你的大脑中迸发出来。
这是代码输出的另一个快照。与上一个非常相似。
下一级别,下一篇文章
我们可以将这个提升到另一个层次,我希望写下一篇文章,其中将
向你展示如何将 Configuration
对象发送到工厂,以便每个实现类都拥有保存其数据所需的所有详细信息(文件名、数据库连接等)。我只是不想让我的读者被一篇过长的文章完全压倒。
关注点
这些是我一直在思考的一些事情,并希望记录下来,希望能帮助他人并开启对话。
历史
- 2016-04-23 - **文章第二版** - 修正了语法错误并添加了代码下载(LINQPad 文件和 VStudio 2013 C# 项目)
- 2016-04-22 - 本文和代码的第一个版本