从零开始构建工业自动化软件框架:基础框架搭建(一)容器与日志功能实现

软件命名

昨天计划开始做这个软件框架后,我就迫不及待开始了软件的编写,首先就是给项目取个名字。我个人比较喜欢《三体》,所以用了三体里面智子的英文Sophon来作为我解决方案的名称。

架构调整

然后在架构上面,我也做了小的调整。

Common类库主要时写一些公共可以复用的内容, 包括容器,日志,配置管理这些。我想的是到手其他项目上我也能直接拿过去使用。

Application类库中,计划主要放设备运行流程,这样如果需要调整运行逻辑只需要更改这里即可。

Core类库中,计划主要放设备逻辑的抽象,以及状态机的管理,这个时框架的重点内容之一。

Infrastructure类库中,计划主要放硬件管理,数据管理这些内容。

这样各个项目之间的引用也发生了一些变化。

工程项目 依赖关系
Common 无引用
Sophon.Application 引用 Core、Infrastructure、Common
Sophon.Core 引用 Common
Sophon.Infrastructure 引用 Common
Sophon.UI 引用 Application

功能代码

架构搭建完成后,就是开始代码的编写。

按照之前的计划,这一步应该开始完成IOC/日志/配置管理这几项。

IOC容器部分

编写代码

IOC容器我选用了autofac,原因其实很简单,我之前简单用过这个,不想再增加新的学习成本所以就直接使用了。

我希望通过自动注册降低耦合,提升可扩展性,所以就会有一个类来集中注册所有的类的地方。而对于不同的项目则需要不同的注册类,对于这些类,则需要定义他们的行为Register(),这样就需要一个接口来约束注册类。

csharp 复制代码
//接口
public interface IModuleRegister
{
    /// <summary>
    /// 注册模组内所有需要注册的类
    /// </summary>
    /// <param name="builder"></param>
    void Register(ContainerBuilder builder);
}

每个项目都需要一个注册类,实现接口,然后在实现的方法里面完成当前项目的注册。

scss 复制代码
//Common模块注册类
public class CommonModuleRegister : IModuleRegister
{
    public void Register(ContainerBuilder builder)
    {
        var configPath = ConfigurationManager.AppSettings["ConfigPath"];
        var configType = (ConfigType)Enum.Parse(typeof(ConfigType), ConfigurationManager.AppSettings["ConfigType"]);

        //注册日志工厂
        builder.Register(c => new LoggerFactory(name => new NlogManager(name)))
               .As<ILoggerFactory>()
               .SingleInstance();
        //注册配置器
        builder.Register(c => new ConfigManager(configType, configPath))
               .As<IConfigManager>()
               .SingleInstance();
    }
}

当所有项目都注册完成后,通过查找所有项目中继承IModuleRegister的类,然后调用其Register方法,这样就可以注册完成所有需要注册的类。

csharp 复制代码
//注册所有模块的方法
public static class RegisterAllModule
{
    /// <summary>
    /// 注册所有模组的扩展方法
    /// </summary>
    /// <param name="builder"></param>
    public static ContainerBuilder RegisterAllModuleExt(this ContainerBuilder builder)
    {
        //1、加载所有assembly
        List<Assembly> assemblies = new List<Assembly>();
        foreach (var name in assemblyNames)
        {
            try
            {
                assemblies.Add(Assembly.Load(name));
            }
            catch (Exception e)
            {
                Trace.WriteLine($"加载{name}失败:" + e.Message);
            }
        }
        //2、获取所有继承IModuleRegister的类
        foreach (var assembly in assemblies)
        {
            var types = assembly.GetTypes()
                          .Where(x => typeof(IModuleRegister).IsAssignableFrom(x)
                                   && !x.IsInterface);
            //3、使用所有类的RegisterModule()方法
            foreach (var type in types)
            {
                try
                {
                    var instance = (IModuleRegister)Activator.CreateInstance(type);
                    instance.Register(builder);
                }
                catch (Exception e)
                {
                    Trace.WriteLine(e.Message);
                }
            }
        }
        return builder;
    }

    /// <summary>
    /// 解决方案内所有项目名称集合
    /// </summary>
    private static readonly List<string> assemblyNames = new List<string>
        {
            "Common",
            "Sophon.Application",
            "Sophon.Core",
            "Sophon.Infrastructure",
            "Sophon.UI"
        };
}

改进代码

在编写完成后,我自己有重新看了一下这部分代码,发现有一些地方是可以优化的。

首先就是assemblyNames 这个集合,按照当前的写法,如果后期新增了其他的类库,则需要修改代码,这样就不够灵活,所以我把他放在了app.config中。

ini 复制代码
<add key="ModuleAssemblies" 
value="Common;Sophon.Application;Sophon.Core;Sophon.Infrastructure;Sophon.UI"/>

同时使用的地方也需要进行改动。

ini 复制代码
var assemblyNames = ConfigurationManager.AppSettings["ModuleAssemblies"].Split(';');

除此之外,我还给各个模组的注册类增加了一个特性,其中增加了顺序,可以决定注册的先后。

同时也与接口双重约束获取模组注册类,接口保证功能一致,Attribute用于控制注册顺序。

csharp 复制代码
[AttributeUsage(AttributeTargets.Class)]
 public class ModuleRegisterAttribute : Attribute
 {
     /// <summary>
     /// 根据order决定注册顺序
     /// </summary>
     public int Order { get; }
     public ModuleRegisterAttribute(int order = 0)
     {
         Order = order;
     }
 }

