类设计:Demeter与连接寿命

时间:2022-10-19 22:09:55

Okay, so here's a problem I'm running into.

好的,所以这是我遇到的一个问题。

I have some classes in my application that have methods that require a database connection. I am torn between two different ways to design the classes, both of which are centered around dependency injection:

我的应用程序中有一些类具有需要数据库连接的方法。我在设计类的两种不同方式之间徘徊,两者都以依赖注入为中心:

  1. Provide a property for the connection that is set by the caller prior to method invocation. This has a few drawbacks.

    为方法调用之前调用方设置的连接提供属性。这有一些缺点。

    • Every method relying on the connection property has to validate that property to ensure that it isn't null, it's open and not involved in a transaction if that's going to muck up the operation.

      依赖于连接属性的每个方法都必须验证该属性,以确保它不为空,它是打开的,如果要破坏操作,则不参与事务。

    • If the connection property is unexpectedly closed, all the methods have to either (1.) throw an exception or (2.) coerce it open. Depending on the level of robustness you want, either case is appropriate. (Note that this is different from a connection that is passed to a method in that the reference to the connection exists for the lifetime of the object, not simply for the lifetime of the method invocation. Consequently, the volatility of the connection just seems higher to me.)

      如果连接属性意外关闭,则所有方法都必须(1.)抛出异常或(2.)强制将其打开。根据您想要的稳健程度,任何一种情况都是合适的。 (请注意,这与传递给方法的连接不同,因为对象的生命周期中存在对连接的引用,而不仅仅是方法调用的生命周期。因此,连接的波动性似乎更高对我来说。)

    • Providing a Connection property seems (to me, anyway) to scream out for a corresponding Transaction property. This creates additional overhead in the documentation, since you'd have to make it fairly obvious when the transaction was being used, and when it wasn't.

      提供一个Connection属性似乎(无论如何)来为相应的Transaction属性尖叫。这会在文档中产生额外的开销,因为在使用事务时必须使其显而易见,而在事务处理时则不然。

    On the other hand, Microsoft seems to favor the whole set-and-invoke paradigm.

    另一方面,微软似乎更青睐整个set-and-invoke范式。

  2. Require the connection to be passed as an argument to the method. This has a few advantages and disadvantages:

    要求将连接作为参数传递给方法。这有一些优点和缺点:

    • The parameter list is naturally larger. This is irksome to me, primarily at the point of call.

      参数列表自然更大。这对我来说很烦人,主要是在通话时。

    • While a connection (and a transaction) must still be validated prior to use, the reference to it exists only for the duration of the method call.

      虽然在使用之前仍必须验证连接(和事务),但对它的引用仅在方法调用的持续时间内存在。

    • The point of call is, however, quite clear. It's very obvious that you must provide the connection, and that the method won't be creating one behind your back automagically.

      然而,调用的目的非常明确。很明显,您必须提供连接,并且该方法不会自动创建背后的连接。

    • If a method doesn't require a transaction (say a method that only retrieves data from the database), no transaction is required. There's no lack of clarity due to the method signature.

      如果方法不需要事务(比如只从数据库中检索数据的方法),则不需要事务。由于方法签名,不乏清晰度。

    • If a method requires a transaction, it's very clear due to the method signature. Again, there's no lack of clarity.

      如果方法需要事务,由于方法签名,它非常清楚。同样,也不乏清晰度。

    • Because the class does not expose a Connection or a Transaction property, there's no chance of callers trying to drill down through them to their properties and methods, thus enforcing the Law of Demeter.

      因为类不公开Connection或Transaction属性,所以调用者不可能尝试向下钻取它们的属性和方法,从而强制执行Demeter法则。

I know, it's a lot. But on the one hand, there's the Microsoft Way: Provide properties, let the caller set the properties, and then invoke methods. That way, you don't have to create complex constructors or factory methods and the like. Also, avoid methods with lots of arguments.

我知道,这很多。但一方面,有Microsoft Way:提供属性,让调用者设置属性,然后调用方法。这样,您就不必创建复杂的构造函数或工厂方法等。另外,避免使用包含大量参数的方法。

