Làm thế nào để tránh vi phạm SRP trong một lớp để quản lý bộ nhớ đệm?


12

Lưu ý: Mẫu mã được viết bằng c #, nhưng điều đó không quan trọng. Tôi đã đặt c # làm thẻ vì tôi không thể tìm thấy thẻ nào phù hợp hơn. Đây là về cấu trúc mã.

Tôi đang đọc Clean Code và cố gắng trở thành một lập trình viên tốt hơn.

Tôi thường thấy mình phải vật lộn để tuân theo Nguyên tắc Trách nhiệm Đơn lẻ (các lớp và chức năng chỉ nên làm một việc), đặc biệt trong các chức năng. Có lẽ vấn đề của tôi là "một điều" không được xác định rõ, nhưng vẫn ...

Một ví dụ: Tôi có một danh sách Fluffies trong cơ sở dữ liệu. Chúng tôi không quan tâm Fluffy là gì. Tôi muốn một lớp học để phục hồi lông tơ. Tuy nhiên, lông tơ có thể thay đổi theo một số logic. Tùy thuộc vào một số logic, lớp này sẽ trả về dữ liệu từ bộ đệm hoặc nhận mới nhất từ ​​cơ sở dữ liệu. Chúng ta có thể nói rằng nó quản lý lông tơ, và đó là một điều. Để làm cho nó đơn giản, giả sử dữ liệu được tải là tốt trong một giờ, và sau đó nó phải được tải lại.

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()có vẻ ổn với tôi Người dùng yêu cầu một số lông tơ, chúng tôi cung cấp cho họ. Sẽ phục hồi chúng từ DB nếu cần, nhưng đó có thể được coi là một phần của việc lấy lông tơ (tất nhiên, điều đó hơi chủ quan).

NeedsReload()có vẻ đúng Kiểm tra nếu chúng ta cần tải lại lông tơ. UpdateNextLoad vẫn ổn. Cập nhật thời gian cho lần tải lại tiếp theo. Đó chắc chắn là một điều duy nhất.

Tuy nhiên, tôi cảm thấy những gì LoadFluffies()không thể được mô tả là một điều duy nhất. Nó nhận dữ liệu từ Cơ sở dữ liệu và lên lịch tải lại lần tiếp theo. Thật khó để tranh luận rằng việc tính toán thời gian cho lần tải lại tiếp theo là một phần của việc lấy dữ liệu. Tuy nhiên, tôi không thể tìm ra cách nào tốt hơn để làm điều đó (đổi tên hàm thành LoadFluffiesAndScheduleNextLoadcó thể tốt hơn, nhưng nó chỉ làm cho vấn đề rõ ràng hơn).

Có một giải pháp tao nhã để thực sự viết lớp này theo SRP không? Có phải tôi đang quá tầm phào?

Hoặc có lẽ lớp học của tôi không thực sự chỉ làm một điều?


3
Dựa trên "được viết bằng C #, nhưng điều đó không quan trọng", "Đây là về cấu trúc mã", "Một ví dụ: Vách Chúng tôi không quan tâm Fluffy là gì", "Để đơn giản, hãy nói Rằng", đây không phải là yêu cầu đánh giá mã, mà là câu hỏi về nguyên tắc lập trình chung.
200_success

@ 200_success cảm ơn bạn, và xin lỗi, tôi nghĩ rằng điều này sẽ phù hợp với CR
raven


2
Trong tương lai, bạn sẽ tốt hơn với "widget" thay vì fluffy cho các câu hỏi tương tự trong tương lai, vì một widget được hiểu là một ví dụ không đặc biệt trong các ví dụ.
whatsisname

1
Tôi biết đó chỉ là mã ví dụ, nhưng sử dụng DateTime.UtcNowđể bạn tránh thay đổi tiết kiệm ánh sáng ban ngày hoặc thậm chí thay đổi múi giờ hiện tại.
Đánh dấu Hurd

Câu trả lời:


23

Nếu lớp này thực sự tầm thường như nó có vẻ, thì sẽ không cần phải lo lắng về việc vi phạm SRP. Vậy điều gì sẽ xảy ra nếu một hàm 3 dòng có 2 dòng làm một việc và một dòng khác làm một việc khác? Vâng, chức năng tầm thường này vi phạm SRP, và vậy thì sao? Ai quan tâm? Việc vi phạm SRP bắt đầu trở thành một vấn đề khi mọi thứ trở nên phức tạp hơn.

