关于 .NET 中两种模式的简短故事






4.74/5 (9投票s)
探索 .NET 框架中使用的一些模式以及它们如何发挥作用
引言
论坛上很多开发者对设计模式持怀疑态度,想知道银弹是否真的能带来实际价值,还是只会增加混乱和复杂性。我之前的雇主成功地使用了一些模式,我想分享一些我通过观察 .NET 框架的源代码和实现中一些常见模式所学到的东西。
工厂模式
先从至少一个成员讨厌的东西开始,工厂基本上是一个返回新对象的实体(类或方法)。不用担心那里所有正式的不同工厂;我们将完全忽略它们。我们也跳过对模式类型的任何分类,因为对于计算机来说,它是创建型还是行为型模式,关系不大。
在 .NET 框架中一个未被充分利用的工厂是 DBProviderFactory
[^];
using System.Data.Common;
static DbProviderFactory dbProviderFactory =
DbProviderFactories.GetFactory("System.Data.SqlClient");
是的,一个用于创建 SqlConnection
的工厂。改变参数,你会得到一个不同的连接。如果每次数据库调用都使用这个工厂来创建连接,那么改变连接的类型就会变得更简单。不用将所有“new SqlConnection()”语句替换为“new SQLiteConnection()”语句,你 *只需要* 更改传递给工厂的参数。
“但是 Eddy,”你会说,“我们还需要重写所有那些 SqlCommand 调用。” 是的,大多数人会使用下面的代码模式来处理数据库相关的工作;
using (var con = new SqlConnection())
using (var cmd = new SqlCommand())
{
cmd.Connection = con;
// more here
你可以看到两个类是如何硬编码的,所以用工厂替换连接创建只能帮助我们一半。我们是否将 DbProviderFactory
作为额外的参数传递?我们可以,它实现了一个 CreateCommand
和 CreateParameter
方法。幸运的是,我们不需要,因为 IDbConnection
[^] 接口 *要求* 所有连接实现一个 CreateCommand
工厂。是的,你没看错,在所有连接中包含一个工厂方法并不符合 SOLID[^] 原则,但它确实让生活变得非常轻松;
using (var con = MyConnectionFactory.CreateConnection())
using (var cmd = con.CreateCommand())
{
// no longer a need to assign the Connection-property of the command,
// since the connection creating the command is assigned by default.
}
所以现在我们使用一个工厂类(DbProviderFactory
)来创建连接,然后使用该特定连接上的工厂方法来创建命令。命令上还有工厂方法用于创建 Parameters
,这一点此时应该不会让你感到惊讶。我们不是在代码中四处散布“new bla()”,而是在一个可以更轻松控制的单一位置进行创建;这比仅仅在替换创建的对象时不必更新所有代码点有更多优势。这意味着你可以对对象进行一些初始化或轻松跟踪它。作为例子,我在下面包含了我在这个示例中使用的工厂。我将连接的打开移到了工厂,这意味着这个语句不再需要在每次获取连接的地方重复。
public static class MyFactory
{
static DbProviderFactory dbProviderFactory =
DbProviderFactories.GetFactory("System.Data.SqlClient");
public static IDbConnection CreateConnection()
{
var con = dbProviderFactory.CreateConnection();
con.Open();
return con;
}
}
如果你的老板要求记录每次打开数据库的操作,那么添加到上面的代码中会很容易。由于连接上的 CreateCommand
方法还会设置命令的 Connection 属性,所以你现在可以省略这个模式中的两个常见语句:打开连接和将连接分配给命令。这使得每次数据库操作可能失败的行数减少了两行。
结论
因此,工厂模式在 .NET 框架中使用,并且它们的优势很容易解释。将这种技术与遵循 SQL92 标准结合起来,你就可以突然能够利用各种数据库产品作为你的存储。使用另一个供应商的不同数据库可能突然变成一个配置问题,而不是数月的迁移时间。
宗教警告
我 **并非** 说你应该只创建工厂而再也不使用 new 关键字。在示例中,额外的代码是明显有价值的,但不要仅仅因为你能就使用工厂;如果你可以不用,那很可能更好的选择。不要增加不增加价值的复杂性,因为所有代码都有成本。学习抽象工厂的定义也毫无用处;学习何时(以及何时不)应用该模式,而不是正确地命名它。
装饰器模式
装饰器是一种比继承更灵活地向类添加功能的好方法。下面是一个简单的抽象示例;
class Person
{
public string Name { get; set; }
public string SocialSecurityId { get; set; }
public void ShowId();
}
class ClothedPerson: Person
{
Person _decoree;
ClothedPerson(Person who, bool hasPants)
{
_decoree = who;
}
public bool Pants { get; set; }
public void ShowId() { _decoree.ShowId(); }
}
class Prisoner: Person
{
Person _decoree;
Prisoner(Person who)
{
_decoree = who;
}
public void ShowId() { _decoree.ShowId(); }
}
这是一个无用的例子,我已经听到你说你永远不会在实际代码中使用它,但如果你看看,这就是 streams 在 .NET 中的工作方式;需要在你的(文件/内存/其他)stream 上进行加密?添加正确的装饰器,你就完成了。现在,让我们来看一个更实用的装饰器示例,它可能更有用。
public class LoggedConnection : IDbConnection
{
ILogger _logger;
IDbConnection _host;
Guid _connectionId;
string _category;
public Guid ConnectionId
{
get { return _connectionId; }
}
public LoggedConnection(IDbConnection host, ILogger logger)
{
if (null == host) throw new ArgumentNullException("host");
if (null == logger) throw new ArgumentNullException("logger");
_host = host;
_connectionId = Guid.NewGuid();
_category = string.Format("{0} ({1})", _host.GetType().Name, _connectionId);
_logger = logger;
_logger.Write(
_category,
"Created a new IDbConnection to database '{0}'",
_host.Database);
}
public string ConnectionString
{
get { return _host.ConnectionString; }
set { _host.ConnectionString = value; }
}
public int ConnectionTimeout
{
get { return _host.ConnectionTimeout; }
}
public string Database
{
get { return _host.Database; }
}
public ConnectionState State
{
get { return _host.State; }
}
public IDbTransaction BeginTransaction()
{
return _host.BeginTransaction();
}
public IDbTransaction BeginTransaction(IsolationLevel level)
{
return _host.BeginTransaction(level);
}
public void ChangeDatabase(string dbName)
{
_host.ChangeDatabase(dbName);
}
[DebuggerStepThrough()] // this makes sure that any error
// points to the Open() method of the _host.
public void Open()
{
_logger.Write(
_category,
"Opening database '{0}' with ConnectionString '{1}'",
_host.Database,
_host.ConnectionString);
_host.Open();
}
public void Close()
{
_logger.Write(
_category,
"Closing database '{0}'",
_host.Database);
_host.Close();
}
public IDbCommand CreateCommand()
{
return new LoggedCommand(_host.CreateCommand(), _connectionId, _logger);
}
void IDisposable.Dispose()
{
Close();
_host.Dispose();
System.GC.SuppressFinalize(this);
_logger.Write(
_category,
"Disposed IDbConnection to database '{0}'",
_host.Database);
}
}
你看到的是 IDbConnection
接口的装饰器;这里只分享一部分以展示概念是如何工作的。要使其正常工作,你还需要 IDbCommand
和 IDataReader
接口的装饰器,由于它们是一个交付成果的一部分,所以我无法分享。如你所见,我们向连接添加了一个日志记录器;你可以看到如何实现名为“CreateCommand”的工厂方法来返回底层命令的装饰版本。如果你实现了 IDbCommand
接口的装饰器,那么你就可以从那里执行一个装饰过的 IDataReader
。这个概念简单但强大。将工厂模式和装饰器模式结合起来,你就可以跟踪(“分析”)通过装饰后的提供者执行的所有 SQL 语句(无论它是 SQL Server、SQLite 还是 MS Access)。
connectionId
用于唯一标识每个连接,正如你所见,我忘记在连接关闭时提及 id。这证明了了解模式并不能让你免于愚蠢。
它的使用方式与下面展示的代码类似;
ThreadPool.QueueUserWorkItem((w) =>
{
using (var loggedTestDatabase = FetchConnection(true))
{
使用下面显示的本地工厂;
static IDbConnection FetchConnection(bool trace)
{
if (null == factory)
factory = DbProviderFactories.GetFactory(providerInvariantName: providerString);
IDbConnection connection = factory.CreateConnection();
if (trace)
{
// see ILogger CoClass attribute on what class is created.
ILogger logger = new ILogger(includeThreadName: true, includeDateStamp: true);
connection = new LoggedConnection(connection, logger);
}
return connection;
}
这意味着我们现在可以“分析”任何合法的 Data Provider,并且可以简单地开启或关闭这种行为。如果出现一个新的 Data Provider,就不需要更新或重写任何东西,它会自动工作。这仅仅是借助两个简单的模式,我们大多数人都可以轻松掌握。这意味着可以进行日志记录,而无需一个辛苦的程序员来编写所有日志语句。
你看到日志记录器不是来自工厂;我没有习惯用工厂替换所有构造函数——如果不需要工厂,最好不要使用它。在上面的示例中,这两种模式的使用都有其增加功能的理由。开发者经常争论什么才是“正确”的;他们争论就是因为他们是 *正确* 的,这意味着有人在证明他/她的决定。这些决定可能是正确的,也可能是错误的,但争论是找出答案的唯一途径。
反模式
这里有一个反模式给你,一个在 StyleCop 眼中的改进,也是我经常违反的一个规则;SA1122 建议使用空对象模式,用 String.Empty
代替硬编码的“” 。它没有任何好处,只是使用了更多的字符来传达相同的信息,使其比原来的更难读。我确实喜欢 EventArgs.Empty
,但很久以前就接受了 EventArgs 成员在自定义遗留代码中通常是 null
。你呢?你喜欢 string.Empty
吗?
还有什么?
还有很多其他的模式;它们不会像这里宣传的那样自动解决你的问题,但它们会在你处理现有事物时为你提供一些额外的弹药。有没有你希望我看一下的模式?请留言!
历史
- 2017年12月2日 - 初始版本