【问题标题】:Sharing an instance of a class with related classes与相关类共享一个类的实例
【发布时间】:2018-12-21 14:35:08
【问题描述】:

我有一组继承自 ConfigurationSection 类的类。我还有一个Logger 类,用于为每个类创建单独的日志文件,但这还不包括这些配置类。

我不想为每个ConfigurationSection 类创建一个新的Logger 实例(因此,一个新的日志文件),我希望它们都使用同一个实例,并登录到同一个文件。

实现这一目标的最佳方法是什么?

以下是我考虑过的几个选项。


在实例化每个相关类时将实例化的 Logger 作为参数传递

这似乎是最明显的选择,但我真的不喜欢它。我的Logger 类并不完全是轻量级的,我宁愿避免传递如此大的参数。无论如何,这是一种选择。

class Program
{
    static void Main( string[] args )
    {
        // These variables are used for illustrative purposed,
        // instead of showing my `Logger' class
        bool loggerInstantiated = true;
        DateTime InstantiatedTime = DateTime.Now;

        DerivedClass01 dc01 = new DerivedClass01( loggerInstantiated, InstantiatedTime );

        // added to create delay between the instantiation of different derived classes
        System.Threading.Thread.Sleep( 1000 );

        DerivedClass02 dc02 = new DerivedClass02( loggerInstantiated, InstantiatedTime );
    }
}


class BaseClass : ConfigurationSection
{
    public BaseClass( bool loggerInstantiated, DateTime instantiatedAt )
    {
        Console.WriteLine(string.Format("BaseClass:\n\r\tLoggerInitiated is {0} at {1}\n\r", loggerInstantiated, instantiatedAt ) );
    }
}

class DerivedClass01 : BaseClass
{ 
    public DerivedClass01( bool loggerInstantiated, DateTime instantiatedAt ) : base(loggerInstantiated, instantiatedAt)
    {
        Console.WriteLine( string.Format( "DerivedClass01:\n\r\tLoggerInitiated is {0} at {1}\n\r", loggerInstantiated, instantiatedAt ) );
    }
}

class DerivedClass02 : BaseClass
{
    public DerivedClass02( bool loggerInstantiated, DateTime instantiatedAt ) : base( loggerInstantiated, instantiatedAt )
    {
        Console.WriteLine( string.Format( "DerivedClass02:\n\r\tLoggerInitiated is {0} at {1}\n\r", loggerInstantiated, instantiatedAt ) );
    }
}

输出:

/*
BaseClass:
    LoggerInitiated is True at 21/12/2018 14:16:09

DerivedClass01:
    LoggerInitiated is True at 21/12/2018 14:16:09

BaseClass:
    LoggerInitiated is True at 21/12/2018 14:16:09

DerivedClass02:
    LoggerInitiated is True at 21/12/2018 14:16:09
*/

结论:

这当然是一种选择。从BaseClass 派生的每个类都可以使用相同的Logger。但是由于我的Logger 类的大小,我认为最好不要将整个实例作为参数传递。

编辑:

正如@Malior 和@ScottHannen 所指出的,当您将类的实例作为参数时,我完全误解了实际发生的情况。我不想这样做的原因现在有些无效,但我仍然宁愿在实例化派生自BaseClass 的类时不必将Logger 作为参数传递。


从基类继承

创建一个继承自ConfigurationSection 的BaseClass,并在BaseClass 上实例化Logger。任何相关的ConfigurationSection 类都可以从这个BaseClass 继承。

代码:

class Program
{
    static void Main( string[] args )
    {
        DerivedClass01 dc01 = new DerivedClass01();

        // added to create delay between the instantiation of different derived classes
        System.Threading.Thread.Sleep( 1000 );

        DerivedClass02 dc02 = new DerivedClass02();
    }
}

class BaseClass : ConfigurationSection
{
    // These variables are used for illustrative purposed,
    // instead of showing my `Logger' class
    public bool LoggerInstantiated;
    public DateTime InstantiatedAt;

    public BaseClass()
    {
        LoggerInstantiated = true;
        InstantiatedAt = DateTime.Now;

        Console.WriteLine(string.Format("BaseClass:\n\r\tLoggerInitiated is {0} at {1}\n\r", LoggerInstantiated, InstantiatedAt));
    }
}

class DerivedClass01 : BaseClass
{
    public DerivedClass01()
    {
        Console.WriteLine( string.Format( "DerivedClass01:\n\r\tLoggerInitiated is {0} at {1}\n\r", LoggerInstantiated, InstantiatedAt ) );
    }
}

class DerivedClass02 : BaseClass
{
    public DerivedClass02()
    {
        Console.WriteLine( string.Format( "DerivedClass02:\n\r\tLoggerInitiated is {0} at {1}\n\r", LoggerInstantiated, InstantiatedAt ) );
    }
}

输出:

