캐싱을 관리하기 위해 클래스에서 SRP를 위반하지 않는 방법은 무엇입니까?


12

참고 : 코드 샘플은 C #으로 작성되었지만 중요하지 않습니다. 더 적합한 태그를 찾을 수 없기 때문에 C #을 태그로 넣었습니다. 이것은 코드 구조에 관한 것입니다.

Clean Code를 읽고 더 나은 프로그래머가 되려고 노력하고 있습니다.

나는 종종 특히 기능에서 단일 책임 원칙 (클래스와 함수가 한 가지만 수행해야 함)을 따르는 데 어려움을 겪고 있습니다. 어쩌면 내 문제는 "한 가지"가 잘 정의되어 있지 않지만 여전히 ...

예 : 데이터베이스에 Fluffies 목록이 있습니다. 우리는 플러피가 무엇인지 상관하지 않습니다. 나는 수업이 fluffies를 복구하기를 원합니다. 그러나 일부 논리에 따라 플러 프가 변경 될 수 있습니다. 일부 논리에 따라이 클래스는 캐시에서 데이터를 반환하거나 데이터베이스에서 최신을 가져옵니다. 우리는 그것이 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()나에게 괜찮아 보인다. 사용자가 몇 개의 플러 프를 요청하면 제공합니다. 필요한 경우 DB에서 복구하려고하지만 플 루프를 얻는 것의 일부로 간주 될 수 있습니다 (물론 다소 주관적입니다).

NeedsReload()옳은 것 같습니다. 플러피를 다시로드해야하는지 확인합니다. UpdateNextLoad는 괜찮습니다. 다음 재 장전 시간을 업데이트합니다. 그것은 확실히 하나의 것입니다.

그러나 나는 LoadFluffies()한 가지로 설명 할 수없는 것을 느낍니다 . 데이터베이스에서 데이터를 가져오고 다음 재로드를 예약합니다. 다음 재로드 시간을 계산하는 것이 데이터를 얻는 것의 일부라고 주장하기는 어렵습니다. 그러나 더 나은 방법을 찾을 수는 없습니다 (함수의 이름을 바꾸면 LoadFluffiesAndScheduleNextLoad문제가 더 분명해집니다).

SRP에 따라 실제로이 클래스를 작성하는 우아한 솔루션이 있습니까? 내가 너무 장난이 되나요?

아니면 내 수업이 실제로 한 가지 일을하지 않습니까?


3
"C #으로 작성되었지만 중요하지 않습니다.", "이것은 코드 구조에 관한 것", "예 :… 이것은 코드 검토 요청이 아니라 일반적인 프로그래밍 원칙에 대한 질문입니다.
200_suc.

@ 200_success 감사합니다. 죄송합니다. CR에 적합하다고 생각했습니다.
raven


2
위젯은 예를 들어 비특이적 인 것으로 이해되므로 앞으로 비슷한 질문에 푹신한 대신 "위젯"을 사용하는 것이 좋습니다.
whatsisname

1
나는 그것이 예제 코드 일 뿐이라는 것을 알고 있지만 DateTime.UtcNow일광 절약 전환 또는 현재 시간대의 변경을 피하기 위해 사용 합니다.
Mark Hurd

답변:


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
fluffies, 캐싱 및 데이터베이스 액세스는 실제로 세 가지 책임임을 인식하여 +1합니다. FluffiesProvider 인터페이스와 데코레이터를 일반 (IProvider <Fluffy>, ...)으로 만들 수도 있지만 YAGNI 일 수 있습니다.
Roman Reiner

솔직히, 캐시 유형이 하나만 있고 항상 데이터베이스에서 오브젝트를 가져 오는 경우, 이는 "실제"클래스가 예제에서 볼 수 있듯이 더 복잡하더라도 IMHO가 과도하게 설계되었습니다. 추상화를위한 추상화는 코드를 더 깨끗하게 유지하거나 유지하기가 쉽지 않습니다.
Doc Brown

@DocBrown 문제는 질문에 대한 맥락이 부족하다는 것입니다. 나는이 답변이 더 큰 응용 프로그램에서 시간과 시간을 다시 사용한 방법을 보여 주며, 테스트를 작성하기 쉽기 때문에 작은 변화 일뿐 아니라 과도하게 설계하지 않고 명확한 것을 산출하기 때문에 내 대답을 좋아합니다. 현재 상황에
맞지

