Các loại dữ liệu tùy chỉnh và trách nhiệm


10

Trong những tháng qua, tôi đã yêu cầu mọi người ở đây trên SE và trên các trang web khác cung cấp cho tôi một số lời chỉ trích mang tính xây dựng liên quan đến mã của tôi. Có một điều luôn xuất hiện gần như mọi lúc và tôi vẫn không đồng ý với khuyến nghị đó; : P Tôi muốn thảo luận về nó ở đây và có lẽ mọi thứ sẽ trở nên rõ ràng hơn với tôi.

Đó là về nguyên tắc trách nhiệm đơn (SRP). Về cơ bản, tôi có một lớp dữ liệu, Fontkhông chỉ chứa các hàm để thao tác dữ liệu mà còn để tải nó. Tôi đã nói với hai người nên tách biệt, rằng các chức năng tải nên được đặt bên trong một lớp nhà máy; Tôi nghĩ rằng đây là một cách hiểu sai về SRP ...

Một mảnh từ lớp phông chữ của tôi

class Font
{
  public:
    bool isLoaded() const;
    void loadFromFile(const std::string& file);
    void loadFromMemory(const void* buffer, std::size_t size);
    void free();

    void some();
    void another();
};

Thiết kế đề xuất

class Font
{
  public:
    void some();
    void another();
};


class FontFactory
{
  public:
    virtual std::unique_ptr<Font> createFromFile(...) = 0;
    virtual std::unique_ptr<Font> createFromMemory(...) = 0;
};

Thiết kế được đề xuất được cho là theo SRP, nhưng tôi không đồng ý - tôi nghĩ nó đi quá xa. Các Fontlớp học không phải là còn tự túc (nó là vô ích mà không có nhà máy), và FontFactorynhu cầu để biết thông tin chi tiết về việc thực hiện của tài nguyên, mà có lẽ là thực hiện thông qua tình bạn hoặc getters công cộng, trong đó tiếp tục phơi bày việc thực hiện Font. Tôi nghĩ rằng đây là một trường hợp trách nhiệm phân mảnh .

Đây là lý do tại sao tôi nghĩ rằng cách tiếp cận của tôi là tốt hơn:

  • Fontlà tự túc - Tự túc, dễ hiểu và dễ duy trì hơn. Ngoài ra, bạn có thể sử dụng lớp mà không cần phải bao gồm bất cứ điều gì khác. Tuy nhiên, nếu bạn thấy bạn cần một sự quản lý tài nguyên phức tạp hơn (một nhà máy), bạn cũng có thể dễ dàng làm điều đó (sau này tôi sẽ nói về nhà máy của riêng tôi, ResourceManager<Font>).

  • Theo thư viện chuẩn - Tôi tin rằng các loại do người dùng xác định nên thử càng nhiều càng tốt để sao chép hành vi của các loại tiêu chuẩn trong ngôn ngữ tương ứng đó. Nó std::fstreamlà tự cung cấp và nó cung cấp các chức năng như openclose. Theo thư viện tiêu chuẩn có nghĩa là không cần phải nỗ lực học thêm một cách làm việc khác. Bên cạnh đó, nói chung, ủy ban tiêu chuẩn C ++ có thể biết nhiều về thiết kế hơn bất kỳ ai ở đây, vì vậy nếu có nghi ngờ, hãy sao chép những gì họ làm.

  • Khả năng kiểm tra - Một cái gì đó sai, vấn đề có thể ở đâu? - Đó là cách Fontxử lý dữ liệu của nó hay cách FontFactorytải dữ liệu? Bạn không thực sự biết. Có các lớp tự túc làm giảm vấn đề này: bạn có thể kiểm tra một Fontcách cô lập. Nếu sau đó bạn phải kiểm tra nhà máy và bạn biết nó Fonthoạt động tốt, bạn cũng sẽ biết rằng bất cứ khi nào xảy ra sự cố thì nó phải ở trong nhà máy.

  • Đó là thuyết bất khả tri - (Điều này giao thoa một chút với điểm đầu tiên của tôi.) Thực Fonthiện điều đó và không đưa ra giả định nào về cách bạn sẽ sử dụng nó: bạn có thể sử dụng nó theo bất kỳ cách nào bạn muốn. Buộc người dùng sử dụng một nhà máy làm tăng sự ghép nối giữa các lớp.

