所谓的“跨领域关注点”是打破SOLID / DI / IoC的有效借口吗?


43

我的同事们喜欢说“日志记录/缓存/等是一个跨领域的问题”,然后继续在各处使用相应的单例。但是他们喜欢IoC和DI。

违反SOLI D原则真的是一个合理的借口吗?


27
我发现SOLID原则仅对那些已经具有必要的经验以不知不觉地遵循它们的人有用。
svidgen '16

1
我发现术语“跨领域关注”是一个非常具体的术语(与各方面有关),不一定与单例同义。在某些情况下,日志记录可能是一个横切关注的问题,因为您有时希望以相同的方式在多个位置记录通用消息(例如,将每次调用与服务调用方法记录到服务方法中,或由其他人进行记录)。但是,我认为(一般意义上的)日志记录并不是一个横切关注的问题,仅仅因为它已被广泛使用。我认为这更像是一个“普遍存在的问题”,尽管这并不是一个标准术语。
佩斯

4
@svidgen SOLID原则在不了解它们的人查看您的代码并询问您为什么这样做时也很有用。能够指出一个原则并说“这就是为什么”,这真是太好了
candied_orange

1
您有任何示例为什么您认为日志记录是特例?将数据重定向到(通常是所用X框架的一部分)的过程非常简单。
ksiimson '16

Answers:


42

没有。

SOLID作为解决不可避免变化的准则而存在。您是否真的不会更改日志记录库,目标,过滤或格式或...?您是否真的不会更改缓存库,目标,策略,作用域或...?

当然可以 在最起码,你会想嘲笑这些东西在一个健全的方式来隔离它们用于测试。而且,如果您想将它们隔离进行测试,则很可能会遇到业务原因,出于现实原因要隔离它们。

然后您将得到一个论点,即记录器本身将处理更改。“哦,如果目标/过滤/格式/策略发生变化,那么我们只需更改配置即可!” 那是垃圾 现在,您不仅拥有处理所有这些问题的God Object,而且还以XML(或类似格式)编写代码,而您没有静态分析,也没有编译时错误,并且您没有真正获得有效的单元测试。

是否有违反SOLID准则的案例?绝对。有时情况不会改变(无论如何都不需要完全重写)。有时稍微违反LSP是最干净的解决方案。有时制作一个隔离的接口没有任何价值。

但是,日志记录和缓存(以及其他普遍存在的跨领域问题)并非如此。它们通常是忽略准则时遇到的耦合和设计问题的绝佳示例。


22
那你有什么选择呢?将ILogger注入您编写的每个类中吗?为每个需要记录器的类编写一个装饰器?
罗伯特·哈维

16
是的 如果某物是依赖项,则使其成为依赖项。而且,如果这意味着过多的依赖关系,或者您不应该将记录器传递到某个地方,那就好了,现在您可以修复您的设计。
泰拉斯汀


20
我对教条式IoC的真正问题是,几乎所有现代代码都依赖于既不注入也不抽象的东西。如果你写C#,例如,你不会经常去抽象的String或者Int32甚至是List你的模块。计划变更在某种程度上是合理理智的。而且,除了最明显的“核心”类型之外,识别您可能会更改的内容实际上只是经验和判断的问题。
svidgen '16

6
@svidgen-希望您不要因为主张教条式IoC而没有读过我的答案。仅仅因为我们没有抽象出这些核心类型(以及其他一些审慎的东西),并不意味着可以拥有一个全局的List / String / Int / etc。
泰拉斯汀

38

这就是“跨领域关注点”一词的全部要点-这意味着某些内容不能完全符合SOLID原则。

这是理想主义与现实相遇的地方。

刚接触SOLID和跨领域的人经常会遇到这种精神挑战。没关系,别害怕。力争将所有内容都放在SOLID方面,但是在日志记录和缓存等一些地方,SOLID根本没有用。横切是SOLID的兄弟,他们携手并进。


9
一个合理,实用,非教条的答案。
user1936

11
您能告诉我们为什么所有五个SOLID原则都因跨领域关注而被破坏了吗?我很难理解。
布朗

4
是的 我可能会想到一个很好的理由来单独“打破”每个原则。但不是打破所有5条的唯一原因!
svidgen '16

1
对于我来说,不需要每次模拟单例记录器/缓存进行单元测试时都不必将头撞墙。试想一下,如果没有对Web应用程序进行单元测试HttpContextBase(正是由于这个原因而引入的),那会是什么样子。我可以肯定的是,如果没有这堂课,我的现实会真的很酸。
devnull