1
FWIW, 질문을 할 때 염두에 둔 수업은 FluffiesManager보다 더 복잡하지만 지나치게 그렇지는 않습니다. 약 200 줄일 것입니다. SRP를 엄격하게 준수하는 방법을 찾을 수 없기 때문에 디자인에 문제가 있음을 발견했기 때문에이 질문을하지 않았습니다. 더 복잡한 경우에는 문제가 될 수 있습니다. 따라서 맥락의 부족은 다소 의도 된 것입니다. 이 답변이 훌륭하다고 생각합니다.
까마귀

2
@ stijn : 글쎄, 나는 당신의 대답이 크게 찬성한다고 생각합니다. 불필요한 추상화를 추가하는 대신 책임을 다르게 잘라 이름을 지정하면됩니다. 이러한 간단한 문제에 상속의 세 계층을 쌓기 전에 항상 첫 번째 선택이되어야합니다.
Doc Brown

6

수업 자체는 괜찮아 보이지만, LoadFluffies()그 이름이 광고하는 것과 정확히 일치하지는 않습니다. 간단한 해결책 중 하나는 이름을 변경하고 GetFluffies에서 명시 적 재로드를 적절한 설명이있는 함수로 옮기는 것입니다. 같은 것

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

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

(Patrick이 말한 것처럼 다른 작은 SRP 순종 함수로 구성되어 있기 때문에) 나에게도 깨끗해 보이며, 특히 중요한 부분이 분명합니다.


1
나는 이것의 단순함을 좋아한다.
까마귀

6

수업이 한 가지 일을하고 있다고 생각합니다. 시간이 초과 된 데이터 캐시입니다. LoadFluffies는 여러 곳에서 호출하지 않는 한 쓸모없는 추상화처럼 보입니다. LoadFluffies에서 두 줄을 가져 와서 GetFluffies의 NeedsReload 조건에 넣는 것이 좋습니다. 이렇게하면 GetFluffies의 구현이 훨씬 더 명확 해지고 여전히 코드가 깨끗해집니다. 단일 책임 서브 루틴을 작성하여 단일 목표, 데이터베이스에서 캐시 된 데이터 검색을 달성하기 때문입니다. 업데이트 된 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
새로 고침 논리가 예제보다 복잡해지면 별도의 클래스로 리팩토링하는 것이 좋습니다. 그러나 나는 예제의 수업이 그 자체로 너무 많은 일에 동의하지 않습니다.
Doc Brown

@ kevin, 나는 TDD에 경험이 없습니다. TimedRefreshCache를 테스트하는 방법에 대해 자세히 설명해 주시겠습니까? 나는 그것이 "매우 쉬운"것으로 보이지는 않지만, 나의 전문 지식이 부족할 수 있습니다.
까마귀

1
나는 복잡하기 때문에 개인적으로 당신의 대답을 좋아하지 않습니다. 매우 일반적이고 매우 추상적이며보다 복잡한 상황에서 가장 좋습니다. 그러나이 간단한 경우에는 '많은 것'입니다. stijn의 답변을 살펴보십시오. 멋지고 짧고 읽기 쉬운 답변입니다. 모두가 즉시 이해합니다. 어떻게 생각해?
Dieter Meemken

1
@raven 100ms와 같은 짧은 간격과 DateTime.Now와 같은 매우 간단한 제작자를 사용하여 TimedRefreshCache를 테스트 할 수 있습니다. 100ms마다 캐시는 새로운 값을 생성하고 그 사이에서 이전 값을 반환합니다.
케빈 클라인

1
@ DocBrown : 문제는 작성된대로 테스트 할 수 없다는 것입니다. 타이밍 로직 (테스트 가능)은 데이터베이스 로직과 결합되며,이 로직은 대부분 조롱됩니다. 데이터베이스 호출을 조롱하기 위해 이음새가 생성되면 일반 솔루션으로가는 방법의 95 %가됩니다. 나는이 작은 수업을 만드는 것이 보통 예상보다 더 많이 재사용되기 때문에 돈을 지불한다는 것을 발견했다.
케빈 클라인

1

클래스는 괜찮고 SRP는 함수가 아닌 클래스에 관한 것입니다. 전체 클래스는 "데이터 소스"에서 "Fluffies"를 제공해야하므로 내부 구현이 자유 롭습니다.

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
    }
}

밥 아저씨에 따르면 : 기능은 한 가지를해야합니다. 그들은 잘해야합니다. 그들은 그것을해야합니다. 클린 코드 p.35.
까마귀
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.