Однозначна відповідальність та користувацькі типи даних


10

Протягом останніх місяців я просив, щоб люди, які перебувають тут, на SE та інших сайтах, пропонували мені конструктивну критику щодо мого коду. Є одне, що постійно вискакує майже кожен раз, і я все ще не згоден з цією рекомендацією; : P Я хотів би це обговорити тут, і, можливо, мені все стане зрозумілішим.

Це стосується принципу єдиної відповідальності (СРП). В основному, у мене клас даних Font, який не тільки містить функції для маніпулювання даними, але і для його завантаження. Мені кажуть, що обидві повинні бути окремими, що функції завантаження повинні бути розміщені всередині заводського класу; Я думаю, що це неправильне тлумачення СРП ...

Фрагмент з мого класу шрифтів

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

Запропонований дизайн нібито відповідає СРП, але я не згоден - я думаю, що це заходить занадто далеко. FontКлас більше не є самодостатнім (це марно без заводу), і FontFactoryповинен знати подробиці про реалізацію ресурсу, який, ймовірно , зроблений через дружбу або громадських добувач, які в подальшому викрити реалізацію Font. Я думаю, що це скоріше випадок фрагментарної відповідальності .

Ось чому я вважаю, що мій підхід кращий:

  • Fontє самодостатньою - Будучи самодостатньою, її легше зрозуміти та підтримувати. Крім того, ви можете використовувати клас, не включаючи нічого іншого. Якщо, однак, вам здається, що вам потрібно більш складне управління ресурсами (фабрикою), ви також можете це легко зробити (пізніше я розповім про свою власну фабрику, ResourceManager<Font>).

  • Дотримується стандартної бібліотеки - я вважаю, що визначені користувачем типи повинні намагатися максимально скопіювати поведінку стандартних типів на відповідній мові. Це std::fstreamсамодостатнє і забезпечує такі функції, як openі close. Дотримуватися стандартної бібліотеки означає, що не потрібно витрачати зусиль на вивчення ще одного способу виконання справ. Крім того, загалом кажучи, стандартний комітет 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...
}

Нижня лінія...

(Тут хотілося б поставити тл; д-р, але я не можу придумати його.: \)
Ну, ось у вас це є, я зробив свою справу якнайкраще. Будь ласка, опублікуйте будь-які зустрічні аргументи, а також будь-які переваги, на які, на вашу думку, запропонована конструкція має над моїм власним дизайном. В основному, спробуйте показати мені, що я помиляюся. :)


2
Нагадує мені про ActiveRecord Мартіна Фаулера проти DataMapper .
Користувач

Забезпечте зручність (ваш сучасний дизайн) у самому зовнішньому інтерфейсі, орієнтованому на користувача. Використовуйте SRP внутрішньо, щоб це полегшило ваші подальші зміни в реалізації. Я можу подумати про прикраси навантажувачів шрифту, які пропускають курсив і жирний шрифт; що завантажує тільки BMP Unicode тощо.
rwong


@rwong Я знаю про цю презентацію, у мене була закладка до неї ( відео ). :) Але я не розумію, що ви говорите в своєму іншому коментарі ...
Пол,

1
@rwong Це вже не один лайнер? Вам потрібен лише один рядок, незалежно від того, завантажуєте Ви шрифт безпосередньо або через ResourceManager. І що мене заважає повторно виконувати RM, якщо користувачі скаржаться?
Пол

Відповіді:


7

З цим кодом немає нічого поганого, він робить те, що вам потрібно, у розумному та досить простому у обслуговуванні способі.

Однак проблема з цим кодом полягає в тому, що якщо ви хочете, щоб він робив щось інше, вам доведеться все це змінити .

Сенс SRP полягає в тому, що якщо у вас є один компонент "CompA", який робить алгоритм A (), і вам потрібно змінити алгоритм A (), вам також не доведеться змінювати "CompB".

Мої навички C ++ занадто іржаві, щоб запропонувати гідний сценарій, коли вам потрібно буде змінити рішення для управління шрифтом, але звичайний випадок, який я роблю, - це ідея ковзання в шарі кешування. В ідеалі ви не хочете, щоб річ, що завантажує речі, знала про те, звідки вона походить, а також не потрібно, щоб вона завантажувалась, звідки вона походить, адже тоді внесення змін простіше. Вся справа в ремонтопридатності.

Одним із прикладів може бути завантаження шрифту з третього джерела (скажімо образ спрайту символів). Для цього вам потрібно буде змінити свій завантажувач (викликати третій метод, якщо перші два виходять з ладу) і сам клас Font, щоб реалізувати цей третій виклик. В ідеалі ви просто зробите іншу фабрику (SpriteFontFactory чи будь-яку іншу), застосуйте той же метод loadFont (...) і вставте його в список заводів десь, які можна використовувати для завантаження шрифту.


1
Ах, я бачу: якщо я додаю ще один спосіб завантаження шрифту, мені потрібно додати ще одну функцію придбання до менеджера та ще одну функцію завантаження до ресурсу. Дійсно, це один недолік. Однак, залежно від того, яким може бути цей новий джерело, вам, ймовірно, доведеться по-різному обробляти дані (TTF - це одне, шрифти - інше), тому ви не можете реально передбачити, наскільки гнучким буде певний дизайн. Я бачу вашу думку, хоча.
Пол

Так, як я вже говорив, мої навички C ++ досить іржаві, тому я намагався придумати життєздатну демонстрацію проблеми, я згоден щодо гнучкості. Це дійсно залежить від того, що ви збираєтеся зі своїм кодом, як я вже сказав, я вважаю, що ваш оригінальний код є цілком розумним рішенням проблеми.
Ед Джеймс

Чудове запитання та чудова відповідь, і найкраще, що багато розробників можуть навчитися на цьому. Ось чому я люблю тусуватися тут :). О, і тому мій коментар не зовсім зайвий, SRP може бути трохи хитрим, тому що вам доведеться запитати себе "що робити", що може здатися протилежним: "передчасна оптимізація - корінь всього зла" або " Філософії ЯГНІ. Чорно-білої відповіді ніколи не буває!
Martijn Verburg

0

Одне, що мене клопоче про ваш клас, - це те, що ти маєш loadFromMemoryі loadFromFileметоди. В ідеалі ви повинні мати лише loadFromMemoryметод; шрифт не повинен байдуже, як з’явилися дані в пам'яті. Інша справа, що вам слід використовувати конструктор / деструктор замість навантаження та freeметодів. Таким чином, loadFromMemoryстав би Font(const void *buf, int len)і free()став ~Font().


Функції навантаження доступні від двох конструкторів, а у деструкторі викликається вільна - я просто цього не показав. Мені здається, що можна завантажувати шрифт безпосередньо з файлу, а не спочатку відкривати файл, записувати дані в буфер, а потім передавати їх у шрифт. Іноді мені також потрібно завантажити з буфера, тому я маю обидва методи.
Павло
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.