我的同事们喜欢说“日志记录/缓存/等是一个跨领域的问题”,然后继续在各处使用相应的单例。但是他们喜欢IoC和DI。
违反SOLI D原则真的是一个合理的借口吗?
我的同事们喜欢说“日志记录/缓存/等是一个跨领域的问题”,然后继续在各处使用相应的单例。但是他们喜欢IoC和DI。
违反SOLI D原则真的是一个合理的借口吗?
Answers:
没有。
SOLID作为解决不可避免变化的准则而存在。您是否真的不会更改日志记录库,目标,过滤或格式或...?您是否真的不会更改缓存库,目标,策略,作用域或...?
当然可以 在最起码,你会想嘲笑这些东西在一个健全的方式来隔离它们用于测试。而且,如果您想将它们隔离进行测试,则很可能会遇到业务原因,出于现实原因要隔离它们。
然后您将得到一个论点,即记录器本身将处理更改。“哦,如果目标/过滤/格式/策略发生变化,那么我们只需更改配置即可!” 那是垃圾 现在,您不仅拥有处理所有这些问题的God Object,而且还以XML(或类似格式)编写代码,而您没有静态分析,也没有编译时错误,并且您没有真正获得有效的单元测试。
是否有违反SOLID准则的案例?绝对。有时情况不会改变(无论如何都不需要完全重写)。有时稍微违反LSP是最干净的解决方案。有时制作一个隔离的接口没有任何价值。
但是,日志记录和缓存(以及其他普遍存在的跨领域问题)并非如此。它们通常是忽略准则时遇到的耦合和设计问题的绝佳示例。
String
或者Int32
甚至是List
你的模块。计划变更在某种程度上是合理和理智的。而且,除了最明显的“核心”类型之外,识别您可能会更改的内容实际上只是经验和判断的问题。
是
这就是“跨领域关注点”一词的全部要点-这意味着某些内容不能完全符合SOLID原则。
这是理想主义与现实相遇的地方。
刚接触SOLID和跨领域的人经常会遇到这种精神挑战。没关系,别害怕。力争将所有内容都放在SOLID方面,但是在日志记录和缓存等一些地方,SOLID根本没有用。横切是SOLID的兄弟,他们携手并进。
HttpContextBase
(正是由于这个原因而引入的),那会是什么样子。我可以肯定的是,如果没有这堂课,我的现实会真的很酸。
对于日志记录,我认为是。日志无处不在,通常与服务功能无关。使用日志记录框架单例模式是常见且易于理解的。如果不这样做,那么您将在各处创建和注入记录器,而您并不需要这样做。
上面的一个问题是,有人会说“但是我该如何测试日志记录?” 。我的想法是,除了断言可以真正读取日志文件并理解它们之外,我通常不会测试日志记录。当我看到日志测试已通过测试时,通常是因为有人需要断言某个类实际上已经做了某件事,并且他们正在使用日志消息来获取该反馈。我宁愿在该类上注册一些侦听器/观察器,并在测试中声明被调用。然后,您可以将事件日志记录放入该观察器中。
我认为缓存是完全不同的情况。
我的2美分...
是的,没有。
永远不要真的侵犯您采用的原则; 但是,为了达到更高的目标,您的原则应始终保持细微差别并被采用。因此,在有适当条件的理解下,某些明显的违反可能不是对“精神”或“整体原则”的实际违反。
尤其是SOLID原则,除了需要很多细微差别外,最终还是要服从“交付可运行,可维护的软件”的目标。因此,遵循任何特定的SOLID原则都是自欺欺人的,并且与SOLID的目标相矛盾。在这里,我经常注意到,提供王牌可维护性。
因此,有关的什么d的SOLID?好的,它通过使您的可重用模块相对于其上下文相对不可知,从而有助于提高可维护性。我们可以将“可重用模块”定义为“您计划在另一个不同上下文中使用的代码集合”。这适用于单个函数,类,类集和程序。
是的,更改记录器实现可能会使您的模块进入“另一个不同的上下文”。
因此,让我提供两个重要警告:
首先: 围绕构成“可重用模块”的代码块划清界线是专业判断的问题。而且您的判断必然限于您的经验。
如果没有目前在另一种情况下使用一个模块计划,它是可能确定它在其上无奈地依赖。警告:您的计划可能是错误的-但这也没关系。您编写的模块之间的时间越长,对“是否有一天我会再次需要”的理解就会越来越直观和准确。但是,您可能永远无法回顾性地说:“我已经尽最大可能对所有组件进行了模块化和解耦,但没有多余的内容。”
如果您对自己的判断错误感到内,请认罪并继续...
其次: 反转控制不等于注入依赖项。
当您开始注入依赖项nauseam时尤其如此。依赖注入是总体IoC策略的有用策略。但是,我认为与其他一些策略(例如使用接口和适配器)相比,DI的有效性要差一些,它们是从模块内部暴露于上下文的单个点。
让我们真正地专注于此。因为,即使您注入了Logger
广告恶心,也需要针对该Logger
界面编写代码。您无法开始使用Logger
来自其他供应商的新产品,该新产品以不同的顺序获取参数。该能力来自针对模块中存在的接口的编码,该接口存在于模块中,并且其中具有单个子模块(适配器)来管理依赖性。
而且,如果您要针对某个适配器进行编码,则无论Logger
是将其注入该适配器还是由适配器发现,对于总体可维护性目标而言,通常都是微不足道的。更重要的是,如果您具有模块级适配器,则将其注入任何东西可能是荒谬的。它是为模块编写的。
tl; dr-不要为原则而大惊小怪,不用考虑为什么要使用原则。而且,实际上,只需Adapter
为每个模块构建一个。在确定在何处绘制“模块”边界时,请使用您的判断。在每个模块中,继续并直接参考Adapter
。当然,将真实的记录器注入Adapter
-而不是注入可能需要的所有小东西。
日志记录应该始终作为一个单例实施的想法是经常被人告知的谎言之一。
只要有现代操作系统,就已经认识到您可能希望根据输出的性质登录到多个位置。
系统设计者应该不断地质疑过去的解决方案的有效性,然后再盲目地将它们包含在新解决方案中。如果他们没有进行这样的努力,那么他们就没有做好自己的工作。
真正记录是一种特殊情况。
@Telastyn写道:
您真的不会更改日志记录库,目标,过滤或格式或...吗?
如果您预计可能需要更改日志记录库,则应使用外观。即SLF4J(如果您在Java世界中)。
至于其余的内容,一个体面的日志库负责更改日志记录的位置,过滤哪些事件,如何使用日志记录器配置文件和(如有必要)自定义插件类格式化日志事件。有许多现成的选择。
简而言之,这些已解决的问题……用于记录……因此无需使用“依赖注入”来解决它们。
DI(相对于标准日志记录方法)可能是有益的唯一情况是,您想对应用程序的日志进行单元测试。但是,我怀疑大多数开发人员会说日志记录不是类功能的一部分,也不是需要测试的内容。
@Telastyn写道:
然后您将得到一个论点,即记录器本身将处理更改。“哦,如果目标/过滤/格式/策略发生变化,那么我们只需更改配置即可!” 那是垃圾 现在,您不仅拥有处理所有这些事情的God Object,而且还以XML(或类似格式)编写代码,而您没有静态分析,也没有编译时错误,并且您没有真正获得有效的单元测试。
恐怕这是一个非常理论上的问题。实际上,大多数开发人员和系统集成人员都喜欢可以通过配置文件配置日志记录。他们喜欢这样的事实,即不希望对单元的日志进行单元测试。
当然,如果您填充日志记录配置,则可能会遇到问题,但是它们将表现为应用程序在启动过程中失败或日志记录过多/过少。1)通过修复配置文件中的错误,可以轻松解决这些问题。2)替代方法是每次更改日志记录级别时都有完整的构建/分析/测试/部署周期。那是不可接受的。
是的 , 没有!
是的:我认为不同的子系统(或语义层或库或模块化捆绑的其他概念)在初始化期间各自接受(相同或)可能不同的记录器是合理的,而不是所有子系统都依赖相同的公共共享单例。
然而,
否:同时对每个小对象(通过构造函数或实例方法)进行参数化日志记录是不合理的。为避免不必要和毫无意义的膨胀,较小的实体应使用其封闭上下文的单例记录器。
这是在多个层次上考虑模块化的原因之一:方法被捆绑到类中,而类被捆绑到子系统和/或语义层中。这些较大的捆绑包是有价值的抽象工具。与跨越边界时相比,我们应该在模块化边界内给出不同的考虑。
首先,它以强大的单例缓存开始,接下来您将看到数据库层的强大单例,引入了全局状态,class
es的非描述性API 和不可测试的代码。
如果您决定不为数据库提供单例,那么为缓存提供单例可能不是一个好主意,毕竟,它们代表了非常相似的概念,即数据存储,仅使用不同的机制。
在类中使用单例会将具有特定数量的依赖关系的类转换为理论上具有无限数量的依赖关系的类,因为您永远不知道静态方法背后真正隐藏了什么。
在过去的十年中,我花了很多时间在编程上,目睹了更改日志记录逻辑的努力(当时写为单例)。因此,尽管我喜欢依赖注入,但日志记录并不是真正的大问题。另一方面,对于缓存,我肯定总是将其作为依赖项。
是和否,但大多是否
我假设大多数对话都是基于静态实例与注入实例的。没有人提出日志记录会破坏我假设的SRP吗?我们主要在谈论“依赖倒置原则”。Tbh我大都同意Telastyn的不回答。
什么时候可以使用静态函数?因为很显然有时候还可以。抽象的好处是肯定的答案,“否”的答案指出它们是您要付出的。您的工作辛苦的原因之一是您无法写下并适用于所有情况的答案。
采取:
Convert.ToInt32("1")
我更喜欢这样:
private readonly IConverter _converter;
public MyClass(IConverter converter)
{
Guard.NotNull(converter)
_converter = conveter
}
....
var foo = _converter.ToInt32("1");
为什么? 我接受,如果我需要灵活地换出转换代码,则需要重构代码。我接受我将无法模拟这一点。我认为简单而简洁的做法值得进行此交易。
从频谱的另一端看,如果IConverter
是a SqlConnection
,我会非常震惊地看到这是一个静态调用。失败的原因显而易见。我要指出的是,在应用程序中,a SQLConnection
可以是相当“跨领域的”,所以我不会使用那些确切的词。
记录更像是SQLConnection
还是Convert.ToInt32
?我想说的更像是“ SQLConnection”。
您应该嘲笑Logging。它与外界对话。使用编写方法时Convert.ToIn32
,我将其用作一种工具来计算该类的其他一些可单独测试的输出。Convert
在检查“ 1” +“ 2” ==“ 3”时,我不需要检查是否被正确调用。日志记录是不同的,它是该类的完全独立的输出。我假设这是对您,支持团队和业务有价值的输出。如果日志记录不正确,您的课程将无法正常工作,因此不应通过单元测试。您应该测试类的日志。我认为这是杀手argument,我真的可以在这里停下来。
我也认为这很有可能会改变。良好的日志记录不仅可以打印字符串,还可以查看您的应用程序在做什么(我非常喜欢基于事件的日志记录)。我已经看到基本的日志记录变成了非常复杂的报告UI。如果您的日志记录看起来很像_logger.Log(new ApplicationStartingEvent())
,则不太容易朝这个方向前进Logger.Log("Application has started")
。可能有人争辩说,这正在为可能永远不会发生的未来创造库存,这是一个判断电话,我碰巧认为这是值得的。
实际上,在我的一个个人项目中,我纯粹使用_logger
来创建一个非日志记录UI 来确定应用程序在做什么。这意味着我不必编写代码即可弄清楚应用程序在做什么,而我最终获得了坚实的日志记录。我觉得如果我对伐木的态度是简单且不变,那么我就不会想到这个想法。
因此,对于记录日志,我同意Telastyn。
Semantic Logging Application Block
。具有讽刺意味的是,不要使用它,就像MS“模式和实践”团队创建的大多数代码一样,它是一种反模式。
第一次横切关注点不是主要的构建基块,不应将其视为系统中的依赖项。如果未初始化Logger或高速缓存不工作,则系统应该可以工作。您将如何降低系统的耦合性和凝聚力?这就是SOLID在OO系统设计中发挥作用的地方。
将对象保持为单例与SOLID无关。那就是您的对象生命周期,您希望对象在内存中保留多长时间。
需要依赖项进行初始化的类不应知道所提供的类实例是单例还是瞬态。但是tldr; 如果您在每个方法或类中都编写Logger.Instance.Log(),则它的代码有问题(代码异味/硬耦合),真是一团糟。这是人们开始滥用SOLID的时刻。像OP这样的开发人员也开始提出这样的真正问题。