2
@devnull也许是问题所在,而不是横切关注本身……
蜘蛛鲍里斯(Boris)

25

对于日志记录,我认为是。日志无处不在,通常与服务功能无关。使用日志记录框架单例模式是常见且易于理解的。如果不这样做,那么您将在各处创建和注入记录器而您并不需要这样做。

上面的一个问题是,有人会说“但是我该如何测试日志记录?” 。我的想法是,除了断言可以真正读取日志文件并理解它们之外,我通常不会测试日志记录。当我看到日志测试已通过测试时,通常是因为有人需要断言某个类实际上已经做了某件事,并且他们正在使用日志消息来获取该反馈。我宁愿在该类上注册一些侦听器/观察器,并在测试中声明被调用。然后,您可以将事件日志记录放入该观察器中。

我认为缓存是完全不同的情况。


3
到现在为止,我已经对FP有所涉猎,因此对使用(也许是单子函数)功能组合在用例中嵌入日志记录,错误处理等内容引起了极大的兴趣,而又没有将它们放在核心业务逻辑中(见fsharpforfunandprofit.com/rop
萨拉

5
日志记录应该普及。如果是这样,那么您记录的日志太多了,而您实际上需要查看这些日志时只是为自己制造噪音。通过注入记录器依赖项,您不得不考虑是否实际需要记录某些内容。
RubberDuck

12
@RubberDuck-告诉我,当我从现场获得错误报告时,“记录日志不应该是普遍的”,并且唯一的调试功能是我必须弄清楚发生了什么是我不想使其普遍存在的日志文件。吸取的教训是,日志记录应该是“无处不在的”,非常普遍。
邓肯

15
@RubberDuck:使用服务器软件,日志记录是生存的唯一途径。这是找出12个小时前发生的错误的唯一方法,而不是现在就在您自己的笔记本电脑上。不用担心噪音。使用日志管理软件,以便您可以查询日志(理想情况下,您还应该设置警报,以通过错误日志向您发送电子邮件)
slebetman 2016年

6
我们的支持人员打电话给我,并告诉我“客户单击了某些页面,然后弹出错误。” 我问他们错误是什么意思,他们不知道。我问客户具体点击了什么,他们不知道。我问他们是否可以繁殖,他们不能。我同意,在几个关键的枢纽点上,使用部署良好的记录器通常可以实现日志记录,但是在这里和那里很少出现的奇异消息也很有价值。担心日志记录会导致软件质量降低。如果最终存储的数据过多,请修剪一下。不要过早优化。
佩斯

12

我的2美分...

是的,没有。

永远不要真的侵犯您采用的原则; 但是,为了达到更高的目标,您的原则应始终保持细微差别并被采用。因此,在有适当条件的理解下,某些明显的违反可能不是对“精神”或“整体原则”的实际违反。

尤其是SOLID原则,除了需要很多细微差别外,最终还是要服从“交付可运行,可维护的软件”的目标。因此,遵循任何特定的SOLID原则都是自欺欺人的,并且与SOLID的目标相矛盾。在这里,我经常注意到,提供王牌可维护性

因此,有关的什么dSOLID好的,它通过使您的可重用模块相对于其上下文相对不可知,从而有助于提高可维护性。我们可以将“可重用模块”定义为“您计划在另一个不同上下文中使用的代码集合”。这适用于单个函数,类,类集和程序。

是的,更改记录器实现可能会使您的模块进入“另一个不同的上下文”。

因此,让我提供两个重要警告

首先: 围绕构成“可重用模块”的代码块划清界线是专业判断的问题。而且您的判断必然限于您的经验。

如果没有目前在另一种情况下使用一个模块计划,它是可能确定它在其上无奈地依赖。警告:您的计划可能是错误的-但这没关系。您编写的模块之间的时间越长,对“是否有一天我会再次需要”的理解就会越来越直观和准确。但是,您可能永远无法回顾性地说:“我已经尽最大可能对所有组件进行了模块化和解耦,但没有多余的内容。”

如果您对自己的判断错误感到内,请认罪并继续...

其次: 反转控制不等于注入依赖项

当您开始注入依赖项nauseam时尤其如此。依赖注入是总体IoC策略的有用策略。但是,我认为与其他一些策略(例如使用接口和适配器)相比,DI的有效性要差一些,它们是从模块内部暴露于上下文的单个点。

让我们真正地专注于此。因为,即使您注入了Logger 广告恶心,也需要针对该Logger界面编写代码。您无法开始使用Logger来自其他供应商的新产品,该新产品以不同的顺序获取参数。该能力来自针对模块中存在的接口的编码,该接口存在于模块中,并且其中具有单个子模块(适配器)来管理依赖性。

而且,如果您要针对某个适配器进行编码,则无论Logger是将其注入该适配器还是由适配器发现,对于总体可维护性目标而言,通常都是微不足道的。更重要的是,如果您具有模块级适配器,则将其注入任何东西可能是荒谬的。它是模块编写的。

tl; dr-不要为原则而大惊小怪,不用考虑为什么要使用原则。而且,实际上,只需Adapter为每个模块构建一个。在确定在何处绘制“模块”边界时,请使用您的判断。在每个模块中,继续并直接参考Adapter。当然,将真实的记录器注入Adapter-而不是注入可能需要的所有小东西。


4
伙计...我会给出一个简短的答案,但是我没有时间。
svidgen '16

2
+1用于使用适配器。您需要隔离对第三方组件的依赖关系,以便尽可能地替换它们。日志记录是实现此目标的一个简单目标-存在许多日志记录实现,并且大多数都具有类似的API,因此简单的适配器实现可使您非常轻松地对其进行更改。对于那些说您永远都不会更改日志记录提供程序的人:我不得不这样做,这造成了真正的痛苦,因为我没有使用适配器。
Jules

“特别是SOLID原则,除了需要很多细微差别外,最终还是要服从“交付可运行的,可维护的软件”的目标。”-这是我所见过的最好的陈述之一。作为温和的OCD编码器,我在理想主义与生产力之间进行了挣扎。
DVK

每当我看到+1或对旧答案的评论时,我都会再次阅读我的答案,并再次发现我是一个非常混乱且不清楚的作家……尽管如此,我还是很欣赏@DVK。
svidgen '16

适配器可以节省生命,它们的成本不高,并且使您的生活变得更加轻松,特别是当您要应用SOLID时。与他们进行测试很容易。
艾伦(Alan)

8

日志记录应该始终作为一个单例实施的想法是经常被人告知的谎言之一。

只要有现代操作系统,就已经认识到您可能希望根据输出性质登录到多个位置。

系统设计者应该不断地质疑过去的解决方案的有效性,然后再盲目地将它们包含在新解决方案中。如果他们没有进行这样的努力,那么他们就没有做好自己的工作。


2
那么,您提出的解决方案是什么?是否将ILogger注入您编写的每个类中?如何使用装饰器模式?
罗伯特·哈维

4
现在还没有真正回答我的问题,是吗?我仅以示例方式说明了这些模式...如果您有更好的想法,则不必是这些模式。
罗伯特·哈维,

8
就任何具体建议而言,我真的不太理解这个答案
Brian Agnew

3
@RobbieDee-您是说日志记录应该通过我们可能希望记录到多个位置的“偶然性”以最复杂和最不方便的方式实施?即使发生了类似的更改,您真的认为将功能添加到现有Logger实例上的工作要比在决定是否要使用Logger时在类和更改接口之间传递Logger所做的全部工作更多。数十个项目中永远不会进行多地点记录?
邓肯

2
关于:“您可能希望根据输出的性质将日志记录到多个位置”:可以,但是最好的处理方法是在日志记录框架中,而不是尝试注入多个单独的日志记录依赖项。(通用的日志记录框架已经支持了这一点。)
ruakh

4

真正记录是一种特殊情况。

@Telastyn写道:

您真的不会更改日志记录库,目标,过滤或格式或...吗?

如果您预计可能需要更改日志记录库,则应使用外观。即SLF4J(如果您在Java世界中)。

至于其余的内容,一个体面的日志库负责更改日志记录的位置,过滤哪些事件,如何使用日志记录器配置文件和(如有必要)自定义插件类格式化日志事件。有许多现成的选择。

简而言之,这些已解决的问题……用于记录……因此无需使用“依赖注入”来解决它们。

DI(相对于标准日志记录方法)可能是有益的唯一情况是,您想对应用程序的日志进行单元测试。但是,我怀疑大多数开发人员会说日志记录不是类功能的一部分,也不是需要测试的内容。

@Telastyn写道:

然后您将得到一个论点,即记录器本身将处理更改。“哦,如果目标/过滤/格式/策略发生变化,那么我们只需更改配置即可!” 那是垃圾 现在,您不仅拥有处理所有这些事情的God Object,而且还以XML(或类似格式)编写代码,而您没有静态分析,也没有编译时错误,并且您没有真正获得有效的单元测试。

恐怕这是一个非常理论上的问题。实际上,大多数开发人员和系统集成人员都喜欢可以通过配置文件配置日志记录。他们喜欢这样的事实,即不希望对单元的日志进行单元测试。

当然,如果您填充日志记录配置,则可能会遇到问题,但是它们将表现为应用程序在启动过程中失败或日志记录过多/过少。1)通过修复配置文件中的错误,可以轻松解决这些问题。2)替代方法是每次更改日志记录级别时都有完整的构建/分析/测试/部署周期。那是不可接受的。


