Керування та організація масово збільшеної кількості занять після переходу на SOLID?


49

Протягом останніх декількох років ми поступово переходимо до прогресивно вдосконаленого письмового коду, кілька кроків дитини одночасно. Ми нарешті починаємо переходити на щось, що принаймні нагадує СОЛІД, але ми ще не зовсім там. З моменту переходу, одна з найбільших скарг розробників полягає в тому, що вони не витримують експертного огляду та проходження десятків і десятків файлів, де раніше кожна задача вимагала, щоб розробник торкнувся 5-10 файлів.

Перш ніж почати перемикання, наша архітектура була організована приблизно так: (надано, на один-два порядки більше файлів):

Solution
- Business
-- AccountLogic
-- DocumentLogic
-- UsersLogic
- Entities (Database entities)
- Models (Domain Models)
- Repositories
-- AccountRepo
-- DocumentRepo
-- UserRepo
- ViewModels
-- AccountViewModel
-- DocumentViewModel
-- UserViewModel
- UI

Файл мудрий, все було неймовірно лінійно і компактно. Очевидно, було багато дублювання коду, жорсткого зчеплення та головного болю, проте кожен міг його обминути і зрозуміти. Повні початківці, люди, які ніколи не так багато, як відкривали Visual Studio, змогли зрозуміти це всього за кілька тижнів. Відсутність загальної складності файлів робить його порівняно простим для початківців розробників, а нові наймачі починають також надавати внесок, не надто багато часу на збільшення. Але це майже все, де будь-які переваги стилю коду виходять у вікно.

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

  • Тестові одиниці
  • Кількість класів
  • Складність експертного огляду

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

Кількість класів - це, мабуть, найбільша перешкода для подолання. Завдання, які раніше брали 5-10 файлів, тепер можуть займати 70-100! Незважаючи на те, що кожен із цих файлів служить виразною метою, чистий обсяг файлів може бути величезним. Відповідь команди в основному були стогонами та чуханням голови. Раніше завдання може потребувати одного або двох сховищ, моделі або двох, логічного рівня та методу контролера.

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

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


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

Щоб зберегти документ на мережевому диску, мені було потрібно кілька конкретних класів:

- IBasePathProvider 
-- string GetBasePath() // returns the network path to store files
-- string GetPatientFolderName() // gets the name of the folder where patient files are stored
- BasePathProvider // provides an implementation of IBasePathProvider
- BasePathProviderTests // ensures we're getting what we expect

- IUniqueFilenameProvider
-- string GetFilename(string path, string fileType);
- UniqueFilenameProvider // performs some filesystem lookups to get a unique filename
- UniqueFilenameProviderTests

- INewGuidProvider // allows me to inject guids to simulate collisions during unit tests
-- Guid NewGuid()
- NewGuidProvider 
- NewGuidProviderTests

- IFileExtensionCombiner // requests may come in a variety of ways, need to ensure extensions are properly appended.
- FileExtensionCombiner
- FileExtensionCombinerTests

- IPatientFileWriter
-- Task SaveFileAsync(string path, byte[] file, string fileType)
-- Task SaveFileAsync(FilePushRequest request) 
- PatientFileWriter
- PatientFileWriterTests

Це загалом 15 класів (за винятком POCO та ешафотів), щоб виконати досить просте збереження. Ця кількість значно видозмінилася, коли мені потрібно було створити POCO для представлення сутностей у кількох системах, створив кілька репостів для спілкування із сторонніми системами, несумісними з іншими нашими ОРМ, та побудував логічні методи для управління тонкощами певних операцій.


52
"Завдання, які раніше брали 5-10 файлів, тепер можуть займати 70-100!" Як у біса? Це жодним чином не нормально. Які зміни ви вносите, які потребують зміни стількох файлів ??
Ейфорія