Vấn đề của bạn trong trường hợp cụ thể này có lẽ xuất phát từ thực tế là lớp phức tạp hơn so với vài dòng mà bạn đã chỉ cho chúng tôi.

Cụ thể, vấn đề có lẽ nhất nằm ở chỗ lớp này không chỉ quản lý bộ đệm, mà còn có thể chứa việc thực hiện GetFluffiesFromDb()phương thức. Vì vậy, vi phạm SRP là trong lớp, không phải trong một vài phương thức tầm thường được hiển thị trong mã mà bạn đã đăng.

Vì vậy, đây là một gợi ý về cách xử lý tất cả các loại trường hợp nằm trong danh mục chung này, với sự trợ giúp của Mẫu trang trí .

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

và nó được sử dụng như sau:

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

Lưu ý làm thế nào để CachingFluffiesProvider.GetFluffies()không sợ chứa mã kiểm tra và cập nhật thời gian, bởi vì đó là thứ tầm thường. Những gì cơ chế này làm là giải quyết và xử lý SRP ở cấp thiết kế hệ thống, nơi nó quan trọng, không phải ở cấp độ của các phương thức riêng lẻ nhỏ, dù sao nó cũng không quan trọng.


1
+1 để nhận ra rằng lông tơ, bộ nhớ đệm và truy cập cơ sở dữ liệu thực sự là ba trách nhiệm. Bạn thậm chí có thể thử tạo giao diện FluffiesProvider và các trình trang trí chung (IProvider <Fluffy>, ...) nhưng đây có thể là YAGNI.
Roman Reiner

Thành thật mà nói, nếu chỉ có một loại bộ đệm và nó luôn lấy các đối tượng từ cơ sở dữ liệu, thì đây là IMHO được thiết kế quá mức (ngay cả khi lớp "thực" có thể phức tạp hơn trong ví dụ). Trừu tượng chỉ vì lợi ích trừu tượng không làm cho mã sạch hơn hoặc dễ bảo trì hơn.
Doc Brown

Vấn đề @DocBrown là thiếu bối cảnh cho câu hỏi. Tôi thích câu trả lời này vì nó cho thấy một cách mà tôi đã sử dụng hết lần này đến lần khác trong các ứng dụng lớn hơn và bởi vì nó dễ dàng viết các bài kiểm tra, tôi cũng thích câu trả lời của mình vì nó chỉ là một thay đổi nhỏ và mang lại một cái gì đó rõ ràng mà không cần thiết quá hiện đang đứng, không có ngữ cảnh khá nhiều tất cả các câu trả lời ở đây đều tốt:]
stijn

1
FWIW, lớp tôi có trong đầu khi tôi hỏi câu hỏi phức tạp hơn FluffiesManager, nhưng không quá nhiều. Một số 200 dòng, có thể. Tôi chưa hỏi câu hỏi này vì tôi đã tìm thấy bất kỳ vấn đề nào với thiết kế của mình (chưa?), Chỉ vì tôi không thể tìm ra cách tuân thủ nghiêm ngặt SRP, và đó có thể là một vấn đề trong những trường hợp phức tạp hơn. Vì vậy, việc thiếu bối cảnh có phần dự định. Tôi nghĩ rằng câu trả lời này là tuyệt vời.
quạ

2
@stijn: tốt, tôi nghĩ rằng câu trả lời của bạn bị đánh giá thấp. Thay vì thêm sự trừu tượng không cần thiết, bạn chỉ cần cắt và đặt tên cho các trách nhiệm khác nhau, đây luôn là lựa chọn đầu tiên trước khi chồng ba lớp kế thừa cho một vấn đề đơn giản như vậy.
Doc Brown

6

Lớp học của bạn có vẻ tốt đối với tôi, nhưng bạn đúng trong đó LoadFluffies()không chính xác những gì tên quảng cáo. Một giải pháp đơn giản là thay đổi tên và chuyển tải lại rõ ràng ra khỏi GetFluffies, thành một chức năng với một mô tả thích hợp. Cái gì đó như

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

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

đối với tôi có vẻ sạch sẽ (cũng như như Patrick nói: nó bao gồm các chức năng nhỏ bé khác của SRP), và đặc biệt cũng rõ ràng đôi khi cũng quan trọng như vậy.