/*
BaseClass:
    LoggerInitiated is True at 21/12/2018 14:21:51

DerivedClass01:
    LoggerInitiated is True at 21/12/2018 14:21:51

BaseClass:
    LoggerInitiated is True at 21/12/2018 14:21:52

DerivedClass02: 
    LoggerInitiated is True at 21/12/2018 14:21:52
*/

结论:

这不好。每次实例化派生类时都会创建一个新实例 Logger(这很明显 - 我应该早点意识到......)


从包含静态 Logger 的基类继承

与上面类似,但不是实例Logger,而是有一个静态实例。在实例化DerivedClasses 之前创建它,并且不要在BaseClass 构造函数中创建新实例。

代码:

class Program
{
    static void Main( string[] args )
    {

        BaseClass.LoggerInstantiated = true;
        BaseClass.InstantiatedAt = DateTime.Now;

        DerivedClass01 dc01 = new DerivedClass01();

        // added to create delay between the instantiation of different derived classes
        System.Threading.Thread.Sleep( 1000 );

        DerivedClass02 dc02 = new DerivedClass02();
    }
}

class BaseClass : ConfigurationSection
{
    // These variables are used for illustrative purposed,
    // instead of showing my `Logger' class
    public static bool LoggerInstantiated;
    public static DateTime InstantiatedAt;

    public BaseClass()
    {
        Console.WriteLine(string.Format("BaseClass:\n\r\tLoggerInitiated is {0} at {1}\n\r", LoggerInstantiated, InstantiatedAt));
    }
}

class DerivedClass01 : BaseClass
{
    public DerivedClass01()
    {
        Console.WriteLine( string.Format( "DerivedClass01:\n\r\tLoggerInitiated is {0} at {1}\n\r", LoggerInstantiated, InstantiatedAt ) );
    }
}

class DerivedClass02 : BaseClass
{
    public DerivedClass02()
    {
        Console.WriteLine( string.Format( "DerivedClass02:\n\r\tLoggerInitiated is {0} at {1}\n\r", LoggerInstantiated, InstantiatedAt ) );
    }
}

输出:

/*
BaseClass:
    LoggerInitiated is True at 21/12/2018 14:05:56

DerivedClass01:
    LoggerInitiated is True at 21/12/2018 14:05:56

BaseClass:
    LoggerInitiated is True at 21/12/2018 14:05:56

DerivedClass02:
    LoggerInitiated is True at 21/12/2018 14:05:56
*/

结论:

这似乎是一个合理的选择。从BaseClass 派生的每个类都可以访问相同的Logger,而无需将其作为参数传递。

【问题讨论】:

  • 为什么不把记录器做成一个静态类,不用实例化就到处使用呢?
  • @NikhilVartak 问题是相关部分,只有几行。其余的只是我尝试过的各种选择。随意忽略它们,并根据我的问题为我提供一些帮助。谢谢
  • @rashmatash 实际上这不是一个坏主意,但这意味着对许多现有代码进行相当大的更改。我认为我更喜欢我的第三个选项(上图),即在BaseClass 中对实例化记录器进行静态引用,因为精简意味着对现有代码的更改更少。谢谢
  • 在您的第一个示例中,记录器类的大小是什么意思?它是一个类实例,你只转发指针。这不重。
  • 您能否澄清一下:“但由于我的 Logger 类的大小,我认为最好不要将整个实例作为参数传递。” “大小”是什么意思?当您传递对对象的引用时,您只是传递了一个指针。它没有创造任何新的东西或复制任何现有的东西。所以你可能会担心一些无关紧要的事情。

标签: c# class inheritance


【解决方案1】:

您不必将记录器定义为单例或静态类。这会导致无法测试您的代码。还有其他方法可以确保所有类都使用相同的实例。

我将从一个界面开始。编写它以便描述您希望您的类如何使用它,如下所示:

public interface ILogger
{
    void Log(string message);
}

然后,像这样将它注入到你的类中:

public class YourClass
{
    private readonly ILogger _logger;

    public YourClass(ILogger logger)
    {
        _logger = logger;
    }

    private void MethodThatDoesWhatever()
    {
        try
        {
            // do something
        }
        catch(Exception ex)
        {
            _logger.Log(ex.ToString());
        }
    }
}

您还没有编写ILogger 的实现,但这就是重点。如果即使没有实现,您的类也可以使用ILogger,这意味着您的类与ILogger 的实现完全解耦。它依赖于一个抽象,即依赖倒置。

您如何确保所有类都使用相同的实例?如果你使用像 Windsor 这样的依赖注入容器,它可能看起来像这样:

var container = new WindsorContainer();
container.Register(
    Component.For<ILogger, YourLoggerImplementation>(),
    Component.For<YourClass>(),
    Component.For<SomeOtherClassThatNeedsLogger>()
);

当您向容器请求YourClass 的实例时,