43
Те, що вам потрібно змінити більше файлів за завдання (значно більше!), Означає, що ви неправильно робите SOLID. Вся справа в тому, щоб організувати свій код (з часом) таким чином, щоб він відображав спостережувані шаблони змін, роблячи зміни простішими. Кожен принцип у SOLID має певні міркування (коли і навіщо його застосовувати); схоже, ви потрапили в цю ситуацію, застосувавши їх наосліп. Те ж саме з одиничним тестуванням (TDD); якщо ви це робите, не розуміючи, як робити дизайн / архітектуру, ви збираєтеся копати себе в норі.
Філіп Мілованович

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

25
@Euphoric: Проблема може виникати двома способами. Я підозрюю, що ви відповідаєте на те, що 70-100 класів є надмірними. Але це не неможливо, що це просто масштабний проект, який був забитий на 5-10 файлів (я працював у файлах 20KLOC раніше ...), а 70-100 - це фактично потрібна кількість файлів.
Flater

18
Існує розлад думки, який я називаю "хворобою об'єктного щастя", це переконання, що методи ОО - це самоціль, а не лише одна з багатьох можливих методик зменшення витрат на роботу у великій кодовій базі. У вас особливо просунута форма "ХВОРИЙ щастя". SOLID - не мета. Мета - зниження витрат на підтримку бази даних. Оцініть свої пропозиції в цьому контексті, а не в тому, чи це доктрина SOLID. (Те, що ваші пропозиції також, мабуть, насправді не є доктриною SOLID - також хороший момент для розгляду.)
Ерік Ліпперт

Відповіді:


104

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

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

Клас на те, щоб абстрактно відібратись, DateTime.Nowзвучить добре. Але вам потрібна лише одна з них, і вона може бути об'єднана з іншими особливостями навколишнього середовища в єдиний клас з відповідальністю за абстрагування екологічних особливостей. Знову одна відповідальність з декількома підвідповідальністю.

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

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

Це говорить про те, що ви також неправильно зрозуміли одиничні тести. "Одиниця" одиничного тесту не є одиницею коду. Що навіть є одиницею коду? Клас? Метод? Змінна? Єдина інструкція про машину? Ні, "одиниця" відноситься до одиниці ізоляції, тобто коду, який може виконуватися ізольовано від інших частин коду. Простий тест на те, чи є автоматизований тест одиничним тестом чи ні, чи можна його запускати паралельно з усіма іншими тестами, не впливаючи на його результат. Є ще кілька правил, що стосуються тестів, але це ваш ключовий захід.

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

Завжди будьте прагматичними і пам’ятайте, що все є компромісом. Чим більше ви дотримуєтесь DRY, тим більш щільно зв'язаний код повинен стати. Чим більше ви вводите абстракції, тим простіше перевірити код, але складніше це зрозуміти. Уникайте ідеології та знайдіть хороший баланс між ідеалом та зберігаючи його просто. Тут лежить солодке місце максимальної ефективності як для розробки, так і для обслуговування.


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

8
Знову "Завжди будь прагматичним і пам’ятай, що все є компромісом" : учні дядька Боба для цього не відомі (незалежно від початкового наміру).
Пітер Мортенсен

13
Підводячи підсумок першої частини, у вас зазвичай є кава-стажер, а не повний набір плагін-перколятора, фліп-перемикач, чек-цукор-потреби-заправка, відкритий холодильник, вихід-молоко, отримайте - ложки, чашки для спуску, наливання кави, цукру, додавання молока, чаша для перемішування та стажисти. ; P
Джастін Час 2 Відновіть Моніку

12
Першопричиною проблеми ОП, здається, є нерозуміння різниці між функціями, які повинні виконувати одне завдання, і класами, які повинні нести
alephzero

6
"Правила призначені для настанови мудреців та покірності дурнів". - Дуглас Бадер
Каланус

29

Завдання, які раніше брали 5-10 файлів, тепер можуть займати 70-100!

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

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

