Önbelleğe almayı yönetmek için bir sınıftaki SRP'yi ihlal etmekten nasıl kaçınılır?


12

Not: Kod örneği c # ile yazılmıştır, ancak bu önemli olmamalıdır. Daha uygun bir tane bulamadığım için etiket olarak c # koydum. Bu kod yapısı ile ilgilidir.

Temiz Kod okuyorum ve daha iyi bir programcı olmaya çalışıyorum.

Kendimi genellikle Tek Sorumluluk Prensibine (sınıflar ve işlevler sadece bir şey yapmalıdır), özellikle işlevlerde takip etmekte zorlanırım. Belki benim sorunum "tek bir şey" iyi tanımlanmış değil, ama yine de ...

Bir örnek: Bir veritabanında Fluffies listesi var. Bir Fluffy'nin ne olduğu umrumda değil. Bir sınıfın tüyleri düzeltmesini istiyorum. Ancak, kabartmalar bazı mantığa göre değişebilir. Bazı mantığa bağlı olarak, bu sınıf verileri önbellekten döndürür veya veritabanından en son verileri alır. Tüyleri yönettiğini söyleyebiliriz ve bu bir şeydir. Basitleştirmek için, yüklenen verilerin bir saat boyunca iyi olduğunu ve daha sonra yeniden yüklenmesi gerektiğini varsayalım.

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()bana iyi geliyor. Kullanıcı bazı kabarıklıklar ister, biz bunları sağlıyoruz. Gerekirse onları DB'den kurtaracak, ancak bu tüyleri elde etmenin bir parçası olarak düşünülebilir (elbette, bu biraz özneldir).

NeedsReload()doğru görünüyor. Fluffies'i yeniden yüklememiz gerekip gerekmediğini kontrol eder. UpdateNextLoad iyidir. Bir sonraki yeniden yükleme süresini günceller. bu kesinlikle tek bir şey.

Ancak, neyin LoadFluffies()tek bir şey olarak tanımlanamayacağını hissediyorum . Veritabanından veri alıyor ve bir sonraki yeniden yüklemeyi planlıyor. Bir sonraki yeniden yükleme için sürenin hesaplanmasının veri almanın bir parçası olduğunu iddia etmek zor. Bununla birlikte, bunu yapmak için daha iyi bir yol bulamıyorum (işlevi yeniden adlandırmak LoadFluffiesAndScheduleNextLoaddaha iyi olabilir, ancak sorunu daha açık hale getirir).

Bu sınıfı gerçekten SRP'ye göre yazmak için zarif bir çözüm var mı? Çok bilgiç miyim?

Ya da belki de sınıfım gerçekten tek bir şey yapmıyor?


3
"C # ile yazılmış, ama önemli değil", "Bu kod yapısı ile ilgili", "Bir örnek: ... Bir Fluffy'nin ne olduğu umrumda değil", "Basitleştirmek için diyelim ki ...", bu kod incelemesi için bir istek değil, genel bir programlama prensibi hakkında bir soru.
200_success

@ 200_success teşekkür ederim ve üzgünüm, bunun CR için yeterli olacağını düşündüm
kuzgun


2
Gelecekte benzer sorular için kabarık yerine "widget" ile daha iyi olurdu, çünkü bir widget örneklerde belirli bir stand olarak anlaşılmaktadır.
whatsisname

1
Ben sadece örnek kod biliyorum, ama DateTime.UtcNowböylece yaz saati değişiklikleri, hatta geçerli saat diliminde bir değişiklik önlemek için kullanın .
Mark Hurd

Yanıtlar:


23

Eğer bu sınıf göründüğü kadar önemsiz olsaydı, o zaman SRP'yi ihlal etme konusunda endişelenmenize gerek olmazdı. Peki ya 3 satırlı bir işlevin bir işi yapan 2 satır ve başka bir şey yapan başka bir 1 satırı varsa? Evet, bu önemsiz işlev SRP'yi ihlal ediyor, peki ne olacak? Kimin umrunda? SRP'nin ihlali, işler daha karmaşık hale geldiğinde sorun olmaya başlar.