Tôi cũng có một nhà máy

(Bởi vì thiết kế Fontcho phép tôi.)

Hay đúng hơn là một người quản lý, không chỉ đơn thuần là một nhà máy ... Fontlà tự túc nên người quản lý không cần biết cách xây dựng một nhà quản lý ; thay vào đó, người quản lý đảm bảo cùng một tệp hoặc bộ đệm không được tải vào bộ nhớ nhiều lần. Bạn có thể nói một nhà máy có thể làm điều tương tự, nhưng điều đó sẽ không phá vỡ SRP? Nhà máy sau đó không chỉ phải xây dựng các đối tượng, mà còn quản lý chúng.

template<class T>
class ResourceManager
{
  public:
    ResourcePtr<T> acquire(const std::string& file);
    ResourcePtr<T> acquire(const void* buffer, std::size_t size);
};

Đây là một minh chứng về cách người quản lý có thể được sử dụng. Lưu ý rằng về cơ bản nó được sử dụng chính xác như một nhà máy.

void test(ResourceManager<Font>* rm)
{
    // The same file isn't loaded twice into memory.
    // I can still have as many Fonts using that file as I want, though.
    ResourcePtr<Font> font1 = rm->acquire("fonts/arial.ttf");
    ResourcePtr<Font> font2 = rm->acquire("fonts/arial.ttf");

    // Print something with the two fonts...
}

Dòng dưới cùng ...

(Tôi muốn đặt một tl; dr ở đây, nhưng tôi không thể nghĩ ra một
thứ. Vui lòng gửi bất kỳ đối số bạn có và cũng có bất kỳ lợi thế nào mà bạn nghĩ rằng thiết kế được đề xuất có trên thiết kế của riêng tôi. Về cơ bản, hãy cố gắng cho tôi thấy rằng tôi sai. :)


2
Nhắc tôi về ActiveRecord vs DataMapper của Martin Fowler .
Người dùng

Cung cấp sự tiện lợi (thiết kế hiện tại của bạn) trong giao diện ngoài cùng, hướng tới người dùng. Sử dụng SRP trong nội bộ để nó sẽ giảm bớt những thay đổi triển khai trong tương lai của bạn. Tôi có thể nghĩ về các họa tiết của trình tải Font bỏ qua chữ nghiêng và đậm; chỉ tải Unicode BMP, v.v.
rwong


@rwong Tôi biết về bài thuyết trình đó, tôi đã có một dấu trang cho nó ( video ). :) Nhưng tôi không hiểu những gì bạn nói trong bình luận khác của bạn ...
Paul

1
@rwong Không phải nó đã là một lớp lót rồi sao? Bạn chỉ cần một dòng, cho dù bạn tải một Phông chữ trực tiếp hoặc thông qua ResourceManager. Và điều gì ngăn tôi thực hiện lại RM nếu người dùng phàn nàn?
Paul

Câu trả lời:


7

Theo tôi, không có gì sai với mã đó, nó thực hiện những gì bạn cần nó theo cách hợp lý và dễ bảo trì một cách hợp lý.

Tuy nhiên , vấn đề mà bạn gặp phải với mã này là nếu bạn muốn nó làm bất cứ điều gì khác, bạn sẽ phải thay đổi tất cả .

Quan điểm của SRP là nếu bạn có một thành phần duy nhất 'CompA' có thuật toán A () và bạn cần thay đổi thuật toán A () thì bạn cũng không cần phải thay đổi 'CompB'.

