如何避免在管理缓存的类中违反SRP?


12

注意:代码示例是用c#编写的,但这无关紧要。我将c#用作标签,因为找不到更合适的标签。这是关于代码结构的。

我正在阅读Clean Code,并试图成为一个更好的程序员。

我经常发现自己难以遵循“单一责任原则”(类和功能只能做一件事),尤其是在功能方面。也许我的问题是“一件事”的定义不明确,但仍然...

一个例子:我在数据库中有一个Fluffies列表。我们不在乎什么是蓬松。我想上课恢复蓬松。但是,蓬松可以根据某些逻辑进行更改。根据某些逻辑,此类将从缓存中返回数据或从数据库中获取最新数据。我们可以说它管理蓬松,这是一回事。为了简单起见,假设加载的数据可以使用一个小时,然后必须重新加载。

class FluffiesManager
{
    private Fluffies m_Cache;
    private DateTime m_NextReload = DateTime.MinValue;
    // ...
    public Fluffies GetFluffies()
    {
        if (NeedsReload())
            LoadFluffies();

        return m_Cache;
    }

    private NeedsReload()
    {
        return (m_NextReload < DateTime.Now);
    }

    private void LoadFluffies()
    {
        GetFluffiesFromDb();
        UpdateNextLoad();
    }

    private void UpdateNextLoad()
    {
        m_NextReload = DatTime.Now + TimeSpan.FromHours(1);
    }
    // ...
}

GetFluffies()对我来说似乎还可以。用户要求一些蓬松,我们提供它们。如果需要,可以从数据库中恢复它们,但这可以视为获取蓬松度的一部分(当然,这有点主观)。

NeedsReload()似乎也正确。检查是否需要重新加载蓬松物。UpdateNextLoad很好。更新下一次重新加载的时间。那绝对是一件事。

但是,我觉得LoadFluffies()不能将任何事情描述为一件事情。它从数据库中获取数据,并计划下一次重新加载。很难说计算下一次重新加载的时间是获取数据的一部分。但是,我找不到更好的方法(将函数重命名LoadFluffiesAndScheduleNextLoad为更好的方法,但这只会使问题更加明显)。

是否有一个优雅的解决方案可以根据SRP真正编写此类?我太学究了吗?

或者,也许我的课不是只做一件事吗?


3
基于“用C#编写,但这无关紧要”,“这与代码结构有关”,“示例:……我们不在乎什么是Fluffy”,“简单起见,让我们说……”,这不是要求进行代码审查,而是有关通用编程原理的问题。
200_success 2015年

