创建IDisposable对象的工厂,如何处置它们?

时间:2017-10-24 18:08:20

标签: c# factory idisposable

我的应用程序创建了应该重用的IDisposable对象,因此我创建了一个封装这些对象的创建和重用的工厂,代码如下:

public class ServiceClientFactory
{
    private static readonly object SyncRoot = new object();
    private static readonly Dictionary<string, ServiceClient> Clients = new Dictionary<string, ServiceClient>(StringComparer.OrdinalIgnoreCase);

    public static ServiceClient CreateServiceClient(string host)
    {
        lock(SyncRoot)
        {
            if (Clients.ContainsKey(host) == false)
            {
                Clients[host] = new ServiceClient(host);
            }

            return Clients[host];
        }
    }  
}

public class QueryExecutor
{
    private readonly ServiceClient serviceClient;

    public QueryExecutor(string host)
    {
        this.serviceClient = ServiceClientFactory.CreateServiceClient(host);
    }

    public IDataReader ExecuteQuery(string query)
    {
        this.serviceClient.Query(query, ...);
    }
}

让我刮目的是,ServiceClient是IDisposable,我应该在某个时候明确处理它们。

一种方法是在QueryExecutor中实现IDisposable并在部署QueryExecutor时配置ServiceClient,但这样,(1)在部署ServiceClient时,还需要通知ServiceClientFactory,(2)不能重用ServiceClient实例。

所以我认为让ServiceClientFactory管理所有ServiceClient实例的生命周期要容易得多,如果我这样做,那么处理工厂创建的所有IDisposable对象的最佳做法是什么?挂钩AppDomain退出事件并在每个ServiceClient实例上手动调用Dispose()?

2 个答案:

答案 0 :(得分:0)

这里要考虑的几件事...

首先,您似乎要描述的是Flyweight pattern的某些变体。您有一个昂贵的对象ServiceClient,您想重用它,但是您希望允许消费者对象随意创建和销毁而不会破坏该昂贵的对象。 Flyweight传统上是通过引用计数来做到这一点的,这可能有点陈旧。

您需要确保消费者不能直接处置ServiceClient,因此,您还需要轻量级的Facade来拦截对ServiceClient.Dispose的调用并选择是否处置真实的对象与否。您应该隐藏来自消费者的真实ServiceClient

如果所有可行的方法,您都可以将方法重写为:

// this is the facade that you will work from, instead of ServiceClient
public interface IMyServiceClient : IDisposable
{
    void Query(string query);
}

// This is your factory, reworked to provide flyweight instances
// of IMyServiceClient, instead of the real ServiceClient
public class ServiceClientFactory : IDisposable
{
    // This is the concrete implementation of IMyServiceClient 
    // that the factory will create and you can pass around; it 
    // provides both the reference count and facade implementation
    // and is nested inside the factory to indicate that consumers 
    // should not alter these (and cannot without reflecting on 
    // non-publics)
    private class CachedServiceClient : IMyServiceClient
    {
        internal ServiceClient _realServiceClient;
        internal int _referenceCount;

        #region Facade wrapper methods around real ServiceClient ones
        void IMyServiceClient.Query(string query)
        {
            _realServiceClient.Query(query);
        }
        #endregion

        #region IDisposable for the client facade
        private bool _isClientDisposed = false;

        protected virtual void Dispose(bool disposing)
        {
            if (!_isClientDisposed)
            {
                if (Interlocked.Decrement(ref _referenceCount) == 0)
                {
                    // if there are no more references, we really
                    // dispose the real object
                    using (_realServiceClient) { /*NOOP*/ }
                }
                _isClientDisposed = true;
            }
        }

        ~CachedServiceClient() { Dispose(false); }

        void IDisposable.Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
        #endregion
    }

    // The object cache; note that it is not static
    private readonly ConcurrentDictionary<string, CachedServiceClient> _cache
        = new ConcurrentDictionary<string, CachedServiceClient>();

    // The method which allows consumers to create the client; note
    // that it returns the facade interface, rather than the concrete
    // class, so as to hide the implementation details
    public IMyServiceClient CreateServiceClient(string host)
    {
        var cached = _cache.GetOrAdd(
            host,
            k => new CachedServiceClient()
        );
        if (Interlocked.Increment(ref cached._referenceCount) == 1)
        {
            cached._realServiceClient = new ServiceClient(host);
        }
        return cached;
    }

