软件命名
昨天计划开始做这个软件框架后,我就迫不及待开始了软件的编写,首先就是给项目取个名字。我个人比较喜欢《三体》,所以用了三体里面智子的英文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/...] ,欢迎大家围观。