วิธีหลีกเลี่ยงการละเมิด SRP ในชั้นเรียนเพื่อจัดการแคช?


12

หมายเหตุ:ตัวอย่างโค้ดเขียนด้วย c # แต่นั่นไม่ควรสำคัญ ฉันใส่ c # เป็นแท็กเพราะหาแท็บไม่ได้อีก นี่คือเกี่ยวกับโครงสร้างรหัส

ฉันอ่าน Clean Code และพยายามเป็นโปรแกรมเมอร์ที่ดีขึ้น

ฉันมักจะพบว่าตัวเองกำลังดิ้นรนเพื่อทำตามหลักการความรับผิดชอบเดียว (ชั้นเรียนและฟังก์ชั่นควรทำเพียงสิ่งเดียว) โดยเฉพาะอย่างยิ่งในฟังก์ชั่น บางทีปัญหาของฉันคือ "สิ่งหนึ่ง" ไม่ชัดเจน แต่ก็ยัง ...

ตัวอย่าง: ฉันมีรายการ 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()ดูเหมือนว่าตกลงกับฉัน ผู้ใช้ถามถึงความฟู่ฟ่าเราจัดเตรียมไว้ จะกู้คืนพวกมันจากฐานข้อมูลถ้าจำเป็น แต่ก็ถือได้ว่าเป็นส่วนหนึ่งของการรับความฟู่ฟ่า (แน่นอนว่าเป็นเรื่องส่วนตัว)

NeedsReload()ดูเหมือนถูกด้วย ตรวจสอบว่าเราจำเป็นต้องโหลดซ้ำซาก UpdateNextLoad นั้นใช้ได้ อัพเดตเวลาสำหรับการโหลดครั้งถัดไป นั่นเป็นสิ่งหนึ่งที่แน่นอน

อย่างไรก็ตามฉันรู้สึกว่าสิ่งที่LoadFluffies()ไม่สามารถอธิบายได้เป็นสิ่งเดียว กำลังรับข้อมูลจากฐานข้อมูลและกำลังกำหนดเวลาโหลดครั้งถัดไป เป็นการยากที่จะโต้แย้งว่าการคำนวณเวลาสำหรับการโหลดซ้ำครั้งถัดไปเป็นส่วนหนึ่งของการรับข้อมูล อย่างไรก็ตามฉันไม่สามารถหาวิธีที่ดีกว่า (เปลี่ยนชื่อฟังก์ชั่นให้LoadFluffiesAndScheduleNextLoadดีขึ้น แต่มันทำให้ปัญหาชัดเจนขึ้น)

มีทางออกที่ดีที่จะเขียนคลาสนี้ตาม SRP หรือไม่? ฉันเป็นคนเชื่องช้าเกินไปหรือไม่

หรือบางทีชั้นเรียนของฉันไม่ได้ทำอะไรซักอย่างจริงๆ?


3
จาก "เขียนใน C # แต่ไม่ควรสำคัญ", "นี่คือโครงสร้างของโค้ด", "ตัวอย่าง: ... เราไม่สนใจว่าปุยคืออะไร", "เพื่อให้ง่าย, สมมติว่า ... ", นี่ไม่ใช่คำขอตรวจสอบโค้ด แต่เป็นคำถามเกี่ยวกับหลักการเขียนโปรแกรมทั่วไป
200_success

@ 200_success ขอบคุณและขอโทษฉันคิดว่านี่น่าจะเพียงพอสำหรับ CR
raven


2
ในอนาคตคุณจะดีขึ้นด้วย "วิดเจ็ต" แทนที่จะเป็นปุยสำหรับคำถามที่คล้ายกันในอนาคตเนื่องจากเป็นที่เข้าใจว่าวิดเจ็ตเป็นตัวอย่างที่ไม่เฉพาะ
whatsisname

1
ฉันรู้ว่ามันเป็นเพียงตัวอย่างรหัส แต่ใช้DateTime.UtcNowเพื่อหลีกเลี่ยงการเปลี่ยนการประหยัดเวลากลางวันหรือแม้แต่การเปลี่ยนแปลงในเขตเวลาปัจจุบัน
Mark Hurd

คำตอบ:


23

หากชั้นเรียนนี้เล็กน้อยจริง ๆ ดูเหมือนว่าจะเป็นเช่นนั้นก็ไม่จำเป็นต้องกังวลเกี่ยวกับการละเมิด SRP แล้วถ้าฟังก์ชัน 3 บรรทัดมี 2 บรรทัดทำสิ่งหนึ่งและอีก 1 บรรทัดทำสิ่งอื่น? ใช่ฟังก์ชั่นเล็ก ๆ น้อย ๆ นี้ละเมิด SRP และอะไรนะ? ใครสน? การละเมิด SRP เริ่มเป็นปัญหาเมื่อสิ่งต่าง ๆ มีความซับซ้อนมากขึ้น