1
Tôi thích sự đơn giản trong việc này.
quạ

6

Tôi tin rằng lớp học của bạn đang làm một điều; đó là bộ đệm dữ liệu với thời gian chờ. LoadFluffies có vẻ như một sự trừu tượng vô dụng trừ khi bạn gọi nó từ nhiều nơi. Tôi nghĩ sẽ tốt hơn nếu lấy hai dòng từ LoadFluffies và đặt chúng vào điều kiện NeedsReload trong GetFluffies. Điều này sẽ làm cho việc triển khai GetFluffies trở nên rõ ràng hơn rất nhiều và vẫn là mã sạch, vì bạn đang soạn thảo các chương trình con trách nhiệm duy nhất để thực hiện một mục tiêu duy nhất, truy xuất dữ liệu được lưu trong bộ nhớ cache từ db. Dưới đây là phương pháp nhận được lông tơ được cập nhật.

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

    return m_Cache;
}

Mặc dù đây là câu trả lời đầu tiên khá hay, xin lưu ý rằng mã "kết quả" thường là một bổ sung tốt.
Vụ kiện của Quỹ Monica

4

Bản năng của bạn là chính xác. Lớp học của bạn, dù nhỏ nhưng có thể, đang làm quá nhiều. Bạn nên tách logic bộ nhớ đệm làm mới theo thời gian thành một lớp hoàn toàn chung chung. Sau đó, tạo một phiên bản cụ thể của lớp đó để quản lý Fluffies, một cái gì đó như thế này (không được biên dịch, mã làm việc được để lại như một bài tập cho người đọc):

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

Một lợi thế bổ sung là bây giờ rất dễ dàng để kiểm tra TimedRefreshCache.


1
Tôi đồng ý rằng nếu logic làm mới trở nên phức tạp hơn trong ví dụ, thì có thể nên cấu trúc lại nó thành một lớp riêng. Nhưng tôi không đồng ý rằng lớp trong ví dụ, vì nó là quá nhiều.
Doc Brown

@kevin, tôi không có kinh nghiệm về TDD. Bạn có thể giải thích về cách bạn sẽ kiểm tra TimedRefreshCache không? Tôi không thấy nó là "rất dễ", nhưng đó có thể là sự thiếu chuyên môn của tôi.
quạ

1
Cá nhân tôi không thích câu trả lời của bạn vì nó phức tạp. Nó rất chung chung và rất trừu tượng và có thể là tốt nhất trong các tình huống phức tạp hơn. Nhưng trong trường hợp đơn giản này, nó "đơn giản là nhiều". Xin hãy xem câu trả lời của stijn. Thật là một câu trả lời hay, ngắn gọn và dễ đọc. Mọi người sẽ hiểu nó ngay lập tức. Bạn nghĩ sao?
Dieter Meemken

1
@raven Bạn có thể kiểm tra TimedRefreshCache bằng cách sử dụng một khoảng thời gian ngắn (như 100ms) và một nhà sản xuất rất đơn giản (như DateTime.Now). Cứ sau 100 ms bộ đệm sẽ tạo ra một giá trị mới, ở giữa nó sẽ trả về giá trị trước đó.
kevin cline

1
@DocBrown: Vấn đề là như đã viết, nó là không thể kiểm chứng. Logic thời gian (có thể kiểm tra) được kết hợp với logic cơ sở dữ liệu, sau đó bị chế giễu. Khi một đường may được tạo để giả định cuộc gọi cơ sở dữ liệu, bạn sẽ hoàn thành 95% đường đến giải pháp chung. Tôi đã thấy rằng việc xây dựng các lớp nhỏ này thường được đền đáp vì cuối cùng chúng được sử dụng lại nhiều hơn dự kiến.
kevin cline

1

Lớp của bạn vẫn ổn, SRP là về một lớp không phải là một chức năng, cả lớp chịu trách nhiệm cung cấp "Fluffies" từ "Nguồn dữ liệu" để bạn được tự do thực hiện nội bộ.

Nếu bạn muốn mở rộng cơ chế cahing, bạn có thể tạo lớp có thể đo được để xem nguồn dữ liệu

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

Theo chú Bob: CHỨC NĂNG NÊN LÀM MỘT ĐIỀU. HỌ NÊN LÀM NÓ. HỌ NÊN LÀM NÓ. Mã sạch p.35.
quạ
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.