Bagaimana menghindari melanggar SRP di kelas untuk mengelola caching?

12

Catatan: Sampel kode ditulis dalam c #, tetapi itu tidak masalah. Saya telah menempatkan c # sebagai tag karena saya tidak dapat menemukan yang lebih tepat. Ini tentang struktur kode.

Saya membaca Kode Bersih dan berusaha menjadi programmer yang lebih baik.

Saya sering menemukan diri saya berjuang untuk mengikuti Prinsip Tanggung Jawab Tunggal (kelas dan fungsi harus melakukan hanya satu hal), khususnya dalam fungsi. Mungkin masalah saya adalah "satu hal" tidak didefinisikan dengan baik, tetapi masih ...

Contoh: Saya punya daftar Fluffies di database. Kami tidak peduli apa itu Fluffy. Saya ingin kelas memulihkan bulu. Namun, bulu bisa berubah sesuai dengan beberapa logika. Bergantung pada beberapa logika, kelas ini akan mengembalikan data dari cache atau mendapatkan yang terbaru dari database. Kita dapat mengatakan bahwa ia mengelola bulu, dan itu adalah satu hal. Untuk membuatnya sederhana, katakanlah data yang dimuat baik untuk satu jam, dan kemudian harus dimuat ulang.

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()sepertinya tidak masalah bagi saya. Pengguna meminta beberapa fluffies, kami menyediakannya. Akan memulihkan mereka dari DB jika diperlukan, tetapi itu bisa dianggap sebagai bagian dari mendapatkan fluffies (tentu saja, itu agak subyektif).

NeedsReload()sepertinya benar juga. Memeriksa apakah kita perlu memuat ulang fluffies. UpdateNextLoad baik-baik saja. Memperbarui waktu untuk memuat ulang berikutnya. itu pasti satu hal.

Namun, saya merasa apa LoadFluffies()yang tidak bisa digambarkan sebagai satu hal. Itu mendapatkan data dari Database, dan menjadwalkan memuat ulang berikutnya. Sulit untuk membantah bahwa menghitung waktu untuk memuat ulang berikutnya adalah bagian dari mendapatkan data. Namun, saya tidak dapat menemukan cara yang lebih baik untuk melakukannya (mengubah nama fungsi LoadFluffiesAndScheduleNextLoadmenjadi lebih baik, tetapi itu hanya membuat masalah lebih jelas).

Apakah ada solusi elegan untuk benar-benar menulis kelas ini sesuai dengan SRP? Apakah saya terlalu berlebihan?

Atau mungkin kelas saya tidak hanya melakukan satu hal?

gagak
sumber
3
Berdasarkan "ditulis dalam C #, tapi itu tidak masalah", "Ini tentang struktur kode", "Contoh: ... Kami tidak peduli apa Fluffy itu", "Untuk membuatnya sederhana, katakanlah ...", ini bukan permintaan untuk tinjauan kode, tetapi pertanyaan tentang prinsip pemrograman umum.
200_sukses
@ 200_success terima kasih, dan maaf, saya pikir ini akan cukup untuk CR
gagak
1
Sangat lembut!
Mathieu Guindon
2
Di masa depan Anda akan lebih baik dengan "widget" daripada mengembang untuk pertanyaan serupa di masa depan, karena widget dipahami sebagai contoh yang tidak khusus.
whatsisname
1
Saya tahu ini hanya kode contoh, tetapi gunakan DateTime.UtcNowagar Anda menghindari pergantian tabungan siang hari, atau bahkan perubahan zona waktu saat ini.
Mark Hurd

Jawaban:

23

Jika kelas ini benar-benar sepele seperti kelihatannya, maka tidak perlu khawatir melanggar SRP. Jadi bagaimana jika fungsi 3-baris memiliki 2 baris melakukan satu hal, dan 1 baris lainnya melakukan hal lain? Ya, fungsi sepele ini melanggar SRP, dan lalu apa? Siapa peduli? Pelanggaran terhadap SRP mulai menjadi masalah ketika segalanya menjadi lebih rumit.

