Я роблю свої заняття занадто детальними? Як слід застосовувати Принцип єдиної відповідальності?


9

Я пишу багато коду, який включає три основні етапи.

  1. Отримайте дані звідкись.
  2. Перетворіть ці дані.
  3. Покладіть ці дані кудись.

Як правило, я використовую три типи занять - надихнувшись їх відповідними моделями дизайну.

  1. Фабрики - для побудови об'єкта з якогось ресурсу.
  2. Посередники - користуватися фабрикою, виконувати перетворення, потім використовувати командира.
  3. Командири - розмістити ці дані десь в іншому місці.

Мої заняття, як правило, мені досить малі, часто це єдиний (загальнодоступний) метод, наприклад отримати дані, перетворити дані, зробити роботу, зберегти дані. Це призводить до поширення класів, але загалом працює добре.

Там, де я борюся, коли я приходжу до тестування, я, в кінцевому підсумку, буду щільно поєднати тести. Наприклад;

  • Заводська - читає файли з диска.
  • Commander - записує файли на диск.

Я не можу перевірити одне без іншого. Я можу написати додатковий тестовий код для читання / запису диска також, але тоді я повторююсь.

Дивлячись на .Net, клас File застосовує інший підхід, він поєднує в собі обов'язки (мого) заводу та командира разом. Він має функції для створення, видалення, існування та читання всіх в одному місці.

Чи слід дивитись, щоб наслідувати приклад .Net та поєднувати - особливо, коли ми маємо справу із зовнішніми ресурсами - мої заняття разом? Код, який він все-таки поєднав, але це більш навмисно - це відбувається при початковій реалізації, а не в тестах.

Чи моє тут питання про те, що я застосував Принцип єдиної відповідальності дещо надмірно? У мене є окремі класи, які відповідають за читання та письмо. Коли я міг би мати комбінований клас, який відповідає за роботу з певним ресурсом, наприклад системним диском.



6
Looking at .Net, the File class takes a different approach, it combines the responsibilities (of my) factory and commander together. It has functions for Create, Delete, Exists, and Read all in one place.- Зауважте, що ви пов'язуєте "відповідальність" із "річчю, що потрібно робити". Відповідальність більше схожа на "сферу занепокоєння". Відповідальність класу File - виконання файлових операцій.
Роберт Харві

1
Мені здається, ти в хорошій формі. Все, що вам потрібно, це тестовий посередник (або один для кожного типу конверсій, якщо вам це більше подобається). Тестовий посередник може читати файли, щоб перевірити їх правильність, використовуючи .net-клас Файл. З цим не виникає жодних проблем з точки зору SOLID.
Мартін Мейт

1
Як згадував @Robert Harvey, SRP має шалене ім'я, оскільки це насправді не відповідальність. Йдеться про "інкапсуляцію та абстрагування єдиної складної / важкої сфери, яка може змінитись". Я думаю, STDACMC був занадто довгим. :-) Тож, я вважаю, що ваш поділ на три частини здається розумним.
user949300

1
Важливим моментом вашої Fileбібліотеки від C # є те, що, наскільки ми знаємо, Fileклас може бути просто фасадом, розміщуючи всі файлові операції в одному місці - в клас, але можна внутрішньо використовувати аналогічні ваші класи читання / запису, які б насправді містять складнішу логіку обробки файлів. Такий клас (the File) все ще дотримуватиметься SRP, оскільки процес фактичної роботи з файловою системою буде абстраговано за іншим шаром - швидше за все, з об'єднавчим інтерфейсом. Не кажучи, що це так, але це могло бути. :)
Енді

Відповіді:


5

Дотримуючись принципу єдиної відповідальності, можливо, саме ви тут орієнтувались, але там, де ви є інше ім’я.

Розділення відповідальності за запит команд

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

Ви висловили стурбованість тестуванням. Я не думаю, що наступний CQRS не виключає написання тестового коду. Можливо, ви просто слідкуєте за CQRS таким чином, щоб ваш код не був перевіреним.

