在实现旁边记录是否违反SRP?


19

在考虑敏捷软件开发和所有原理(SRP,OCP等)时,我问自己如何对待日志记录。

在实现旁边记录是否违反SRP?

我会说,yes因为该实现也应该能够运行而无需登录。那么如何更好地实现日志记录呢?我检查了一些模式,得出的结论是,最好不要以用户定义的方式违反原则,而是使用已知违反原则的任何模式的最佳方式是使用装饰器模式。

假设我们有一堆完全没有违反SRP的组件,然后我们想要添加日志记录。

  • 成分A
  • 组件B使用A

我们想要记录A,因此我们创建了另一个装饰有A的组件D,都实现了接口I。

  • 接口我
  • 组件L(系统的日志记录组件)
  • 组件A实现I
  • 组件D实现I,修饰/使用A,使用L进行日志记录
  • 组件B使用I

优点:-我可以不使用日志就使用A-测试A意味着我不需要任何日志模拟-测试更简单

缺点:-更多组件和更多测试

我知道这似乎是另一个公开讨论的问题,但我实际上想知道是否有人使用的记录策略比装饰器或SRP违规更好。作为默认NullLogger的静态单例记录器又如何呢?如果需要syslog-logging,则在运行时更改实现对象?



我已经读过了,答案不令人满意,对不起。
Aitch 2015年


@MarkRogers感谢您分享那篇有趣的文章。鲍勃叔叔在“清洁代码”中说,一个不错的SRP组件正在处理具有相同抽象级别的其他组件。对我来说,这种解释更容易理解,因为上下文也可能太大。但是我不能回答这个问题,因为记录器的上下文或抽象级别是什么?
Aitch 2015年

3
“不是我的答案”或“答案不令人满意”有点不屑一顾。你可能会思考什么特别不满意的是(什么要求,你有没有被这个问题的答案是什么?特别是你的问题的独特见面吗?),然后编辑你的问题,以确保这一要求/独特的方面是清楚的解释。目的是让您编辑问题以使其更清晰,更有针对性,而不是要求样板断言您的问题不同/在没有理由的情况下不能结束。(您也可以在其他答案上发表评论。)
DW

Answers:


-1

是的,这是违反SRP的,因为日志记录是一个跨领域的问题。

正确的方法是将日志委托给记录器类(拦截),其唯一目的是进行记录-遵守SRP。

请参阅此链接以获取一个很好的示例:https : //msdn.microsoft.com/zh-cn/library/dn178467%28v=pandp.30%29.aspx

这是一个简短的示例

public interface ITenantStore
{
    Tenant GetTenant(string tenant);
    void SaveTenant(Tenant tenant);
}

public class TenantStore : ITenantStore
{
    public Tenant GetTenant(string tenant)
    {....}

    public void SaveTenant(Tenant tenant)
    {....}
} 

public class TenantStoreLogger : ITenantStore
{
    private readonly ILogger _logger; //dep inj
    private readonly ITenantStore _tenantStore;

    public TenantStoreLogger(ITenantStore tenantStore)
    {
        _tenantStore = tenantStore;
    }

    public Tenant GetTenant(string tenant)
    {
        _logger.Log("reading tenant " + tenant.id);
        return _tenantStore.GetTenant(tenant);
    }

    public void SaveTenant(Tenant tenant)
    {
        _tenantStore.SaveTenant(tenant);
        _logger.Log("saving tenant " + tenant.id);
    }
}

好处包括

  • 您无需登录即可测试-真正的单元测试
  • 您甚至可以在运行时轻松地打开/关闭登录
  • 您可以用日志记录替代其他形式的日志记录,而无需更改TenantStore文件。

谢谢你的链接。该页面上的图1实际上就是我最喜欢的解决方案。横切关注点(日志记录,缓存等)和装饰器模式的列表是最通用的解决方案,尽管较大的社区希望放弃该抽象和内联日志记录,但我很高兴自己的想法完全没有错。
Aitch

2
我看不到您在任何地方分配_logger变量。您是否打算使用构造函数注入而忘了?如果是这样,您可能会收到编译器警告。
user2023861 2015年