Боб Мартін, який спочатку сформулював СРП, кілька років тому написав допис у блозі, щоб спробувати вияснити ситуацію. Він досить довго обговорює, що таке "причина для змін" для цілей СРП. Про це варто прочитати в повному обсязі, але серед речей, що заслуговують на особливу увагу, є така альтернативна формулювання СРП:

Зберіть разом речі, які змінюються з тих же причин . Відокремте ті речі, які змінюються з різних причин.

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

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

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


1
Я здогадуюсь, я просто намагаюся з'ясувати, де знаходиться лінія. У недавньому завданні мені довелося виконати досить просту операцію, але це було в кодовій базі без особливих існуючих лісів або функціональних можливостей. Отже, все, що мені потрібно було зробити, було дуже простим, але все досить унікальним і, здається, не вміщувалося в загальних класах. У моєму випадку мені потрібно було зберегти документ на мережевому диску та записати його у дві окремі таблиці бази даних. Правила навколо кожного кроку були досить особливими. Навіть у генерації імен файлів (проста інструкція) було кілька класів, щоб зробити тестування більш зручним.
JD Davis

3
Знову ж таки, @JDDavis, вибираючи кілька класів над одним, виключно для цілей встановлення, це поставити візок перед конем, і він йде прямо проти SRP, який закликає групувати згуртовані функції разом. Я не можу порадити вам детальну інформацію, але проблема того, що окремі функціональні зміни потребують модифікації багатьох файлів, - це проблема, яку ви повинні вирішити (і намагатися уникнути), а не проблема, яку ви повинні намагатися виправдати.
Джон Боллінгер

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

12

Завдання, які раніше брали 5-10 файлів, тепер можуть займати 70-100!

Це брехня. Завдання ніколи не займали лише 5-10 файлів.

Ви не вирішуєте жодних завдань із меншими 10 файлами. Чому? Тому що ви використовуєте C #. C # - мова високого рівня. Ви використовуєте більше 10 файлів просто для створення привітного світу.

О, звичайно, ви їх не помічаєте, оскільки ви їх не написали. Отже, ви не дивитесь на них. Ви їм довіряєте.

Проблема не в кількості файлів. Це те, що у вас зараз так багато відбувається, що ви не довіряєте.

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

Для невеликих та середніх програм SOLID - це дуже простий продаж. Всі бачать вигоду та простоту ремонту. Однак вони просто не бачать хорошої пропозиції для SOLID у дуже масштабних програмах.

Зміни важко в дуже масштабних програмах, не маючи того, що ви робите. Найкраща мудрість, яку можна застосувати тут, не приходить від дядька Боба. Це походить від Майкла Пір'я в його книзі «Ефективна робота зі спадковим кодексом».

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

Замість цього знайдіть способи зробити перевіряти свій старий нестабільний код (застарілий код у "Пір'я говорять"). У цьому метафорі коду, як сорочка. Великі деталі з'єднуються за допомогою природних швів, які можна відмінити, щоб розділити код так, як ви знімете шви. Зробіть це, щоб дозволити прикріпити тестові "рукави", які дозволять вам виділити решту коду. Тепер, коли ви створюєте пробні рукави, ви впевнені в рукавах, тому що це зробили з робочою сорочкою. (о, ця метафора починає боліти).

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

Що стосується будь-якого з цього:

Керування та організація масово збільшеної кількості занять після переходу на SOLID?

Абстракція.

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

Назвіть мені гарне ім’я, читабельні тести (приклади), які показують, як користуватися інтерфейсом та впорядкувати його, щоб я міг знайти речі, і мені не байдуже, чи використовували ми 10, 100 чи 1000 файлів.

Ви допомагаєте мені знайти речі з хорошими описовими іменами. Поставте речі з добрими іменами в речі з добрими іменами.