    #region IDisposable for the factory (will forcibly clean up all cached items)
    private bool _isFactoryDisposed = false;

    protected virtual void Dispose(bool disposing)
    {
        if (!_isFactoryDisposed)
        {
            Debug.WriteLine($"ServiceClientFactory #{GetHashCode()} disposing cache");
            if (disposing)
            {
                foreach (var element in _cache)
                {
                    element.Value._referenceCount = 0;
                    using (element.Value._realServiceClient) { }
                }
            }
            _cache.Clear();
            _isFactoryDisposed = true;
        }
    }

    ~ServiceClientFactory() { Dispose(false); }

    void IDisposable.Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }
    #endregion
}

// This is just an example `ServiceClient` which uses the default
// implementation of GetHashCode to "prove" that new objects aren't
// created unnecessarily; note it does not implement `IMyServiceClient`
public class ServiceClient : IDisposable
{
    private readonly string _host;

    public ServiceClient(string host)
    {
        _host = host;
        Debug.WriteLine($"ServiceClient #{GetHashCode()} was created for {_host}");
    }

    public void Query(string query)
    {
        Debug.WriteLine($"ServiceClient #{GetHashCode()} asked '{query}' to {_host}");
    }

    public void Dispose()
    {
        Debug.WriteLine($"ServiceClient #{GetHashCode()} for {_host} was disposed");
        GC.SuppressFinalize(this);
    }
}

其次,通常,我将从工厂中删除static子句,并制作ServiceClientFactory : IDisposable。您会在上面的示例中看到我已经完成了该操作。

似乎您只是将问题推上了顶峰,但是这样做可以让您根据具体情况(根据应用程序,会话,请求,单元测试运行-只要有意义),并让代表您某物的对象负责处理。

如果您的应用程序将受益于单实例缓存,则将单例实例作为AppContext类的一部分公开(例如),并在干净的环境中调用AppContext.DefaultServiceClientFactory.Dispose()关闭程序。

这里的重点是干净关机。正如其他人所说,没有保证可以真正调用您的Dispose方法(请考虑对机器重新加电,中途运行)。因此,理想情况下,ServiceClient.Dispose不会有任何有形副作用(即,释放如果进程终止或机器功率自然会释放 的资源) -周期)。

如果ServiceClient.Dispose 确实具有明显的副作用,那么您已经在此处确定了风险,因此您应该明确记录如何从操作系统中恢复系统。在随附的用户手册中“不干净”关闭。

第三次,如果ServiceClientQueryExecutor对象都打算可重用,则让消费者负责创建和处置。

QueryExecutor实际上仅是示例中的IDisposable,因为它可以拥有 ServiceClient(也为IDisposable)。如果QueryExecutor实际上没有创建 ServiceClient,则它将不负责销毁它。

相反,让构造函数使用一个ServiceClient参数(或者,用我的重写,是一个IMyServiceClient参数),而不是string,这样直接使用者就可以为生命周期负责所有对象中的

using (var client = AppContext.DefaultServiceClientFactory.CreateServiceClient("localhost"))
{
    var query = new QueryExecutor(client);
    using (var reader = query.ExecuteReader("SELECT * FROM foo"))
    {
        //...
    }
}

PS :消费者实际上是否需要直接访问ServiceClient,或者是否有其他需要引用的对象?如果不是,则可以稍微减少链条,然后将其直接移动到QueryExecutor,即改为使用QueryExecutorFactory在缓存的QueryExector实例周围创建flyweight ServiceClient对象。 / p>

答案 1 :(得分:-1)

静态类就这么棘手。首次调用实例成员时,将调用静态类的构造函数。如果应用程序域被销毁,则该类将被销毁。如果应用程序突然终止(中止),则无法保证将调用构造函数/终结器。

这里最好的选择是重新设计类以使用单例模式。有一些方法可以解决此问题,但they require strange, dark magic forces that may cost you your soul.

编辑:正如服务指出的那样,单身人士在这里无济于事。析构函数是终结器,您无法保证它将被调用(由于各种原因)。如果是我,我只是将它重构为实现IDisposable的可实例化类,并让调用者处理调用Dispose。我知道,这并不理想,但这是唯一确定的方法。