Bu özel durumdaki probleminiz büyük olasılıkla sınıfın bize gösterdiğiniz birkaç satırdan daha karmaşık olmasından kaynaklanmaktadır.

Özellikle, sorun büyük olasılıkla bu sınıfın sadece önbelleği yönetmesi değil, aynı zamanda GetFluffiesFromDb()yöntemin uygulanmasını da içermesidir . Dolayısıyla, SRP'nin ihlali sınıftadır, yayınladığınız kodda gösterilen birkaç önemsiz yöntemde değil.

İşte, bu genel kategoriye giren her türlü vakanın Dekoratör Kalıbı yardımıyla nasıl ele alınacağına dair bir öneri .

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

ve aşağıdaki gibi kullanılır:

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

CachingFluffiesProvider.GetFluffies()Bu, önemsiz şeyler olduğu için zaman kontrolünü ve güncellemeyi yapan kodu içermekten nasıl korkmadığını unutmayın . Bu mekanizmanın yaptığı, SRP'nin önemsiz olduğu küçük bireysel yöntemler düzeyinde değil, sistem tasarım düzeyinde ele alınması ve ele alınmasıdır.


1
Fluffies, önbellekleme ve veri tabanı erişiminin aslında üç sorumluluk olduğunu kabul eden +1. FluffiesProvider arayüzünü ve dekoratörleri genel hale getirmeyi bile deneyebilirsiniz (IProvider <Fluffy>, ...) ama bu YAGNI olabilir.
Roman Reiner

Dürüst olmak gerekirse, yalnızca bir tür önbellek varsa ve her zaman veritabanından nesneleri çekerse, bu IMHO'yu çok fazla tasarlar (örnekte gördüğümüz gibi "gerçek" sınıf daha karmaşık olsa bile). Sadece soyutlama uğruna soyutlama, kodu daha temiz veya daha sürdürülebilir hale getirmez.
Doc Brown

@DocBrown sorunu, sorunun bağlamının olmamasıdır. Bu cevabı beğendim, daha büyük uygulamalarda tekrar tekrar kullandığım bir yolu gösteriyor ve testlerin yazılması kolay olduğu için, cevabımı da seviyorum çünkü bu sadece küçük bir değişiklik ve fazla tasarım yapmadan net bir şey veriyor şu anda, bağlam olmadan hemen hemen tüm cevaplar burada iyi:]
stijn

1
FWIW, soruyu sorduğumda aklımdaki sınıf FluffiesManager'dan daha karmaşık, ama aşırı değil. Belki 200 hat. Bu soruyu sormadım çünkü tasarımımda herhangi bir sorun buldum (henüz?), Sırf SRP'ye tam olarak uymanın bir yolunu bulamadım ve bu daha karmaşık durumlarda bir sorun olabilir. Yani, bağlam eksikliği bir şekilde amaçlanmıştır. Bence bu cevap harika.
kuzgun

2
@stijn: Bence cevabınız çok az. Gereksiz soyutlama eklemek yerine, sorumlulukları farklı bir şekilde kesip adlandırıyorsunuz, bu kadar basit bir soruna üç miras katmanını kazmadan önce her zaman ilk seçenek olmalı.
Doc Brown

6

Sınıfınızın kendisi bana iyi geliyor, ama haklısınız, LoadFluffies()tam olarak adın reklamını yapmıyor. Basit bir çözüm, adı değiştirmek ve açık yeniden yüklemeyi GetFluffies'ten uygun bir açıklamaya sahip bir işleve taşımak olacaktır. Gibi bir şey

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

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

bana temiz görünüyor (çünkü Patrick'in dediği gibi: diğer küçük SRP'ye itaatkâr işlevlerden oluşuyor) ve özellikle de bazen de önemli olan açık.


1
Bunun basitliğini seviyorum.
kuzgun