var myClass = container.Resolve<YourClass>();
  • 它将尝试创建YourClass 的实例。
  • 它将确定构造函数需要ILogger。
  • 您告诉它用于ILogger 的组件(类)是YourLoggerImplementation,因此它将创建其中一个并将其传递给构造函数。
  • 除非您另有说明,否则它不会创建YourLoggerImplementation 的新实例。它只会保留相同的实例并重用它,因此现在您的所有类都使用相同的记录器实例。

关于如何使用 IoC 容器(Windsor、AutoFac、Unity、Ninject、.NET ServiceCollection 等)我已经完全掩盖了很多。值得理解,但这超出了这个答案的范围。要了解更多信息,您需要从您正在编写的应用程序类型开始。例如,您将搜索“使用 ASP.NET MVC 的依赖注入”或“使用 WPF 的依赖注入”,因为您开始的方式与应用程序的类型有关。 (即使这样我也过于简单化了,但 IMO 最好从这种方式开始。当您看到针对您正在使用的应用程序类型量身定制的示例时,它更有可能点击。)

然后你必须自己编写记录器实现。如果它正在写入文件并且多个类实例都在使用它,我建议将消息​​放在ConcurrentQueue 中的实现。然后,当队列达到一定大小或以固定的计时器间隔(或两者兼而有之)时,您将消息刷新到文件中。使用ConcurrentQueue,多个线程可以同时添加消息,一个线程可以在其他线程写入新消息的同时从队列中取出消息并将它们写入文件。

【讨论】:

  • 这听起来不错。我以前从未真正使用过interface,也从未研究过依赖注入,所以我将对它们进行一些研究。但是,您的描述听起来就像我所追求的确切功能,即使它与我提出问题时的想法相去甚远,但我认为这是最合适的答案。谢谢
【解决方案2】:

你可以使用单例模式

Singleton Pattern C#

public sealed class Singleton
{
    private static readonly Singleton singleton= new Singleton();

    public static Singleton Instance { get { return singleton; } }

    private Singleton()
    {
    }
}

因此您可以在任何部分或代码中使用相同的实例:

var singleton= Singleton.Instance;
var foo = new Foo(singleton);

或者您可能想使用依赖注入框架,例如 Ninject、Unity、Simple Injector

【讨论】:

  • 2018 年有点过时了吧? ... :o
  • @Sam 这是发展模式,它不会变老。答案建议使用 DI 框架,这是一种非常现代的使用方式
  • @pwrigshihanomoronimo,嗯,嘿——我自己也喜欢单身。虽然模式确实会变老,但单身人士不再流行是有正当理由的( testablitiy )。 DI 将是一个答案,但这与 Singletons 几乎没有关系,而且您真的不会使用单例服务进行日志记录吗?只需将对象作为 ctor 参数传入,就可以在以后更改 DI,而 Singleton impl 将永远存在……但这只是一个想法,真的 :)
  • 请不要单身。即使它是必须的,它也应该是线程安全的。这个不是。使用 Lazy 类作为私有实例。
  • @Sam 我同意你的观点:D 并没有像“单例”术语本身那样关注实现/使用
【解决方案3】:

我同意@rashmatash。我自己有一个静态记录器,可以在任何地方使用。它可以创建新文件,也可以附加到现有文件。

公共方法接受和异常,可选的自定义消息,以及可选的所需日志文件的完整路径的字符串。

public static void LogError(Exception Ex, string Message = null, string LogFile = null)

如果 LogFile 为空,将在应用程序基目录 + \Logs 中创建/附加一个名为 ErrorLog.txt 的日志文件。

这里是主要部分:

if (LogFile == null || LogFile == String.Empty)
            {
                LogFile = AppDomain.CurrentDomain.BaseDirectory + @"\Logs\ErrorLog.txt";
            }

            string logDir = Path.GetDirectoryName(LogFile);
            int lineNo = GetExceptionLineNumber(Ex);
            string message = CreateMessageString(Ex, lineNo, Message);

            try
            {
                if (VerifyOrCreateDirectory(logDir) == true)
                {
                    StreamWriter streamWriter = new StreamWriter(path: LogFile, append: true);

                    string logEntryHeaderInfo = DateTime.Now + " :: " + AppDomain.CurrentDomain.FriendlyName.ToString() + " :: " + Environment.UserName + " :: " + Environment.MachineName;
                    streamWriter.WriteLine(logEntryHeaderInfo + " :: " + message);
                    streamWriter.Flush();
                    streamWriter.Close();
                }
            }

【讨论】:

  • 这听起来是个不错的选择。您能否举例说明您是如何实现这一目标的?我主要对为使用相同记录器的每个不同类创建新文件感到疑惑。谢谢
  • 添加了更多细节。
猜你喜欢
  • 1970-01-01
  • 2020-10-14
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-10-23
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多