Các kỹ năng C ++ của tôi quá thô sơ để đề xuất một kịch bản hợp lý, nơi bạn sẽ cần thay đổi giải pháp quản lý phông chữ của mình, nhưng trường hợp thông thường tôi thực hiện là ý tưởng trượt trong một lớp bộ đệm. Lý tưởng nhất là bạn không muốn thứ tải công cụ để biết nó đến từ đâu, cũng không phải thứ được tải chăm sóc từ đâu đến, bởi vì sau đó thực hiện các thay đổi đơn giản hơn. Đó là tất cả về khả năng bảo trì.

Một ví dụ có thể là bạn đang tải phông chữ của mình từ nguồn thứ ba (giả sử hình ảnh nhân vật). Để đạt được điều này, bạn sẽ cần thay đổi trình tải của mình (để gọi phương thức thứ ba nếu hai lần đầu tiên thất bại) và chính lớp Font để thực hiện cuộc gọi thứ ba này. Lý tưởng nhất là bạn chỉ cần tạo một nhà máy khác (SpriteFontFactory hoặc bất cứ thứ gì), thực hiện cùng phương thức loadFont (...) và dán nó vào danh sách các nhà máy ở đâu đó có thể được sử dụng để tải phông chữ.


1
À, tôi hiểu rồi: nếu tôi thêm một cách để tải phông chữ, tôi sẽ cần thêm một chức năng thu nhận vào trình quản lý và thêm một chức năng tải vào tài nguyên. Thật vậy, đó là một bất lợi. Tuy nhiên, tùy thuộc vào nguồn mới này có thể là gì, có thể bạn sẽ phải xử lý dữ liệu khác nhau (TTF là một thứ, các họa tiết phông chữ là một thứ khác), vì vậy bạn thực sự không thể dự đoán được một thiết kế nhất định sẽ linh hoạt như thế nào. Tôi thấy quan điểm của bạn, mặc dù.
Paul

Vâng, như tôi đã nói, các kỹ năng C ++ của tôi khá hoen rỉ nên tôi đã cố gắng đưa ra một minh chứng khả thi cho vấn đề này, tôi đồng ý về điều linh hoạt. Nó thực sự phụ thuộc vào những gì bạn đang làm với mã của bạn, như tôi đã nói, tôi nghĩ rằng mã gốc của bạn là giải pháp hoàn toàn hợp lý cho vấn đề.
Ed James

Câu hỏi tuyệt vời và câu trả lời tuyệt vời, và điều tốt nhất là nhiều nhà phát triển có thể học hỏi từ nó. Đây là lý do tại sao tôi thích đi chơi ở đây :). Ồ và vì vậy, nhận xét của tôi không hoàn toàn dư thừa, SRP có thể hơi rắc rối một chút vì bạn phải tự hỏi mình 'nếu như', điều này có vẻ phản tác dụng: 'tối ưu hóa sớm là gốc rễ của mọi tội lỗi' hoặc ' YAGNI 'triết lý. Không bao giờ có câu trả lời trắng đen!
Martijn Verburg

0

Một điều khiến tôi băn khoăn về lớp học của bạn là bạn có loadFromMemoryloadFromFilephương pháp. Tốt nhất, bạn chỉ nên có loadFromMemoryphương pháp; một phông chữ không nên quan tâm làm thế nào dữ liệu trong bộ nhớ được hình thành. Một điều nữa là bạn nên sử dụng hàm tạo / hàm hủy thay vì tải và freephương thức. Như vậy, loadFromMemorysẽ trở thành Font(const void *buf, int len)free()sẽ trở thành ~Font().


Các hàm tải có thể truy cập được từ hai hàm tạo và miễn phí được gọi trong hàm hủy - Tôi chỉ không thể hiện điều đó ở đây. Tôi thấy thuận tiện khi có thể tải phông chữ trực tiếp từ một tệp, thay vì trước tiên mở tệp, ghi dữ liệu vào bộ đệm và sau đó chuyển nó sang Phông chữ. Đôi khi tôi cũng cần tải từ bộ đệm, tuy nhiên, đó là lý do tại sao tôi có cả hai phương thức.
Paul
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.