65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.74/5 (9投票s)

2017年12月2日

CPOL

6分钟阅读

viewsIcon

16048

探索 .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 作为额外的参数传递?我们可以,它实现了一个 CreateCommandCreateParameter 方法。幸运的是,我们不需要,因为 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 接口的装饰器;这里只分享一部分以展示概念是如何工作的。要使其正常工作,你还需要 IDbCommandIDataReader 接口的装饰器,由于它们是一个交付成果的一部分,所以我无法分享。如你所见,我们向连接添加了一个日志记录器;你可以看到如何实现名为“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日 - 初始版本

 

© . All rights reserved.