Якщо ви зробите все це правильно, ви будете абстрагувати файли, де закінчення завдання має вас лише залежно від 3 до 5 інших файлів. 70-100 файлів все ще є. Але вони ховаються за 3 до 5. Це працює лише в тому випадку, якщо ви довіряєте 3 до 5 робити це правильно.

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

@Delioth гарно ставиться до зростання болю. Коли ви звикли до того, що страви знаходяться в шафі над посудомийною машиною, потрібно трохи звикнути до того, що вони знаходяться над баром для сніданку. Робить деякі речі важче. Полегшує деякі речі. Але це викликає всілякі кошмари, якщо люди не згодні, куди їдуть страви. У великій базі кодів проблема полягає в тому, що ви можете переміщати лише деякі страви одночасно. Тож тепер у вас є посуд у двох місцях. Це заплутано. Важко повірити, що страви є там, де вони повинні бути. Якщо ви хочете пройти це, хоча єдине, що потрібно зробити, це продовжувати рухати посуд.

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

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

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


3
Можливо, варто згадати, що певна біль у питанні, ймовірно, просто наростає біль - хоча, так, їм може знадобитися зробити 15 файлів для цієї однієї речі ... тепер їм більше ніколи не потрібно писати GUIDProvider або BasePathProvider або ExtensionProvider і т. д. Це те саме перешкода, яке ви отримуєте, коли запускаєте новий проект "зеленого поля" - пучки підтримуючих функцій, які в основному є тривіальними, дурними для написання, але все ж їх потрібно записати. Досить побудувати їх, але коли вони там, вам не потрібно буде думати про них .... ніколи.
Деліот

@Delioth Я неймовірно схильний вважати, що це так. Раніше, якщо нам знадобився якийсь підмножина функціональності (скажімо, ми просто хотіли URL-адресу, розміщеного в AppSettings), у нас був просто один масивний клас, який передавались та використовувались. З новим підходом, немає ніяких причин обходити всю сукупність AppSettingsлише для отримання URL-адреси чи шляху до файлу.
JD Davis

1
Не запускайте фест перепису. Старий код являє собою важко здобуті знання. Викидання цього через те, що він має проблеми і не виражається в новій і вдосконаленій парадигмі X, просто вимагає нового набору проблем і не має важких здобутих знань. Це. Абсолютно.
Flot2011

10

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

Зміни коду мають становити 5-10 файлів, якщо ви не робите кодемоду чи масштабний рефакторинг. Якщо одна зміна торкається багатьох файлів, це, ймовірно, означає, що ваші зміни каскаду. Деякі вдосконалені абстракції (більше одноосібної відповідальності, розділення інтерфейсів, інверсія залежності) повинні допомогти. Можливо також, що, можливо, ви взяли на себе занадто однозначну відповідальність і могли б використовувати трохи більше прагматизму - коротші та тонші ієрархії. Це має полегшити розуміння коду, оскільки вам не доведеться розуміти десятки файлів, щоб знати, що робить код.

Це також може бути ознакою того, що ваша робота занадто велика. Замість "ей, додай цю функцію" (яка вимагає змін у інтерфейсі та api змін, а також зміни доступу до даних, зміни безпеки та тестування змін та ...) розбийте її на більш справні шматки. Це стає простішим для перегляду та простішим для розуміння, оскільки це вимагає від вас встановити гідні контракти між бітами.

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


2
5-10 файлів на 70-100 файлів трохи більше, ніж гіпотетичні. Моє останнє завдання було створити певну функціональність в одній з наших нових мікросервісів. Нова служба повинна була отримати запит і зберегти документ. Роблячи це, мені були потрібні класи для представлення сутностей користувачів у двох окремих базах даних та репозиціях для кожної. Репост для подання інших таблиць, які мені потрібно було написати. Виділені класи для обробки перевірки файлових даних та створення імен. І список продовжується. Не кажучи вже про те, що кожен клас, який містив логіку, був представлений інтерфейсом, щоб його можна було знущатися з одиничних тестів.
JD Davis