Це допомагає знати, як використовувати поліморфізм для інвертування залежностей від вихідного коду без необхідності змінювати потік управління. Я не дуже впевнений, де набір тестів на письмові тести.

Слово обережності, дотримуючись звичок, які ви знайдете в бібліотеках, не є оптимальним. Бібліотеки мають свої потреби і відверто старі. Тож навіть найкращий приклад - лише найкращий приклад з того часу.

Це не означає, що не існує абсолютно достовірних прикладів, які не відповідають CQRS. Слідом за цим завжди буде біль. Це не завжди варто платити. Але якщо вам це потрібно, ви будете раді, що ним скористалися.

Якщо ви все-таки користуєтесь цим, прислухайтеся до цього слова попередження:

Зокрема, CQRS слід використовувати лише на певних частинах системи (BoundedContext в DDD lingo), а не в цілому. У такому способі мислення кожен обмежений контекст потребує власних рішень щодо його моделювання.

Мартін Flowler: CQRS


Цікаво раніше не бачили CQRS. Код перевіряється, це більше стосується спроби знайти кращий спосіб. Я використовую знущання та ін'єкцію залежності, коли можу (на мою думку, це ви маєте на увазі).
Джеймс Вуд

Перший раз, читаючи про це, я виявив щось подібне через свою програму: обробляти гнучкі пошуки, безглузді поля, які можна фільтрувати / сортувати, (Java / JPA) - це головний біль і призводить до тонн кодового коду, якщо ви не створите базову пошукову систему, буде обробляти цей матеріал за вас (я використовую rsql-jpa). Хоча у мене однакова модель (скажімо, однакові об'єкти JPA для обох), пошук витягується на спеціальній загальній службі, і шар моделі більше не повинен обробляти її.
Вальфрат

3

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

Скажімо, ви зберігаєте дані програми у файлі XML. Які фактори можуть спричинити зміну коду, пов’язаного з читанням чи письмом? Деякі можливості:

  • Модель даних програми може змінюватися, коли до неї додаються нові функції.
  • До моделі можуть бути додані нові види даних - наприклад, зображення
  • Формат зберігання може бути змінений незалежно від логіки програми: Скажіть, від XML до JSON або до двійкового формату, через проблеми з сумісністю або продуктивністю.

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

Але давайте уявимо інший сценарій: Ви додаток є частиною конвеєра обробки даних. Він читає деякі файли CSV, генеровані окремою системою, виконує певний аналіз та обробку, а потім виводить інший файл, який обробляється третьою системою. У цьому випадку читання та письмо - це самостійні обов'язки, і їх слід відокремити.

Підсумок: Загалом, ви не можете сказати, що читання та запис файлів є окремими обов'язками, це залежить від ролі програми. Але виходячи з вашої підказки щодо тестування, я б припустив, що це відповідальність у вашому випадку.


2

Як правило, ви маєте правильну ідею.

Отримайте дані звідкись. Перетворіть ці дані. Покладіть ці дані кудись.

Здається, у вас є три обов'язки. ІМО "Посередник" може зробити багато. Я думаю, ви повинні почати з моделювання своїх трьох обов'язків:

interface Reader[T] {
    def read(): T
}

interface Transformer[T, U] {
    def transform(t: T): U
}

interface Writer[T] {
    def write(t: T): void
}

Тоді програма може бути виражена як:

def program[T, U](reader: Reader[T], 
                  transformer: Transformer[T, U], 
                  writer: Writer[U]): void =
    writer.write(transformer.transform(reader.read()))

Це призводить до поширення класів

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

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

Кожен шматок повинен бути незалежно перевіряється. Зображений вище, ви можете представляти читання / запис у файл у вигляді:

class FileReader(fileName: String) implements Reader[String] {
    override read(): String = // read file into string
}

class FileWriter(fileName: String) implements Writer[String] {
    override write(str: String) = // write str to file
}

Ви можете написати інтеграційні тести для тестування цих класів, щоб перевірити їх читання та запис у файлову систему. Решту логіки можна записати як перетворення. Наприклад, якщо файли формату JSON, ви можете перетворити Strings.

class JsonParser implements Transformer[String, Json] {
    override transform(str: String): Json = // parse as json
}

Потім ви можете перетворитись на належні об'єкти:

class FooParser implements Transformer[Json, Foo] {
    override transform(json: Json): Foo = // ...
}

Кожен з них незалежно перевіряється. Ви можете також модульне тестування programвище, насміхаючись reader, transformerі writer.


Це майже там, де я зараз. Я можу перевірити кожну функцію окремо, проте, випробовуючи їх, вони стають сполученими. Напр. Для тестування FileWriter, тоді ще щось має прочитати те, що було написано, очевидним рішенням є використання FileReader. Fwiw, посередник часто робить щось інше, як застосувати ділову логіку або, можливо, представлений основною програмою Main function.
Джеймс Вуд

1
@JamesWood, що часто трапляється з тестами на інтеграцію. Однак вам не доведеться поєднувати класи в тесті. Ви можете перевірити FileWriter, прочитавши безпосередньо з файлової системи замість використання FileReader. Адже саме від вас залежить, які ваші цілі випробуєте. Якщо ви користуєтесь FileReader, тест порушиться, якщо FileReaderабо FileWriterпорушено - що може зайняти більше часу для налагодження.
Самуїл

Також дивіться stackoverflow.com/questions/1087351/… це може допомогти зробити ваші тести приємнішими
Самуїл

Це майже де я зараз - це не на 100% правда. Ви сказали, що використовуєте шаблон «Посередник». Я думаю, що це тут не корисно; ця схема використовується, коли у вас багато різних об'єктів, що взаємодіють один з одним в дуже заплутаному потоці; ви поміщаєте туди посередника, щоб полегшити всі відносини та реалізувати їх в одному місці. Здається, це не ваша справа; у вас маленькі одиниці дуже чітко визначені. Також, як і коментар, поданий вище від @Samuel, вам слід перевірити один пристрій і робити свої твердження, не викликаючи інших підрозділів
Emerson Cardoso

@EmersonCardoso; Я дещо спростив сценарій у своєму питанні. Хоча деякі з моїх посередників досить прості, інші складніші і часто використовують декілька заводів / командирів. Я намагаюся уникати деталей одного сценарію, мене більше цікавить архітектура дизайну вищого рівня, яка може бути застосована до декількох сценаріїв.
Джеймс Вуд

2

Я в кінцевому підсумку буде щільно поєднані тести. Наприклад;

  • Заводська - читає файли з диска.
  • Commander - записує файли на диск.

Тож основний акцент тут робиться на тому, що їх з’єднує . Чи передаєте ви об'єкт між двома (наприклад, а File?), То це файл, з яким вони поєднані, а не один з одним.

З того, що ви сказали, ви розділили заняття. Пастка полягає в тому, що ви випробовуєте їх разом, тому що це простіше або "має сенс" .

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

Фактична частина, яку ви тестуєте, - Factoryце: «чи читатиме цей файл правильно і виводить правильно»? Тому знущайтеся над файлом, перш ніж прочитати його в тесті .

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


У цьому конкретному прикладі річ, яка з'єднує їх, - це ресурс - наприклад, системний диск. Інакше взаємодії між двома класами немає.
Джеймс Вуд

1

Отримайте дані звідкись. Перетворіть ці дані. Покладіть ці дані кудись.

Це типовий процедурний підхід, про який Девід Парнас писав ще в 1972 році. Ви зосереджуєтесь на тому, як все йде на думку. Ви приймаєте конкретне рішення вашої проблеми як зразок вищого рівня, що завжди неправильно.

Якщо ви будете дотримуватися об'єктно-орієнтованого підходу, я краще зосередиться на вашому домені . Про що це все? Які основні обов'язки вашої системи? Які основні поняття представлені мовою експертів вашого домену? Отже, зрозумійте свій домен, розкладіть його, розглядайте сфери відповідальності вищого рівня як ваші модулі , трактуйте поняття нижчого рівня, представлені як іменники як ваші об’єкти. Ось приклад, який я подав до недавнього питання, він дуже актуальний.

І є очевидна проблема з згуртованістю, ви це самі згадали. Якщо ви зробите деякі зміни, це вхідна логіка і випишіть на неї тести, це жодним чином не доводить, що ваша функціональність працює, оскільки ви можете забути передати ці дані на наступний рівень. Дивіться, ці шари внутрішньо пов'язані. А штучна розв'язка робить ще гірше. Я це знаю: проект 7 років із 100 чоловіковими роками за моїми плечима повністю написаний у цьому стилі. Біжіть від нього, якщо можете.

І в цілому справа СРП. Вся справа в згуртованості, застосованій до вашого проблемного простору, тобто домену. Це основний принцип СРП. Це призводить до того, що об’єкти розумні і реалізують свої обов'язки для себе. Ніхто їх не контролює, ніхто не надає їм даних. Вони поєднують дані та поведінку, викриваючи лише останні. Отже, ваші об'єкти поєднують як перевірку необроблених даних, трансформацію даних (тобто поведінку), так і постійність. Це може виглядати наступним чином:

class FinanceTransaction
{
    private $id;
    private $storage;

    public function __construct(UUID $id, DataStorage $storage)
    {
        $this->id = $id;
        $this->storage = $storage;
    }

    public function perform(
        Order $order,
        Customer $customer,
        Merchant $merchant
    )
    {
        if ($order->isExpired()) {
            throw new Exception('Order expired');
        }

        if ($customer->canNotPurchase($order)) {
            throw new Exception('It is not legal to purchase this kind of stuff by this customer');
        }

        $this->storage->save($this->id, $order, $customer, $merchant);
    }
}

(new FinanceTransaction())
    ->perform(
        new Order(
            new Product(
                $_POST['product_id']
            ),
            new Card(
                new CardNumber(
                    $_POST['card_number'],
                    $_POST['cvv'],
                    $_POST['expires_at']
                )
            )
        ),
        new Customer(
            new Name(
                $_POST['customer_name']
            ),
            new Age(
                $_POST['age']
            )
        ),
        new Merchant(
            new MerchantId($_POST['merchant_id'])
        )
    )
;

В результаті існує досить багато згуртованих класів, які представляють певну функціональність. Зауважте, що перевірка, як правило, стосується об'єктів value - принаймні в DDD- підході.


1

Там, де я борюся, коли я приходжу до тестування, я, в кінцевому підсумку, буду щільно поєднати тести. Наприклад;

  • Заводська - читає файли з диска.
  • Commander - записує файли на диск.

Слідкуйте за протікаючими абстракціями під час роботи з файловою системою - я занадто часто бачив це занедбаним виглядом, і він має описані вами симптоми.

Якщо клас оперує даними, які надходять з / входять у ці файли, то файлова система стає детальною інформацією про реалізацію (I / O) і повинна бути відокремлена від неї. Ці класи (завод / командир / посередник) не повинні знати про файлову систему, якщо їх єдиним завданням є зберігання / зчитування наданих даних. Класи, які мають справу з файловою системою, повинні інкапсулювати конкретні параметри контексту, такі як шляхи (можуть бути передані через конструктор), так що інтерфейс не виявив його природи (слово "Файл" в назві інтерфейсу - запах більшість часу).


"Ці класи (завод / командир / посередник) не повинні знати про файлову систему, якщо їх єдиним завданням є зберігання / зчитування наданих даних." У цьому конкретному прикладі це все, що вони роблять.
Джеймс Вуд

0

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

Щоб зробити крок далі, слід створити інтерфейси для класів Factory, Mediator та Commander. Тоді ви можете використовувати глумлені версії цих класів під час написання одиничних тестів для конкретних реалізацій інших. За допомогою макетів ви можете перевірити, що методи викликаються у правильному порядку та з правильними параметрами, і що тестовий код поводиться правильно з різними значеннями повернення.

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


1
ЯГНІ - це те, про що слід подумати.
whatsisname
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.