Masalah Anda dalam kasus khusus ini kemungkinan besar berasal dari fakta bahwa kelas lebih rumit daripada beberapa baris yang telah Anda tunjukkan kepada kami.

Secara khusus, masalah yang paling mungkin terletak pada kenyataan bahwa kelas ini tidak hanya mengelola cache, tetapi juga mungkin berisi implementasi GetFluffiesFromDb()metode ini. Jadi, pelanggaran SRP ada di kelas, bukan dalam beberapa metode sepele yang ditunjukkan dalam kode yang Anda posting.

Jadi, inilah saran tentang bagaimana menangani semua jenis kasus yang termasuk dalam kategori umum ini, dengan bantuan Pola Penghias .

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

dan digunakan sebagai berikut:

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

Perhatikan bagaimana CachingFluffiesProvider.GetFluffies()tidak takut mengandung kode yang melakukan pengecekan dan pembaruan waktu, karena itu hal-hal sepele. Apa mekanisme ini lakukan adalah untuk mengatasi dan menangani SRP di tingkat desain sistem, di mana itu penting, bukan pada tingkat metode individu kecil, di mana itu tidak masalah.

Mike Nakis
sumber
1
+1 untuk mengetahui bahwa bulu, caching, dan akses basis data sebenarnya adalah tiga tanggung jawab. Anda bahkan dapat mencoba membuat antarmuka FluffiesProvider dan dekorator generik (IProvider <Fluffy>, ...) tetapi ini mungkin YAGNI.
Roman Reiner
Jujur saja, jika hanya ada satu jenis cache dan selalu menarik objek dari database, ini IMHO terlalu banyak dirancang (bahkan jika kelas "nyata" mungkin lebih kompleks seperti yang bisa kita lihat dalam contoh). Abstraksi hanya demi abstraksi tidak membuat kode lebih bersih atau lebih dapat dipertahankan.
Doc Brown
Masalah @DocBrown adalah kurangnya konteks pertanyaan. Saya suka jawaban ini karena ia menunjukkan cara yang telah saya gunakan berulang kali dalam aplikasi yang lebih besar dan karena mudah untuk menulis tes, saya juga menyukai jawaban saya karena itu hanya perubahan kecil dan menghasilkan sesuatu yang jelas tanpa ada perancangan yang berlebihan sehingga Saat ini berdiri, tanpa konteks cukup banyak semua jawaban di sini baik:]
stijn
1
FWIW, kelas yang ada dalam pikiran saya ketika saya mengajukan pertanyaan lebih rumit daripada FluffiesManager, tetapi tidak terlalu. Sekitar 200 baris, mungkin. Saya belum mengajukan pertanyaan ini karena saya telah menemukan masalah dengan desain saya (belum?), Hanya karena saya tidak dapat menemukan cara untuk secara ketat mematuhi SRP, dan itu bisa menjadi masalah dalam kasus yang lebih kompleks. Jadi, kurangnya konteks agak disengaja. Saya pikir jawaban ini bagus.
gagak
2
@stijn: baik, saya pikir jawaban Anda sangat undervoted. Alih-alih menambahkan abstraksi yang tidak perlu, Anda hanya memotong dan menyebutkan tanggung jawab secara berbeda, yang harus selalu menjadi pilihan pertama sebelum menumpuk tiga lapisan warisan ke masalah yang begitu sederhana.
Doc Brown
6

Kelas Anda sendiri tampaknya baik-baik saja bagi saya, tetapi Anda benar bahwa LoadFluffies()tidak persis apa yang diiklankan namanya. Salah satu solusi sederhana adalah mengubah nama dan memindahkan reload eksplisit dari GetFluffies, ke fungsi dengan deskripsi yang sesuai. Sesuatu seperti

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

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

terlihat bersih bagi saya (juga karena seperti yang dikatakan Patrick: ini terdiri dari fungsi patuh SRP kecil lainnya), dan terutama juga jelas yang kadang-kadang sama pentingnya.

stijn
sumber
1
Saya suka kesederhanaan dalam hal ini.
gagak
6