@ 200_success谢谢,很抱歉,我认为这对于CR来说已经足够了
乌鸦(


2
将来您最好还是使用“小部件”来解决将来的类似问题,因为小部件在示例中被理解为非特定的代表。
whatsisname 2015年

1
我知道这只是示例代码,但DateTime.UtcNow可以避免使用夏令时转换,甚至避免当前时区的更改。
马克·赫德

Answers:


23

如果此类确实像看起来那样琐碎,那么就不必担心违反SRP。那么,如果3行函数有2行执行一项操作,而另外1行执行另一项操作又该怎么办?是的,这个琐碎的功能违反了SRP,那又如何呢?谁在乎?当事情变得更加复杂时,违反SRP便开始成为问题。

在这种情况下,您的问题很可能是由于该类比您向我们展示的几行要复杂得多而引起的。

具体来说,问题很可能在于该类不仅管理缓存,而且还可能包含该GetFluffiesFromDb()方法的实现。因此,违反SRP是在类中,而不是在发布的代码中显示的一些琐碎方法中。

因此,这里有一个关于如何在Decorator Pattern的帮助下如何处理属于该一般类别的各种情况的建议。

/// Provides Fluffies.
interface FluffiesProvider
{
    Fluffies GetFluffies();
}

/// Implements FluffiesProvider using a database.
class DatabaseFluffiesProvider : FluffiesProvider
{
    public override Fluffies GetFluffies()
    {
        ... load fluffies from DB ...
        (the entire implementation of "GetFluffiesFromDb()" goes here.)
    }
}

/// Decorates FluffiesProvider to add caching.
class CachingFluffiesProvider : FluffiesProvider
{
    private FluffiesProvider decoree;
    private DateTime m_NextReload = DateTime.MinValue;
    private Fluffies m_Cache;

    public CachingFluffiesProvider( FluffiesProvider decoree )
    {
        Assert( decoree != null );
        this.decoree = decoree;
    }

    public override Fluffies GetFluffies()
    {
        if( DateTime.Now >= m_NextReload ) 
        {
             m_Cache = decoree.GetFluffies();
             m_NextReload = DatTime.Now + TimeSpan.FromHours(1);
        }
        return m_Cache;
    }
}

它的用法如下:

FluffiesProvider provider = new DatabaseFluffiesProvider();
provider = new CachingFluffiesProvider( provider );
...go ahead and use provider...

注意如何CachingFluffiesProvider.GetFluffies()不害怕包含执行时间检查和更新的代码,因为这很琐碎。该机制的作用是在重要的系统设计级别而不是在单个的无关紧要的级别上解决和处理SRP。


1
+1表示绒毛,缓存和数据库访问实际上是三项职责。您甚至可以尝试使FluffiesProvider接口和装饰器通用(IProvider <Fluffy>,...),但这可能是YAGNI。
罗曼·赖纳

老实说,如果只有一种类型的缓存并且总是从数据库中提取对象,那么恕我直言,这是过度设计的(即使“真实”类可能在示例中更为复杂)。仅出于抽象的目的,抽象不会使代码更清洁或更可维护。
布朗

@DocBrown问题是该问题缺乏上下文。我喜欢这个答案,因为它显示了我在大型应用程序中一次又一次使用的方式,并且因为编写测试很容易,我也喜欢我的答案,因为它只是一个很小的变化,并且产生了一些清晰的东西而没有任何过度设计,因此目前站起来,没有上下文,这里的所有答案几乎都不错:]
stijn 2015年

1
FWIW,当我问这个问题时,我想到的课程要比FluffiesManager更复杂,但并不过分。大概有200行。我之所以没有提出这个问题,是因为我发现我的设计有任何问题(还好吗?),只是因为我找不到严格遵守SRP的方法,在更复杂的情况下这可能是个问题。因此,缺少上下文是有一定意图的。我认为这个答案很棒。
乌鸦2015年

2
@stijn:好吧,我认为您的答案被严重引用了。您不必添加不必要的抽象,而只是以不同的方式剪切和命名职责,在将三层继承堆积到这样一个简单的问题之前,这应该始终是首选。
布朗

6

在我看来,您的课程本身还不错,但是您的意思是正确的,LoadFluffies()它与名称所宣传的不完全相同。一种简单的解决方案是更改名称,并将显式重载从GetFluffies中移出,并带有适当说明的函数。就像是

public Fluffies GetFluffies()
{
  MakeSureTheFluffyCacheIsUpToDate();
  return m_Cache;
}

private void MakeSureTheFluffyCacheIsUpToDate()
{
  if( !NeedsReload )
    return;
  GetFluffiesFromDb();
  SetNextReloadTime();
}

对我来说看起来很干净(还因为就像Patrick所说的那样:它是由其他一些SRP服从的小功能组成的),而且尤其是清楚哪些有时同样重要。


1
我喜欢这种简单性。
乌鸦2015年

6

我相信您的课堂正在做一件事;这是带有超时的数据缓存。除非您从多个地方调用,否则LoadFluffies似乎是一个无用的抽象。我认为最好从LoadFluffies中提取这两行,并将它们放在GetFluffies中的NeedsReload中。这将使GetFluffies的实现更加明显,并且仍然是干净的代码,因为您正在组成单个职责子例程以实现单个目标,即从db缓存的数据检索。下面是更新的get fluffies方法。