ปัญหาของคุณในกรณีนี้ส่วนใหญ่อาจเกิดจากข้อเท็จจริงที่ว่าคลาสนั้นซับซ้อนกว่าสองสามบรรทัดที่คุณแสดงให้เราเห็น

โดยเฉพาะอย่างยิ่งปัญหาที่พบบ่อยที่สุดคือความจริงที่ว่าคลาสนี้ไม่เพียง แต่จัดการแคช แต่ยังอาจมีการใช้งานของ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()ไม่กลัวที่จะมีรหัสที่ใช้ในการตรวจสอบและอัปเดตเวลาเพราะมันเป็นเรื่องเล็กน้อย สิ่งที่กลไกนี้ทำคือการจัดการและจัดการ SRP ในระดับการออกแบบระบบที่สำคัญไม่ใช่ระดับของวิธีการเล็ก ๆ ที่ไม่สำคัญ


1
+1 สำหรับการรับรู้ว่าการเข้าถึงฟลัชแคชและฐานข้อมูลเป็นความรับผิดชอบที่แท้จริงสามประการ คุณสามารถลองใช้ FluffiesProvider และส่วนตกแต่งทั่วไป (IProvider <Fluffy>, ... ) แต่นี่อาจเป็น YAGNI
Roman Reiner

จริงๆแล้วถ้ามีแคชเพียงประเภทเดียวและดึงวัตถุจากฐานข้อมูลเสมอนี่คือ IMHO ที่ออกแบบมาอย่างหนักมากเกินไป (แม้ว่าคลาส "ของจริง" อาจซับซ้อนกว่าที่เราเห็นในตัวอย่าง) สิ่งที่เป็นนามธรรมเพียงเพื่อประโยชน์ของสิ่งที่เป็นนามธรรมไม่ได้ทำให้การทำความสะอาดโค้ดหรือการบำรุงรักษามากขึ้น
Doc Brown

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

ดูสะอาดสำหรับฉัน (เช่นเดียวกับที่ Patrick บอก: มันประกอบด้วยฟังก์ชั่นการเชื่อฟังขนาดเล็กอื่น ๆ ของ SRP) และโดยเฉพาะอย่างยิ่งชัดเจนซึ่งบางครั้งก็สำคัญ


1
ฉันชอบความเรียบง่ายในสิ่งนี้
กา

6

ฉันเชื่อว่าชั้นเรียนของคุณกำลังทำสิ่งหนึ่ง มันเป็นแคชข้อมูลที่มีการหมดเวลา LoadFluffies ดูเหมือนเป็นนามธรรมที่ไร้ประโยชน์เว้นแต่คุณจะเรียกมันจากหลาย ๆ ที่ ฉันคิดว่ามันจะดีกว่าถ้าจะเอาสองบรรทัดจาก LoadFluffies มาใส่ใน NeedsReload แบบมีเงื่อนไขใน GetFluffies สิ่งนี้จะทำให้การใช้งาน GetFluffies มีความชัดเจนมากขึ้นและยังคงเป็นโค้ดที่สะอาดเนื่องจากคุณกำลังเขียนรูทีนย่อยความรับผิดชอบเดี่ยวเพื่อให้บรรลุเป้าหมายเดียวคือการดึงข้อมูลที่เก็บไว้จากฐานข้อมูล ด้านล่างนี้เป็นวิธีการรับข้อมูล 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

@ เควินฉันไม่ได้มีประสบการณ์ใน TDD คุณช่วยอธิบายเกี่ยวกับวิธีทดสอบ TimedRefreshCache ได้อย่างไร? ฉันไม่เห็นว่ามันเป็น "ง่ายมาก" แต่อาจเป็นเพราะฉันขาดความเชี่ยวชาญ
กา

1
โดยส่วนตัวแล้วฉันไม่ชอบคำตอบของคุณเพราะมันซับซ้อน มันธรรมดามากและเป็นนามธรรมมากและอาจดีที่สุดในสถานการณ์ที่ซับซ้อนมากขึ้น แต่ในกรณีง่ายๆนี้มันเป็น 'เพียงมาก' โปรดดูคำตอบของ stijn ช่างเป็นคำตอบที่ดีสั้นและอ่านง่าย ทุกคนจะเข้าใจมันโดยไม่เจตนา คุณคิดอย่างไร?
Dieter Meemken

1
@raven คุณสามารถทดสอบ TimedRefreshCache โดยใช้ช่วงเวลาสั้น ๆ (เช่น 100ms) และผู้สร้างที่ง่ายมาก (เช่น DateTime.Now) แคชทุก 100 มิลลิวินาทีจะสร้างค่าใหม่ในระหว่างนั้นจะคืนค่าก่อนหน้า
วินไคลน์

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

ตามที่ลุงบ็อบ: ฟังก์ชั่นควรทำสิ่งหนึ่งสิ่ง พวกเขาควรทำดี พวกเขาควรจะทำมันเท่านั้น ทำความสะอาดรหัสหน้า 35
กา
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.