1
Що стосується наших старих кодових баз, всі вони щільно з'єднані і неймовірно монолітні. При підході SOLID єдине з'єднання між класами було у випадку POCO, все інше передається через DI та інтерфейси.
JD Davis

3
@JDDavis - зачекайте, чому одна мікросервіс працює безпосередньо з декількома базами даних?
Теластин

1
Це був компроміс з нашим диспетчером розробників. Він масово віддає перевагу монолітному та процедурному програмному забезпеченню. Як такі, наші мікросервіси набагато більше макросу, ніж повинні бути. По мірі покращення нашої інфраструктури поволі все перейде у власні мікросервіси. На даний момент ми дещо дотримуємось душевного підходу для переміщення певної функціональності в мікросервіси. Оскільки багатьом службам потрібен доступ до певного ресурсу, ми переносимо їх також у свої мікросервіси.
JD Davis

4

Я хотів би розповісти про деякі згадані тут речі, але більше з точки зору того, де намальовані межі об'єкта. Якщо ви слідуєте чомусь подібному до дизайну, керованого доменом, то, можливо, ваші об'єкти представлятимуть аспекти вашого бізнесу. Customerі Order, наприклад, були б об'єкти. Тепер, якби я здогадувався, грунтуючись на назвах класів, які ви почали використовувати, у вашому AccountLogicкласі був код, який би виконувався для будь-якого облікового запису. Однак в OO кожен клас повинен мати контекст і тотожність. Ви не повинні отримувати Accountоб'єкт, а потім передавати його в AccountLogicклас і дозволити цьому класу внести зміни до Accountоб'єкта. Це те, що називається анемічною моделлю, і не представляє ОО дуже добре. Натомість вашAccountклас повинен мати поведінку, таку Account.Close()чи Account.UpdateEmail(), і ця поведінка впливатиме лише на цей екземпляр облікового запису.

Тепер, як обробляються такі форми поведінки, можна (і в багатьох випадках слід) перевантажувати залежність, представлену абстракціями (тобто інтерфейсами). Account.UpdateEmailнаприклад, можливо, потрібно оновити базу даних або файл або надіслати повідомлення службовій шині тощо. Це може змінитися в майбутньому. Таким чином, ваш Accountклас може мати залежність від, наприклад, від IEmailUpdate, який може бути одним із багатьох інтерфейсів, реалізованих AccountRepositoryоб'єктом. Ви не хочете передавати цілий IAccountRepositoryінтерфейс Accountоб'єкту, тому що він, ймовірно, зробить занадто багато, наприклад, пошук і пошук інших (будь-яких) облікових записів, до яких ви, можливо, не хочете, щоб Accountоб'єкт мав доступ, але навіть якщо він AccountRepositoryможе реалізувати обидва IAccountRepositoryі IEmailUpdateінтерфейси,AccountОб'єкт мав би доступ лише до малих порцій, які йому потрібні. Це допомагає вам підтримувати принцип розділення інтерфейсу .

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

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


2

Це загалом 15 класів (за винятком POCO та ешафотів), щоб виконати досить просте збереження.

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

  • BasePathProvider- IMHO потребує будь-якого нетривіального проекту, що працює з файлами. Тож я припускаю, що таке вже є, і ви можете використовувати його як є.
  • UniqueFilenameProvider - Звичайно, у вас це вже є, чи не так?
  • NewGuidProvider - Той самий випадок, якщо ви тільки не дивитесь на використання GUID.
  • FileExtensionCombiner - Той самий випадок.
  • PatientFileWriter - Гадаю, це основний клас для поточного завдання.

Для мене це виглядає добре: Вам потрібно написати один новий клас, якому потрібні чотири допоміжні класи. Усі чотири класи помічників звучать досить багаторазово, тому я б ставлю на облік, що вони вже десь у кодовій базі. В іншому випадку це або невдача (чи справді ви людина у вашій команді, щоб писати файли та використовувати GUIDs) або якась інша проблема.