27
不需要使用需要N + 1类的通用日志记录器对TenantStore进行DIP(当您添加LandlordStore,FooStore,BarStore等时),而是将TenantStoreLogger与TenantStore进行DIP,将FooStoreLogger与FooStore进行DIP。等...需要2N个班级。据我所知,为零收益。 当您要进行无日志记录的单元测试时,您需要重新设置N个类,而不仅仅是配置NullLogger。海事组织,这是一种非常糟糕的做法。
user949300 2015年

6
对每个需要日志记录的类进行此操作会极大地增加代码库的复杂性(除非有很少的类进行日志记录,您甚至不再将其称为交叉问题)。这最终使得代码维护简单,因为大量的接口来维持,这违背一切的单一职责原则是为创建。
jpmc26 2015年

9
不赞成投票。您已从“租户”类中删除了日志记录问题,但是现在TenantStoreLogger每次TenantStore更改时,您的情况都会有所变化。与最初的解决方案相比,您没有将关注点分开。
Laurent LA RIZZA 2015年

61

我会说您对SRP的重视程度过高。如果您的代码整洁到足以使日志记录成为SRP的唯一“违规”,那么您的表现要好于所有其他程序员的99%,因此您应该反击自己。

SRP的要点是避免将执行不同操作的代码混在一起的可怕的意大利面条代码。将日志记录与功能代码混合不会给我敲响警钟。


19
@Aitch:您的选择是将日志记录硬连接到您的类中,将句柄传递给记录器,或者根本不记录任何内容。如果您要严格限制SRP却要牺牲其他一切,那么我建议您永远不要记录任何内容。无论您需要了解什么软件在做什么,都可以使用调试器解决。SRP中的P代表“原理”,而不是“永远不能打破的自然物理定律”。
Blrfl 2015年

3
@Aitch:您应该能够将类中的日志记录追溯到某些要求,否则您将违反YAGNI。如果在表上记录日志,则您将提供一个有效的记录器句柄,就像该类需要的其他任何东西一样,最好是已经通过测试的类中的一个。是生成实际的日志条目还是将其转储到位存储桶中,是要实例化类的实例的问题。班级本身不在乎。
Blrfl 2015年

3
@Aitch回答有关单元测试的问题:Do you mock the logger?,这就是您要做的。您应该具有ILogger定义记录器功能的接口。被测代码将注入ILogger您指定的。为了进行测试,您有class TestLogger : ILogger。很棒的事情是TestLogger可以暴露出最后一个字符串或记录的错误之类的东西。测试可以验证被测代码是否正确记录了日志。例如,一个测试可以是UserSignInTimeGetsLogged(),其中该测试检查TestLogger记录的日志。
CurtisHx 2015年

5
99%似乎有点低。您可能比所有程序员中的100%强。
Paul Draper 2015年

2
+1为理智。我们需要更多此类思维:较少关注单词和抽象原理,而更多关注具有可维护的代码库
2015年

15

不,这不违反SRP。

您发送到日志的消息应该以与周围代码相同的原因进行更改。

违反SRP的行为是使用特定的库直接在代码中进行记录。如果您决定更改日志记录方式,则SRP声明它不会影响您的业务代码。

Logger您的实现代码应该可以访问某种抽象,而您的实现应该唯一说的是“将消息发送到日志”,而不必担心它是如何完成的。确定确切的记录方式(甚至打上时间戳)不是您实现的责任。

这样,您的实现还应该不知道它正在向其发送消息的记录器是否为NullLogger

那就是。

作为跨领域的关注,我不会太快地将日志记录删除。发出用于跟踪实现代码中发生的特定事件的日志属于实现代码。

跨部门关注的问题是OTOH,它是执行跟踪:日志记录在每种方法中进入和退出。AOP最适合执行此操作。


假设记录器消息是“登录用户xyz”,该消息发送到记录了时间戳等的记录器。您知道“登录”对实现意味着什么吗?它是使用Cookie或任何其他机制开始会话吗?我认为实现登录有很多不同的方法,因此从逻辑上讲,更改实现与用户登录这一事实无关。这是装饰不同组件(例如OAuthLogin,SessionLogin,BasicAuthorizationLogin)的另一个好例子作为Login装饰有相同记录器的-interface。
Aitch 2015年