3

是的 没有

是的:我认为不同的子系统(或语义层或库或模块化捆绑的其他概念)在初始化期间各自接受(相同或)可能不同的记录器是合理的,而不是所有子系统都依赖相同的公共共享单例

然而,

否:同时对每个小对象(通过构造函数或实例方法)进行参数化日志记录是不合理的。为避免不必要和毫无意义的膨胀,较小的实体应使用其封闭上下文的单例记录器。


这是在多个层次上考虑模块化的原因之一:方法被捆绑到类中,而类被捆绑到子系统和/或语义层中。这些较大的捆绑包是有价值的抽象工具。与跨越边界时相比,我们应该在模块化边界内给出不同的考虑。


3

首先,它以强大的单例缓存开始,接下来您将看到数据库层的强大单例,引入了全局状态,classes的非描述性API 和不可测试的代码。

如果您决定不为数据库提供单例,那么为缓存提供单例可能不是一个好主意,毕竟,它们代表了非常相似的概念,即数据存储,仅使用不同的机制。

在类中使用单例会将具有特定数量的依赖关系的类转换为理论上具有无限数量的依赖关系的类,因为您永远不知道静态方法背后真正隐藏了什么。

在过去的十年中,我花了很多时间在编程上,目睹了更改日志记录逻辑的努力(当时写为单例)。因此,尽管我喜欢依赖注入,但日志记录并不是真正的大问题。另一方面,对于缓存,我肯定总是将其作为依赖项。