Saya percaya kelas Anda melakukan satu hal; ini adalah data cache dengan batas waktu. LoadFluffies tampak seperti abstraksi yang tidak berguna kecuali Anda menyebutnya dari berbagai tempat. Saya pikir akan lebih baik untuk mengambil dua baris dari LoadFluffies dan menempatkannya dalam persyaratan NeedsReload di GetFluffies. Ini akan membuat implementasi GetFluffies jauh lebih jelas dan kode masih bersih, saat Anda menyusun subrutin tanggung jawab tunggal untuk mencapai tujuan tunggal, pengambilan data yang di-cache dari db. Di bawah ini adalah metode get fluffies yang diperbarui.

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

    return m_Cache;
}
Patrick Goley
sumber
Meskipun ini adalah jawaban pertama yang cukup bagus, harap diingat bahwa kode "hasil" seringkali merupakan tambahan yang bagus.
Dana Gugatan Monica
4

Nalurimu benar. Kelas Anda, meskipun kecil, terlalu banyak melakukan. Anda harus memisahkan logika cache penyegaran berjangka waktu ke dalam kelas yang sepenuhnya umum. Kemudian buat instance spesifik kelas itu untuk mengelola Fluffies, sesuatu seperti ini (tidak dikompilasi, kode kerja dibiarkan sebagai latihan untuk pembaca):

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

Keuntungan tambahan adalah sekarang sangat mudah untuk menguji TimedRefreshCache.

kevin cline
sumber
1
Saya setuju bahwa jika logika penyegaran menjadi lebih rumit daripada dalam contoh, mungkin ide yang bagus untuk mengubahnya menjadi kelas yang terpisah. Tetapi saya tidak setuju bahwa kelas dalam contoh ini, seperti apa adanya, terlalu banyak.
Doc Brown
@ Kevin, saya tidak berpengalaman dalam TDD. Bisakah Anda menguraikan bagaimana Anda menguji TimedRefreshCache? Saya tidak melihatnya sebagai "sangat mudah", tetapi itu bisa menjadi kurangnya keahlian saya.
gagak
1
Saya pribadi tidak suka jawaban Anda karena kerumitannya. Ini sangat generik dan sangat abstrak dan mungkin yang terbaik dalam situasi yang lebih rumit. Tetapi dalam kasus sederhana ini adalah 'hanya untuk banyak'. Silakan lihat jawaban stijn. Jawaban yang bagus, pendek, dan mudah dibaca. Semua orang akan memahaminya secara tidak dewasa. Bagaimana menurut anda?
Dieter Meemken
1
@raven Anda dapat menguji TimedRefreshCache dengan menggunakan interval pendek (seperti 100 ms) dan produser yang sangat sederhana (seperti DateTime.Now). Setiap 100 ms cache akan menghasilkan nilai baru, di antaranya akan mengembalikan nilai sebelumnya.
kevin cline
1
@DocBrown: Masalahnya adalah bahwa seperti yang tertulis itu tidak dapat dicoba. Logika pengaturan waktu (dapat diuji) digabungkan dengan logika basis data, yang kemudian banyak diejek. Setelah jahitan dibuat untuk mengejek panggilan basis data, Anda 95% dari jalan menuju solusi generik. Saya telah menemukan bahwa membangun kelas-kelas kecil ini biasanya terbayar karena mereka akhirnya digunakan kembali lebih dari yang diharapkan.
kevin cline
1

Kelas Anda baik-baik saja, SRP adalah tentang kelas bukan fungsi, seluruh kelas bertanggung jawab untuk menyediakan "Fluffies" dari "Sumber Data" sehingga Anda bebas dalam implementasi internal.

Jika Anda ingin memperluas mekanisme cahing, Anda dapat membuat kelas bertanggung jawab untuk menonton sumber data

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
    }
}
yousif.aljamri
sumber
Menurut Paman Bob: FUNGSI HARUS MELAKUKAN SATU HAL. MEREKA HARUS MELAKUKANNYA. MEREKA HARUS MELAKUKANNYA SAJA. Kode Bersih hal.35.
gagak