Що стосується тестових класів, то обов’язково під час створення нового класу чи оновлення його слід перевірити. Отже, писати п’ять класів означає також писати п'ять тестових класів. Але це не ускладнює дизайн:

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

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


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

@JDDavis Я намагався написати щось інше, ніж інші відповіді (з якими я переважно згоден). Кожного разу, коли ви щось копіюєте та вставляєте, ви перешкоджаєте повторному використанню, оскільки замість того, щоб щось узагальнювати, ви створюєте ще один фрагмент коду, який не використовується повторно, що змусить вас копіювати та вставляти більше одного дня. ІМХО - це другий за величиною гріх, лише після сліпого дотримання правил. Вам потрібно знайти своє солодке місце, де дотримання правил робить вас більш продуктивними (особливо це стосується майбутніх змін), а час від часу їх трохи порушувати допомагає у випадках, коли зусилля не будуть недоречними. Це все відносно.
maaartinus

@JDDavis І все залежить від якості ваших інструментів. Приклад: Є люди, які стверджують, що DI є підприємливим і складним, в той час як я стверджую, що в основному це безкоштовно . +++Щодо порушення правил: мені потрібні чотири класи, де я можу вводити їх лише після капітального рефакторингу, що робить код більш потворним (принаймні для моїх очей), тому я вирішив зробити їх однотонними (кращим програмістом може знайти кращий спосіб, але я задоволений цим; кількість цих синглів не змінюється з віками).
maaartinus

Ця відповідь в значній мірі виражає те, про що я думав, коли ОП додав приклад до питання. @JDDavis Дозвольте додати, що ви можете зберегти деякий код / ​​класи котла, використовуючи функціональні інструменти для простих випадків. Наприклад, постачальник графічного інтерфейсу, наприклад, замість того, щоб запровадити новий інтерфейс, новий клас для цього, чому б просто не використовувати Func<Guid>для цього і ввести анонімний метод, як ()=>Guid.NewGuid()у конструктор? І не потрібно тестувати цю функцію .Net Framework, це те, що Microsoft зробив для вас. Загалом це допоможе вам заощадити 4 класи.
Док Браун

... і вам слід перевірити, чи інші спрощені вами випадки можна спростити таким же чином (напевно, не всі).
Док Браун

2

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

Ось де я підозрюю, що це йде з рейок:

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

Однією з переваг, що випливає з більшості принципів SOLID (безумовно, не єдина перевага), є те, що це спрощує написання одиничних тестів для нашого коду. Якщо клас залежить від абстракцій, ми можемо знущатися над абстракціями. Абстракції, відокремлені, легше знущатися. Якщо клас робить одне, він, ймовірно, має меншу складність, а значить, простіше пізнати та перевірити всі його можливі шляхи.

Якщо ваша команда не пише тестові одиниці, відбуваються дві пов’язані речі:

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

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

По-друге, написання одиничних тестів може допомогти вам зрозуміти, скільки абстракцій дійсно потрібен ваш код. Як я вже сказав, це не наука. Ми починаємо погано, вередуючи всюди, і стаємо кращими. Одиничні тести мають своєрідний спосіб доповнення SOLID. Звідки ви знаєте, коли вам потрібно додати абстракцію чи щось розбити? Іншими словами, як ти знаєш, коли ти "досить ТВОРИЙ?" Часто відповідь - коли ти щось не можеш перевірити.

Можливо, ваш код можна перевірити, не створюючи стільки крихітних абстракцій та класів. Але якщо ви не пишете тести, як це можна сказати? Як далеко ми йдемо? Ми можемо стати одержимими, коли розбиваємо речі все менше і менше. Це кроляча нора. Здатність писати тести для нашого коду допомагає нам побачити, коли ми досягли своєї мети, щоб ми могли перестати одержимо, рухатися далі та весело писати більше коду.

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

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

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

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

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

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