6

Sınıfınızın bir şey yaptığını düşünüyorum; zaman aşımı olan bir veri önbelleğidir. LoadFluffies, birden fazla yerden çağırmadıkça işe yaramaz bir soyutlama gibi görünüyor. LoadFluffies iki satır almak ve GetFluffies NeedsReload koşullu koymak daha iyi olacağını düşünüyorum. Bu, GetFluffies'in uygulanmasını çok daha açık hale getirecektir ve tek bir hedefi gerçekleştirmek için tek sorumluluk altprogramları oluşturduğunuzdan, db'den verilerin önbelleğe alınmış bir şekilde elde edilmesinden dolayı temiz koddur. Aşağıda güncellenen fluffies yöntemi verilmiştir.

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

    return m_Cache;
}

Bu oldukça iyi bir ilk yanıt olsa da, lütfen "sonuç" kodunun genellikle iyi bir ek olduğunu unutmayın.
Monica'nın Davası

4

İçgüdülerin doğru. Sınıfınız, küçük olsa da, çok fazla şey yapıyor. Zamanlı yenileme önbellek mantığını tamamen genel bir sınıfa ayırmalısınız. Ardından, Fluffies'ı yönetmek için bu sınıfın belirli bir örneğini oluşturun, böyle bir şey (derlenmemiş, çalışma kodu okuyucu için bir alıştırma olarak bırakılır):

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

Ek bir avantaj, TimedRefreshCache'i test etmenin artık çok kolay olmasıdır.


1
Yenileme mantığı örnekten daha karmaşık hale gelirse, onu ayrı bir sınıfa yeniden yansıtmanın iyi bir fikir olabileceğini kabul ediyorum. Ancak, örnekteki sınıfın olduğu gibi çok fazla şey yaptığına katılmıyorum.
Doc Brown

@kevin, TDD konusunda deneyimli değilim. TimedRefreshCache'i nasıl test edeceğinizi açıklayabilir misiniz? Bunu "çok kolay" olarak görmüyorum, ama bu benim uzmanlık eksikliğim olabilir.
kuzgun

1
Kişisel olarak karmaşıklığından dolayı cevabınızı sevmiyorum. Çok genel ve çok soyuttur ve daha karmaşık durumlarda en iyisi olabilir. Ancak bu basit durumda 'basitçe çoktur'. Lütfen stijn'in cevabına bir göz atın. Ne güzel, kısa ve okunabilir bir cevap. Herkes hemen anlayacak. Ne düşünüyorsun?
Dieter Meemken

1
@raven TimedRefreshCache'i kısa bir aralık (100 ms gibi) ve çok basit bir yapımcı (DateTime.Now gibi) kullanarak test edebilirsiniz. Her 100 ms'de önbellek yeni bir değer üretecek, aralarında önceki değeri döndürecektir.
kevin cline

1
@DocBrown: Sorun, yazıldığı gibi test edilemez olması. Zamanlama mantığı (test edilebilir), daha sonra alay edilecek olan veritabanı mantığı ile birleştirilir. Veritabanı çağrısı alay etmek için bir dikiş oluşturulduktan sonra, genel çözüm yolunun% 95'i vardır. Bu küçük sınıfları inşa etmenin, genellikle beklenenden daha fazla yeniden kullanılmaları nedeniyle işe yaradığını gördüm.
kevin cline

1

Sınıfınız iyi, SRP bir işlev değil bir sınıf hakkında, tüm sınıf "Veri Kaynağı" "Fluffies" sağlamaktan sorumludur, bu yüzden dahili uygulamada özgürsünüz.

Cahing mekanizmasını genişletmek istiyorsanız, veri kaynağını izlemek için sorumlu sınıf oluşturabilirsiniz

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

Bob Amca'ya göre: FONKSİYONLAR BİR ŞEY YAPMALIDIR. Onlar iyi yapmalılar. SADECE YAPMALIDIR. Temizlik Kodu s.35.
kuzgun
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.