单一责任和自定义数据类型


10

在过去的几个月中,我一直要求SE和其他网站上的人员给我一些有关我的代码的建设性批评。一件事几乎每次都会弹出,我仍然不同意这个建议。:P我想在这里讨论它,也许事情对我来说会变得更加清楚。

它与单一责任原则(SRP)有关。基本上,我有一个数据类,Font它不仅包含用于处理数据的函数,而且还用于加载数据。有人告诉我这两个应该分开,加载函数应该放在工厂类中。我认为这是对SRP的误解。

我字体类的片段

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

建议设计

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


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

建议的设计应该遵循SRP,但我不同意-我认为它太过分了。该Font班是不再自给自足(这是一个没有工厂没用),并FontFactory需要了解的资源,这可能是通过朋友或公共干将做的实施,这进一步暴露的实现细节Font。我认为这是一个分散的责任的情况。

这就是我认为我的方法更好的原因:

  • Font是自给自足的-自给自足,更易于理解和维护。另外,您可以使用该类而不必包含其他任何内容。但是,如果您发现需要对资源(工厂)进行更复杂的管理,则也可以轻松地做到这一点(稍后我将讨论我自己的工厂ResourceManager<Font>)。

  • 遵循标准库-我相信用户定义的类型应尽可能尝试以相应的语言复制标准类型的行为。该std::fstream是自给自足的,它提供了类似的功能openclose。遵循标准库意味着无需花费精力学习另一种处理方式。此外,一般而言,C ++标准委员会可能比这里的任何人都对设计了解更多,因此,如有疑问,请复制他们的工作。

  • 可测试性-出问题了,问题出在哪里?—是Font处理数据还是FontFactory加载数据的方式?你真的不知道 使类具有自足性可以减少此问题:您可以单独进行测试Font。如果您随后必须测试工厂,并且知道Font可以正常工作,那么您还将知道,只要出现问题,问题一定在工厂内部。

  • 它是与上下文无关的(这与我的第一要点相交。)Font是它的工作,并且不假设您将如何使用它:可以按自己喜欢的方式使用它。强迫用户使用工厂会增加类之间的耦合。

我也有工厂

(因为的设计Font允许我这样做。)

或者说,更多的是经理,而不仅仅是工厂…… Font是自给自足的,因此经理不需要知道如何建立一个。相反,管理器确保同一文件或缓冲区不会多次加载到内存中。您可以说工厂可以做同样的事情,但这不会破坏SRP吗?这样,工厂不仅必须构建对象,还必须管理它们。

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

这是如何使用管理器的演示。请注意,它的使用基本上和工厂一样。

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

底线...

(它想在这里放一个tl; dr,但我想不到一个
。请发布您有的任何反论点,以及您认为建议的设计比我自己的设计有的优势。基本上,请告诉我我错了。:)


2
让我想起了马丁·福勒的ActiveRecordDataMapper
用户

在最外面的,面向用户的界面中提供便利(您当前的设计)。在内部使用SRP,这样可以简化您将来的实现更改。我可以想到跳过斜体和粗体的Font loader装饰。只有加载的Unicode BMP等
rwong


@rwong我知道那个演讲,我有一个书签(视频)。:)但是我不明白您在其他评论中所说的...
Paul

1
@rwong它不是一个班轮吗?无论直接加载字体还是通过ResourceManager加载字体,您都只需要一行。如果用户抱怨,是什么阻止了我重新实施RM?
保罗

Answers:


7

我认为该代码没有错,它以合理且易于维护的方式完成了您需要的代码。

但是,这段代码的问题是,如果您希望它执行其他任何操作,则必须全部更改

SRP的要点是,如果您具有执行算法A()的单个组件“ CompA”,并且需要更改算法A(),则也不必更改“ CompB”。

我的C ++技巧太生锈了,无法建议您需要更改字体管理解决方案的良好情况,但是我通常会想到在缓存层中滑动的想法。理想情况下,您不希望加载的东西知道它的来源,也不希望加载的东西关心它的来源,因为那样进行更改会更简单。一切都与可维护性有关。

一个示例可能是您从第三种来源(例如,字符图片图像)加载字体。为了实现此目的,您将需要更改您的加载器(如果前两个失败,则调用第三个方法),并更改Font类本身以实现此第三个调用。理想情况下,您只需要创建另一个工厂(SpriteFontFactory或其他任何工厂),实现相同的loadFont(...)方法,并将其粘贴在可用于加载字体的工厂列表中即可。


1
嗯,我明白了:如果我添加了一种加载字体的方法,则需要向管理器添加一个获取功能,并向资源添加一个加载功能。确实,这是一个缺点。但是,取决于此新来源可能是什么,您可能必须以不同的方式处理数据(TTF是一回事,字体精灵是另一回事),因此您无法真正预测某个设计的灵活性。我确实明白你的意思。
保罗

是的,就像我说的那样,我的C ++技能非常生锈,因此我很难给出一个可行的论证方法,我同意灵活性这一点。就像我说的那样,这确实取决于您要使用的代码,我认为您的原始代码完全可以解决问题。
Ed James

好的问题和好的答案,最好的是,多个开发人员可以从中学到东西。这就是为什么我喜欢在这里闲逛:)。哦,所以我的评论并非完全多余,SRP可能会有些棘手,因为您必须问自己“如果”,这似乎与“过早的优化是万恶之源”或“ YAGNI的哲学。从来没有黑白答案!
Martijn Verburg

0

让我烦恼的是您的类loadFromMemoryloadFromFile方法。理想情况下,您应该只有loadFromMemory方法;字体不关心内存中的数据如何变化。另一件事是,您应该使用构造函数/析构函数代替加载和free方法。因此,loadFromMemory将成为,Font(const void *buf, int len)并且free()将成为~Font()


可以从两个构造函数访问加载函数,并在析构函数中调用free-我只是在这里没有显示。我发现能够直接从文件中加载字体很方便,而不是先打开文件,将数据写入缓冲区,然后将其传递给Font。不过,有时我还需要从缓冲区加载,这就是为什么我同时使用这两种方法。
保罗
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.