同时在获取时增加排序。

ini 复制代码
var types = assembly.GetTypes()
                    .Where(t => typeof(IModuleRegister).IsAssignableFrom(t)
                              && !t.IsInterface && !t.IsAbstract
                              && t.IsDefined(typeof(ModuleRegisterAttribute), false));
var moduletypes = types
    .Select(t => new
    {
        Type = t,
        Order = t.GetCustomAttribute<ModuleRegisterAttribute>().Order
    }).OrderBy(t => t.Order).ToList();

日志部分

日志我选用了NLog。

首先定义了一个接口,这样可以充分解耦,我在其他地方使用时只需要知道接口即可,也可以快速切换成其他日志框架。日志我希望可以指定名称,不同的模组使用时用不同的名称,这样我在查看时也比较方便。所以构造方法中需要传入loggerName。

csharp 复制代码
public interface ILoggerManager
{
    void Trace(string msg);
    void Debug(string msg);
    void Info(string msg);
    void Warn(string msg);
    void Error(string msg);
    void Fatal(string msg);
}

public class NlogManager : ILoggerManager
{
    private readonly ILogger _logger;

    /// <summary>
    /// 根据传入的名称创建日志
    /// </summary>
    /// <param name="loggername"></param>
    public NlogManager(string loggername)
    {
        _logger = LogManager.GetLogger(loggername);
    }

    public void Trace(string msg) => _logger.Trace(msg);
    public void Debug(string msg) => _logger.Debug(msg);
    public void Info(string msg) => _logger.Info(msg);
    public void Warn(string msg) => _logger.Warn(msg);
    public void Error(string msg) => _logger.Error(msg);
    public void Fatal(string msg) => _logger.Fatal(msg);
}

对于这样的日志,如果我想通过容器注册,那就只有一个名字,这样明显不符合我最开始的想法。所以采用工厂模式,解决日志名称动态分配的问题,容器中只注册工厂,然后再在调用方法时指定名字。

同时我希望能对以及建立的日志进行缓存,这样就不会再重复创建相同的日志。

csharp 复制代码
public interface ILoggerFactory
{
    ILoggerManager CreateLogger(string loggername);
}

public class LoggerFactory : ILoggerFactory
{
    /// <summary>
    /// 传入名称,返回ILoggerFactory
    /// </summary>
    private readonly Func<string, ILoggerManager> _loggerCreator;

    /// <summary>
    /// 存储所有日志缓存
    /// </summary>
    private readonly ConcurrentDictionary<string, ILoggerManager> _loggercache = new ConcurrentDictionary<string, ILoggerManager>();

    /// <summary>
    /// 传入带名字的委托
    /// </summary>
    /// <param name="loggerCreator"></param>
    public LoggerFactory(Func<string, ILoggerManager> loggerCreator)
    {
        _loggerCreator = loggerCreator;
    }

    /// <summary>
    /// 工厂创建日志
    /// 如果cache有,则直接给出,如果没有,则调用传入的委托,并把新的放入cache
    /// </summary>
    /// <param name="loggername"></param>
    /// <returns></returns>
    public ILoggerManager CreateLogger(string loggername)
    {
        return _loggercache.GetOrAdd(loggername, _loggerCreator);
    }
}

而在工厂里面采用委托注入具体实例的创建逻辑,降低工厂与具体实现的耦合,这样后期如果更换日志,工厂则不需要改动。所以在注册的时候传入一个委托,返回实例日志框架。

javascript 复制代码
builder.Register(c => new LoggerFactory(name => new NlogManager(name)))
       .As<ILoggerFactory>()
       .SingleInstance();

后记

以上是我当前已经完成的部分内容,其实配置部分也做了一部分,但是没有完全写完,等写完再一起整理理解然后发出来。

在写这个的过程中我也参考了以前使用过的框架以及网上搜索了一些比较好的解决方法。

当然,写的内容也有可能有一些错误,如果大家能看到错误的地方,还请帮忙指正一下,我也会同步更改我的代码并且在后续发出来。

我的代码也放在了Github里,地址是:[github.com/JeffreyXXL/...] ,欢迎大家围观。

相关推荐
CodeCraft Studio2 小时前
图像处理控件Aspose.Imaging教程:使用 C# 将 SVG 转换为 EMF
图像处理·microsoft·c#·svg·aspose·图片格式转换·emf
★YUI★3 小时前
学习游戏制作记录(将各种属性应用于战斗以及实体的死亡)8.5
学习·游戏·unity·c#
jason成都3 小时前
ubuntu编译opendds开发(C#)
linux·ubuntu·c#·opendds
小黄花呀小黄花5 小时前
从零开始构建工业自动化软件框架:基础框架搭建(三)容器、配置、日志功能测试
c#
马达加斯加D6 小时前
C# --- 本地缓存失效形成缓存击穿触发限流
开发语言·缓存·c#
q__y__L8 小时前
C# WaitHandle类的几个有用的函数
java·开发语言·c#
步、步、为营8 小时前
.NET8 正式发布, C#12 新变化
ui·c#·.net
伽蓝_游戏9 小时前
Unity UI的未来之路:从UGUI到UI Toolkit的架构演进与特性剖析(7)
游戏·ui·unity·架构·c#·游戏引擎·.net
q__y__L14 小时前
C#线程同步(三)线程安全
安全·性能优化·c#