Як уникнути порушення SRP в класі для кешування кешування?


12

Примітка: Зразок коду написаний c #, але це не має значення. Я поставив c # як тег, тому що не можу знайти більш підходящого. Йдеться про структуру коду.

Я читаю «Чистий код» і намагаюся стати кращим програмістом.

Мені часто буває важко дотримуватися принципу єдиної відповідальності (класи та функції повинні виконувати лише одне), особливо у функціях. Можливо, моя проблема полягає в тому, що "одна річ" недостатньо визначена, але все ж ...

Приклад: у мене є список 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_успіх

@ 200_success дякую, і вибачте, я вважав, що це буде достатньо для CR
ворон


2
Надалі вам краще буде використовувати "віджет" замість пухнастих для майбутніх подібних питань, оскільки віджет вважається не конкретною позицією для прикладів.
whatsisname

1
Я знаю, що це лише приклад коду, але використовуйте, DateTime.UtcNowщоб уникнути зміни переходу на літній час або навіть зміни поточного часового поясу.
Марк Херд

Відповіді:


23

Якби цей клас справді був настільки тривіальним, як здається, тоді не потрібно було б турбуватися про порушення СРП. Що робити, якщо у 3-рядковій функції 2 рядки роблять одне, а в іншому 1 рядок - інша справа? Так, ця тривіальна функція порушує СРП, і що? Кого хвилює? Порушення СРП починає ставати проблемою, коли справи ускладнюються.

Ваша проблема в цьому конкретному випадку, ймовірно, випливає з того, що клас складніший, ніж кілька рядків, які ви нам показали.

Зокрема, проблема, ймовірно, полягає в тому, що цей клас не тільки управляє кешем, але й, ймовірно, містить реалізацію GetFluffiesFromDb()методу. Отже, порушення SRP полягає в класі, а не в декількох тривіальних методах, які показані в опублікованому вами коді.

Отже, ось вам пропозиція про те, як вирішувати всі види справ, які належать до цієї загальної категорії, за допомогою шаблону декораторів .

/// 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()не боїться містити код, який робить час перевірки та оновлення, адже це дрібниці. Цей механізм робить - вирішувати та керувати СРП на рівні проектування системи, де це важливо, а не на рівні крихітних окремих методів, де це все одно не має значення.


1
+1 за визнання, що пухирі, кешування та доступ до бази даних - це фактично три обов'язки. Можна навіть спробувати зробити інтерфейс FluffiesProvider та декоратори загальним (IProvider <Fluffy>, ...), але це може бути YAGNI.
Роман Райнер

Чесно кажучи, якщо є лише один тип кешу і він завжди витягує об'єкти з бази даних, це IMHO сильно перероблений (навіть якщо "реальний" клас може бути складнішим, як ми бачимо в прикладі). Абстракція лише заради абстракції не робить код чистішим або більш ретельним.
Док Браун

Проблема @DocBrown - відсутність контексту для питання. Мені подобається, що ця відповідь відповідає тому, що вона показує спосіб, який я знову і знову використовував у більших програмах, і тому що легко писати тести проти, мені також подобається моя відповідь, оскільки це лише невелика зміна і дає щось чітке без будь-якого переоформлення, на даний момент стоїть, без контексту майже всі відповіді тут хороші:]
stijn

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();
}

для мене виглядає чистою (ще й тому, що, як каже Патрік: він складається з інших крихітних функцій, слухняних SRP), а особливо також зрозумілого, що іноді так само важливо.


1
Мені подобається простота в цьому.
ворон

6

Я вважаю, що ваш клас займається однією справою; це кеш даних з тайм-аутом. LoadFluffies видається марною абстракцією, якщо ви не називаєте її з кількох місць. Я думаю, що було б краще взяти два рядки з LoadFluffies і помістити їх в умовно NeedsReload в GetFluffies. Це зробило б реалізацію GetFluffies набагато більш очевидною і досі чистим кодом, оскільки ви складаєте підпрограми єдиної відповідальності для досягнення єдиної мети, кешованого пошуку даних з db. Нижче наведено оновлений метод отримання пуху.

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. Яка приємна, коротка та читана відповідь. Усі зрозуміють це негайно. Що ти думаєш?
Дітер Меемкен

1
@raven Ви можете протестувати TimedRefreshCache, скориставшись коротким інтервалом (наприклад, 100 мс) та дуже простим виробником (наприклад, DateTime.Now). Кожні 100 мс кеш створюватиме нове значення, між ними повертається попереднє значення.
кевін клайн

1
@DocBrown: Проблема полягає в тому, що, як написано, це невідчутно. Логіка синхронізації (тестувана) поєднується з логікою бази даних, яка значною мірою потім знущається. Після створення шва для знущання над викликом бази даних, ви перебуваєте на 95% шляху до загального рішення. Я виявив, що побудова цих маленьких класів зазвичай окупається, оскільки вони в кінцевому підсумку використовуються більше, ніж очікувалося.
Кевін Клайн

1

Ваш клас добре, SRP - це про клас, а не про функцію, весь клас відповідає за надання "Fluffies" з "Джерела даних", щоб ви були вільні у внутрішній реалізації.

Якщо ви хочете розширити механізм кешування, ви можете створити клас, відповідальний для перегляду джерела даних

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.
ворон
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.