Then, there's the simple fact that if I expose these two properties on my objects, they'll tend to encourage consumers to use them in nefarious ways. (Not that I'm responsible for that, but still.) But I just don't really want to write crappy code.

然后,有一个简单的事实,即如果我在我的对象上公开这两个属性,它们往往会鼓励消费者以邪恶的方式使用它们。 (不是我对此负责,但仍然。)但我真的不想写糟糕的代码。

If you were in my shoes, what would you do?

如果你穿我的鞋子,你会怎么做?

2 个解决方案

#1


Here is a third pattern to consider:

这是第三种需要考虑的模式:

  • Create a class called ConnectionScope, which provides access to a connection
  • 创建一个名为ConnectionScope的类,它提供对连接的访问

  • Any class at any time, can create a ConnectionScope
  • 任何时候任何类都可以创建一个ConnectionScope

  • ConnectionScope has a property called Connection, which always returns a valid connection
  • ConnectionScope有一个名为Connection的属性,它始终返回有效的连接

  • Any (and every) ConnectionScope gives access to the same underlying connection object (within some scope, maybe within the same thread, or process)
  • 任何(和每个)ConnectionScope都可以访问相同的底层连接对象(在某个范围内,可能在同一个线程或进程内)

You then are free to implement that Connection property however you want, and your classes don't have a property that needs to be set, nor is the connection a parameter, nor do they need to worry about opening or closing connections.

然后,您可以根据需要*实现Connection属性,并且您的类没有需要设置的属性,连接也不是参数,也不需要担心打开或关闭连接。

More details:

  • In C#, I'd recommend ConnectionScope implement IDisposable, that way your classes can write code like "using ( var scope = new ConnectionScope() )" and then ConnectionScope can free the connection (if appropriate) when it is destroyed
  • 在C#中,我建议使用ConnectionScope实现IDisposable,这样你的类就可以编写类似“using(var scope = new ConnectionScope())”的代码,然后ConnectionScope可以在它被销毁时释放连接(如果适用)

  • If you can limit yourself to one connection per thread (or process) then you can easily set the connection string in a [thread] static variable in ConnectionScope
  • 如果您可以将自己限制为每个线程(或进程)一个连接,那么您可以在ConnectionScope中的[thread]静态变量中轻松设置连接字符串

  • You can then use reference counting to ensure that your single connection is re-used when its already open and connections are released when no one is using them
  • 然后,您可以使用引用计数来确保在已经打开时重新使用单个连接,并在没有人使用它们时释放连接

Updated: Here is some simplified sample code:

更新:以下是一些简化的示例代码:

public class ConnectionScope : IDisposable
{
   private static Connection m_Connection;
   private static int m_ReferenceCount;

   public Connection Connection
   {
      get
      {
          return m_Connection;
      }
   }

   public ConnectionScope()
   {
      if ( m_Connection == null )
      {
          m_Connection = OpenConnection();
      }
      m_ReferenceCount++;
   }

   public void Dispose()
   {
      m_ReferenceCount--;
      if ( m_ReferenceCount == 0 )
      {
         m_Connection.Dispose();
         m_Connection = null;
      }
   }
}

Example code of how one (any) of your classes would use it:

一个(任何)类如何使用它的示例代码:

using ( var scope = new ConnectionScope() )
{
   scope.Connection.ExecuteCommand( ... )
}

#2


I would prefer the latter method. It sounds like your classes use the database connection as a conduit to the persistence layer. Making the caller pass in the database connection makes it clear that this is the case. If the connection/transaction were represented as a property of the object, then things are not so clear and all of the ownership and lifetime issues come out. Better to avoid them from the start.

我更喜欢后一种方法。听起来您的类使用数据库连接作为持久层的管道。使调用者在数据库连接中传递可以清楚地表明这种情况。如果连接/事务被表示为对象的属性,那么事情就不那么清楚了,所有的所有权和生命周期问题都会出现。最好从一开始就避开它们。

#1


Here is a third pattern to consider:

这是第三种需要考虑的模式:

  • Create a class called ConnectionScope, which provides access to a connection
  • 创建一个名为ConnectionScope的类,它提供对连接的访问

  • Any class at any time, can create a ConnectionScope
  • 任何时候任何类都可以创建一个ConnectionScope

  • ConnectionScope has a property called Connection, which always returns a valid connection
  • ConnectionScope有一个名为Connection的属性,它始终返回有效的连接

  • Any (and every) ConnectionScope gives access to the same underlying connection object (within some scope, maybe within the same thread, or process)
  • 任何(和每个)ConnectionScope都可以访问相同的底层连接对象(在某个范围内,可能在同一个线程或进程内)

You then are free to implement that Connection property however you want, and your classes don't have a property that needs to be set, nor is the connection a parameter, nor do they need to worry about opening or closing connections.

然后,您可以根据需要*实现Connection属性,并且您的类没有需要设置的属性,连接也不是参数,也不需要担心打开或关闭连接。

More details:

  • In C#, I'd recommend ConnectionScope implement IDisposable, that way your classes can write code like "using ( var scope = new ConnectionScope() )" and then ConnectionScope can free the connection (if appropriate) when it is destroyed
  • 在C#中,我建议使用ConnectionScope实现IDisposable,这样你的类就可以编写类似“using(var scope = new ConnectionScope())”的代码,然后ConnectionScope可以在它被销毁时释放连接(如果适用)

  • If you can limit yourself to one connection per thread (or process) then you can easily set the connection string in a [thread] static variable in ConnectionScope
  • 如果您可以将自己限制为每个线程(或进程)一个连接,那么您可以在ConnectionScope中的[thread]静态变量中轻松设置连接字符串

  • You can then use reference counting to ensure that your single connection is re-used when its already open and connections are released when no one is using them
  • 然后,您可以使用引用计数来确保在已经打开时重新使用单个连接,并在没有人使用它们时释放连接

Updated: Here is some simplified sample code:

更新:以下是一些简化的示例代码:

public class ConnectionScope : IDisposable
{
   private static Connection m_Connection;
   private static int m_ReferenceCount;

   public Connection Connection
   {
      get
      {
          return m_Connection;
      }
   }

   public ConnectionScope()
   {
      if ( m_Connection == null )
      {
          m_Connection = OpenConnection();
      }
      m_ReferenceCount++;
   }

   public void Dispose()
   {
      m_ReferenceCount--;
      if ( m_ReferenceCount == 0 )
      {
         m_Connection.Dispose();
         m_Connection = null;
      }
   }
}

Example code of how one (any) of your classes would use it:

一个(任何)类如何使用它的示例代码:

using ( var scope = new ConnectionScope() )
{
   scope.Connection.ExecuteCommand( ... )
}

#2


I would prefer the latter method. It sounds like your classes use the database connection as a conduit to the persistence layer. Making the caller pass in the database connection makes it clear that this is the case. If the connection/transaction were represented as a property of the object, then things are not so clear and all of the ownership and lifetime issues come out. Better to avoid them from the start.

我更喜欢后一种方法。听起来您的类使用数据库连接作为持久层的管道。使调用者在数据库连接中传递可以清楚地表明这种情况。如果连接/事务被表示为对象的属性,那么事情就不那么清楚了,所有的所有权和生命周期问题都会出现。最好从一开始就避开它们。