public Fluffies GetFluffies()
{
    if (NeedsReload()) {
        GetFluffiesFromDb();
        UpdateNextLoad();
    }

    return m_Cache;
}

虽然这是一个很好的第一答案,但是请记住,“结果”代码通常是一个很好的补充。
基金莫妮卡的诉讼

4

你的直觉是正确的。您的课程虽然规模很小,但做得太多。您应该将定时刷新缓存逻辑分为一个完全通用的类。然后创建该类的特定实例来管理Fluffies,如下所示(未编译,工作代码留给读者练习):

public class TimedRefreshCache<T> {
    T m_Value;
    DateTime m_NextLoadTime;
    Func<T> m_producer();
    public CacheManager(Func<T> T producer, Interval timeBetweenLoads) {
          m_nextLoadTime = INFINITE_PAST;
          m_producer = producer;
    }
    public T Value {
        get {
            if (m_NextLoadTime < DateTime.Now) {
                m_Value = m_Producer();
                m_NextLoadTime = ...;
            }
            return m_Value;
        }
    }
}

public class FluffyCache {
    private TimedRefreshCache m_Cache 
        = new TimedRefreshCache<Fluffy>(GetFluffiesFromDb, interval);
    private Fluffy GetFluffiesFromDb() { ... }
    public Fluffy Value { get { return m_Cache.Value; } }
}

另一个优点是,现在很容易测试TimedRefreshCache。


1
我同意,如果刷新逻辑比示例中的复杂,那么将其重构为一个单独的类可能是一个好主意。但我不同意示例中的类确实做得太多。
布朗

@kevin,我没有TDD经验。您能否详细说明如何测试TimedRefreshCache?我认为这不是“很容易”,但这可能是我缺乏专​​业知识。
乌鸦

1
我个人不喜欢您的答案,因为它很复杂。它非常通用,非常抽象,在更复杂的情况下可能最好。但是在这种简单情况下,它“简直就是太多”。请看看stijn的答案。这是一个不错的,简短易懂的答案。每个人都会立即理解它。你怎么看?
Dieter Meemken

1
@raven您可以使用较短的间隔(例如100ms)和非常简单的生成器(例如DateTime.Now)来测试TimedRefreshCache。缓存每隔100毫秒将产生一个新值,在此之间它将返回以前的值。
凯文·克莱恩

1
@DocBrown:问题是,按照书面规定,它是无法测试的。时序逻辑(可测试的)与数据库逻辑耦合,然后对其进行了模拟。一旦创建了一个接缝以模拟数据库调用,就可以达到通用解决方案的95%。我发现建立这些小类通常会有所回报,因为它们最终被重用的程度超出了预期。
凯文·克莱恩

1

您的课程很好,SRP是关于课程而不是功能,整个课程负责提供“数据源”中的“绒毛”,因此您可以自由地进行内部实现。

如果您希望扩展cahing机制,则可以创建负责监视数据源的类

public class ModelWatcher
{

    private static Dictionary<Type, DateTime> LastUpdate;

    public static bool IsUpToDate(Type entityType, DateTime lastRead) {
        if (LastUpdate.ContainsKey(entityType)) {
            return lastRead >= LastUpdate[entityType];
        }
        return true;
    }

    //call this method whenever insert/update changed to any entity
    private void OnDataSourceChanged(Type changedEntityType) {
        //update Date & Time
        LastUpdate[changedEntityType] = DateTime.Now;
    }
}
public class FluffyManager
{
    private DateTime LastRead = DateTime.MinValue;

    private List<Fluffy> list;



    public List<Fluffy> GetFluffies() {

        //if first read or not uptodated
        if (list==null || !ModelWatcher.IsUpToDate(typeof(Fluffy),LastRead)) {
            list = ReadFluffies();
        }
        return list;
    }
    private List<Fluffy> ReadFluffies() { 
    //read code
    }
}

据鲍伯叔叔说:功能应该做一件事。他们应该做得很好。他们只能这样做。清洁代码第35页。
乌鸦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.