是的,日志记录很少更改,通常不必是可交换模块。但是,我曾经尝试对日志记录帮助程序类进行单元测试,该类对日志记录系统具有静态依赖性。我认为,使其可测试的最简单机制是在一个单独的进程中运行被测类,配置其记录器以写入STDOUT,然后在我的测试用例中解析该输出ಠ_clock d显然除了实时之外什么都不想要,对吗?当然,除了在测试时区/ DST边缘情况时…
amon

@amon:时钟就像登录一样,已经有另一种机制与DI发挥相同的作用,即Joda-Time及其许多端口。(不是使用DI代替有什么问题;但是直接使用Joda-Time比尝试编写自定义可注射适配器要容易得多,而且我从未见过有人后悔。)
ruakh

@amon“我在时钟上也有类似的经验,您显然除了实时之外什么都不需要,对吗?当然,除了在测试时区/ DST边缘情况时……” –或当您意识到一个错误损坏了数据库,将其恢复的唯一希望是解析事件日志并从上次备份开始重播它……但是突然之间,您需要所有代码根据当前日志条目的时间戳(而不是当前日志)来工作时间。
Jules

3

是和否,但大多是否

我假设大多数对话都是基于静态实例与注入实例的。没有人提出日志记录会破坏我假设的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。


这就是我偶然记录的方式。我想链接到一篇文章或其他内容,但是找不到。如果您位于.NET世界中,并查找事件日志,则会找到Semantic Logging Application Block。具有讽刺意味的是,不要使用它,就像MS“模式和实践”团队创建的大多数代码一样,它是一种反模式。
内森·库珀

3

第一次横切关注点不是主要的构建基块,不应将其视为系统中的依赖项。如果未初始化Logger或高速缓存不工作,则系统应该可以工作。您将如何降低系统的耦合性和凝聚力?这就是SOLID在OO系统设计中发挥作用的地方。

将对象保持为单例与SOLID无关。那就是您的对象生命周期,您希望对象在内存中保留多长时间。

需要依赖项进行初始化的类不应知道所提供的类实例是单例还是瞬态。但是tldr; 如果您在每个方法或类中都编写Logger.Instance.Log(),则它的代码有问题(代码异味/硬耦合),真是一团糟。这是人们开始滥用SOLID的时刻。像OP这样的开发人员也开始提出这样的真正问题。


2

我已经使用继承和特征(在某些语言中也称为mixins)的组合解决了这个问题。特质对于解决这种跨领域关注非常方便。尽管它通常是语言功能,所以我认为真正的答案是它取决于语言功能。


有趣。我从未使用支持特质的语言来做任何重要的工作,但是我已经考虑过在将来的某些项目中使用这种语言,因此如果您可以对此进行扩展,并展示特质如何解决问题,这对我会有所帮助。
Jules
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.