这取决于消息“登录用户xyz”的含义。如果它表明登录成功,则将消息发送到日志属于登录用例。将登录信息表示为字符串(OAuth,Session,LDAP,NTLM,指纹,仓鼠轮)的特定方式属于表示凭据或登录策略的特定类。无需删除它。这种特殊的情况不是跨领域的问题。它特定于登录用例。
Laurent LA RIZZA 2015年

7

由于日志记录通常被认为是一个跨领域的问题,因此我建议使用AOP将日志记录与实现分开。

根据语言的不同,您将使用拦截器或某些AOP框架(例如Java中的AspectJ)来执行此操作。

问题是,这样做是否值得解决。请注意,这种分离将增加项目的复杂性,而带来的好处却很小。


2
我看到的大多数AOP代码都是关于记录每种方法的每个进入和退出步骤的。我只想记录一些业务逻辑部分。因此,可能只记录带注释的方法,但是AOP只能存在于脚本语言和虚拟机环境中,对吗?在例如C ++中,这是不可能的。我承认我对AOP方法不是很满意,但是也许没有更清洁的解决方案。
Aitch

1
@Aitch。“ C ++是不可能的。” :如果您用Google搜索“ aop c ++”,则会发现有关它的文章。“ ...我看到的AOP代码是关于记录每种方法的每个进入和退出步骤。我只想记录一些业务逻辑部分。” Aop允许您定义模式以查找要修改的方法。即来自命名空间“ my.busininess。*”的所有方法
k3b

1
日志记录通常不是跨领域的问题,尤其是当您希望日志包含有趣的信息(即,比异常堆栈跟踪中包含的信息更多)时。
Laurent LA RIZZA 2015年

5

听起来不错。您正在描述一个相当标准的日志装饰器。你有:

组件L(系统的日志记录组件)

这有一个责任:记录传递给它的信息。

组件A实现I

这有一个责任:提供接口I的实现(即,我完全符合SRP要求)。

这是关键部分:

组件D实现I,修饰/使用A,使用L进行日志记录

当这样说时,听起来很复杂,但要这样看:组件D做件事:将A和L组合在一起。

  • 组件D不记录;它将它委托给L
  • 组件D本身未实现I;它将它委托给A

组件D 的唯一职责是确保在使用A时通知L。A和L的实现都在别处。这完全符合SRP,并且是OCP的简洁示例和装饰器的常见用法。

一个重要的警告:当d使用您的日志记录组件L,它应该的方式,让你改变这样做怎么你的日志记录。最简单的方法是拥有由L实现的接口IL。然后:

  • 组件D使用IL进行记录;提供了L的一个实例
  • 组件D使用I提供功能;提供了A的实例
  • 组件B使用I;提供了D的一个实例

这样,没有任何东西直接依赖于其他任何东西,因此很容易将它们交换出去。这样可以轻松地适应变化,并且可以轻松地模拟系统的各个部分,从而可以进行单元测试。


我实际上只知道具有本机委派支持的C#。这就是我写信的原因D implements I。谢谢您的回答。
Aitch

1

当然,这是违反SRP的,因为您有一个横切的关注点。但是,您可以创建一个类,该类负责将日志记录与任何操作的执行结合起来。

例:

class Logger {
   ActuallLogger logger;
   public Action ComposeLog(string msg, Action action) {
      return () => {
          logger.debug(msg);
          action();
      };
   }
}

2
不赞成投票。日志记录确实是一个跨领域的问题。您的代码中的排序方法调用也是如此。这还不足以声称违反了SRP。记录应用程序中特定事件的发生不是跨领域的问题。这些消息传递给任何感兴趣的用户的方式确实是一个单独的问题,并且在实现代码中对其进行描述是对SRP的违反。
Laurent LA RIZZA 2015年

“排序方法调用”或功能组成不是一个交叉问题,而是实现细节。我创建的函数的职责是编写带有操作的日志语句。我不需要使用单词“和”来描述此功能的作用。
Paul Nikonowicz 2015年

这不是实现细节。它对代码的形状有深远的影响。
Laurent LA RIZZA 2015年

我认为我是从“此功能做什么”的角度看待SRP的,而当您从“此功能如何做”的角度看待SRP的时候。
Paul Nikonowicz 2015年
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.