SOLID проти статичних методів


11

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

(А)

public class Product {
  ...
  public Collection<Review> getReviews() {...}
}

(B)

public class Review {
  ...
  static public Collection<Review> forProduct( Product product ) {...}
}

Подивившись на код, я вибрав би (A): він не статичний і йому не потрібен параметр. Однак я відчуваю, що (A) порушує Принцип єдиної відповідальності (SRP) та Принцип відкритого закриття (OCP), тоді як (B) не:

  • (SRP) Коли я хочу змінити спосіб збирання відгуків для продукту, я повинен змінити клас продукту. Але має бути лише одна причина, чому потрібно змінити клас продукту. І це точно не відгуки. Якщо я упакую кожну функцію, яка має щось спільне з продуктами в Продукті, вона незабаром буде забита.

  • (OCP) Я повинен змінити клас продукту, щоб розширити його за допомогою цієї функції. Я думаю, що це порушує частину принципу "Закрито для змін". Перш ніж я отримав запит замовника на реалізацію оглядів, я вважав Продукт готовим і "закрив" його.

Що важливіше: слідувати принципам SOLID або мати більш простий інтерфейс?

Або я взагалі щось не так роблю?

Результат

О, дякую за всі ваші чудові відповіді! Важко вибрати одну як офіційну відповідь.

Дозвольте підсумувати основні аргументи з відповідей:

  • pro (A): OCP не є законом і читабельність коду також має значення.
  • pro (A): відносини сутності повинні бути судноплавними. Обидва класи можуть знати про цей взаємозв'язок.
  • pro (A) + (B): зробіть і те, і делегуйте в (A) - (B), щоб виріб мінявся повторно.
  • pro (C): покладіть методи пошуку в третій клас (сервіс) там, де він не є статичним.
  • contra (B): перешкоджає глузуванню під час тестів.

Кілька додаткових речей, які сприяли мої коледжі на роботі:

  • pro (B): наша рамка ORM може автоматично генерувати код для (B).
  • pro (A): з технічних причин нашої системи ORM потрібно буде в деяких випадках змінити "закритий" об'єкт, незалежно від того, куди шукає шукач. Тому я не завжди зможу дотримуватися SOLID.
  • contra (C): сильно метушиться ;-)

Висновок

Я використовую обидва (A) + (B) з делегацією для мого поточного проекту. Однак у середовищі, орієнтованій на сервіс, я піду з (C).


2
Поки це не статична змінна, все круто. Статичні методи прості для тестування та прості простеження.
Кодер

3
Чому б просто не мати клас ProductReviews? Тоді продукт і огляд залишаються однаковими. А може, я неправильно розумію.
ElGringoGrande

2
@Coder "Статичні методи прості для перевірки", справді? Їх не можна знущатися, див. На веб-сайті googletesting.blogspot.com/2008/12/…, щоб отримати докладнішу інформацію.
StuperUser

1
@StuperUser: Нічого знущатися над цим. Assert(5 = Math.Abs(-5));
Кодер

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

Відповіді:


7

Що важливіше: слідувати принципам SOLID або мати більш простий інтерфейс?

Інтерфейс відносно SOLID

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

Відкритий / закритий принцип

"не чіпай цього!" є занадто спрощеною інтерпретацією. І якщо припустити, що ми маємо на увазі, що "клас" є довільним, і не обов'язково правильним. Скоріше, OCP означає, що ви створили свій код таким, що зміна його поведінки не вимагає (не повинна) безпосередньо змінювати існуючий, робочий код. Крім того, недоторкання коду в першу чергу є ідеальним способом збереження цілісності існуючих інтерфейсів; на мій погляд, це суттєвий результат ОКП.

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

Дайте найкраще, у нас є команда лікарів

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

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


1
+1 за те, що OCP - це більше показник хорошої якості, а не швидке правило для будь-якого коду. Якщо ви виявите, що правильно дотримуватися OCP важко або неможливо, то це знак, що ваша модель повинна бути відновлена, щоб забезпечити більш гнучко використання.
CodexArcanum

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

10

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

Одне, що слід пам’ятати при використанні статичних методів, - це їх вплив на тестабельність .


Тестування forProduct(Product product)не складе проблем.

Тестувати щось, що від цього буде, буде.

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

Будемо мати метод, який називається, CUT()що викликаєforProduct()

Якщо forProduct()це staticви не можете перевірити , CUT()як атомна одиниця і всі тести стають інтеграційні тести , які перевіряють блок логіки.

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

Дивіться цей чудовий пост у блозі для отримання більш детальної інформації: http://googletesting.blogspot.com/2008/12/static-methods-are-death-to-testability.html


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


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

+1, дякую за встановлення червоного прапора для тестів! Я погоджуюся з @Wolfgang, зазвичай я теж не дуже суворий, але коли мені потрібні такі випробування, я дуже ненавиджу статичні методи. Зазвичай, якщо статичний метод занадто сильно взаємодіє зі своїми параметрами або якщо взаємодіє з будь-яким іншим статичним методом, я вважаю за краще зробити його методом екземпляра.
Адріано Репетті

Це не залежить повністю від мови, якою ви користуєтесь? ОП використовував приклад Java, але ніколи не згадував мови у питанні, а також не вказаний у тегах.
Ізката

1
@StuperUser If forProduct() is static you can't test CUT() as an atomic unit and all of your tests become integration tests that test unit logic.- Я вважаю, що і Javascript, і Python дозволяють перекрити / вимкнути статичні методи. Я не впевнений на 100%.
Ізката

1
@Izkata JS динамічно набирається, тому його немає static, ви імітуєте його із закриттям та однотонним малюнком. Читаючи на Python (особливо stackoverflow.com/questions/893015/… ), ви повинні успадкувати та розширити. Переосмислення не є глузуючим; схоже, у вас все ще немає шва для тестування коду як атомної одиниці.
StuperUser

4

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

Наприклад, я б спокусився ввести щось, що грало роль ретривера. Я, мабуть, дав би йому інтерфейс IRetrieveReviews. Ви можете помістити це в конструктор продукту (залежність впорскування). Якщо ви хочете змінити спосіб отримання оглядів, ви можете це легко зробити, ввівши інший співпрацівник - TwitterReviewRetrieverабо, AmazonReviewRetrieverабо MultipleSourceReviewRetrieverабо що-небудь інше, що вам потрібно.

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


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

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

Так, це було б реалізацією стратегії. Що було б аргументом для третього класу. (A) і (B) не підтримують це. Шукач обов'язково використовуватиме ORM, тому немає причини замінити алгоритм. Вибачте, якщо мені в цьому питанні не було зрозуміло.
Вольфганг

3

У мене буде клас ProductReview. Ви кажете, що Огляд все одно новий. Це не означає, що це може бути просто будь-що. Це все одно має бути лише одна причина для зміни. Якщо ви зміните спосіб отримання відгуків з будь-якої причини, вам доведеться змінити клас «Огляд».

Це не правильно.

Ви ставите статичний метод у клас Огляд, тому що ... чому? Чи не з цим ти борешся? Чи не в цьому вся проблема?

Тоді не варто. Складіть клас, на який покладається лише огляд продуктів. Потім ви можете підкласифікувати його до ProductReviewsByStartRating, що завгодно. Або підклас, щоб отримати відгуки про клас продуктів.


Я не згоден з тим, що використання методу «Перегляд» порушило б SRP. Що стосується продукту, це було б, але не було на огляді. Моя проблема там полягала в тому, що вона статична. Якби я перейшов метод до якогось третього класу, він все-таки був би статичним і матиме параметр product.
Вольфганг

Отже, ви говорите, що відповідальність за клас «Огляд», єдина причина його зміни, полягає в тому, що якщо статичний метод продукту потрібно змінити? У класі огляду немає інших функціональних можливостей?
ElGringoGrande

На Огляді багато чого. Але я думаю, що forProduct (Product) йому добре підійде. Пошук відгуків багато в чому залежить від атрибутів та структури Огляду (які унікальні ідентифікатори, які сфери зміни атрибутів?). Продукт, однак, не знає і не повинен знати про Відгуки.
Вольфганг

3

Я не ставлю функцію "Огляд відгуків про продукт" ні в Productклас, ні в Reviewклас ...

У вас є місце, де ви можете отримати свої продукти, правда? Щось із, GetProductById(int productId)а може, GetProductsByCategory(int categoryId)і так далі.

Крім того, у вас має бути місце для отримання своїх відгуків із GetReviewbyId(int reviewId)позначкою a, а може, і а GetReviewsForProduct(int productId).

Коли я хочу змінити спосіб збирання відгуків для продукту, я повинен змінити клас продукту.

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


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

@CodexArcanum Ну, ти не один. :-)
Ерік Кінг

2

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

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

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


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

Я думаю, що для невеликого набору класів було б непогано навіть надати методи BOTH (A) та (B). Занадто багато разів користувальницький інтерфейс недостатньо відомий на момент написання цієї логіки.
Адріано Репетті

1

Ваш Продукт може просто делегувати ваш метод статичного огляду, і в цьому випадку ви надаєте зручний інтерфейс у природному місці (Product.getReviews), але деталі щодо вашої реалізації містяться в Review.getForProduct.

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


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

Ключ OCP полягає в тому, що ви можете замінити реалізацію, не змінюючи код. На практиці це тісно пов'язане із заміною Ліскова. Одна реалізація повинна бути заміною іншої. Можливо, у вас є DatabaseReviews та SoapReviews як різні класи, що реалізують IGetReviews.getReviews та getReviewsForProducts. Тоді ваша система відкрита для розширення (змінюється спосіб отримання оглядів) і закрита для модифікації (залежності від IGetReviews не порушуються). Це я маю на увазі під управлінням залежності. У вашому випадку вам доведеться змінити код, щоб змінити його поведінку.
pfries

Чи не це принцип інверсії залежності (DIP), який ви описуєте?
Вольфганг

Вони пов'язані принципами. Ваш пункт заміни досягається DIP.
pfries

1

Я трохи по-іншому сприймаю це питання, ніж більшість інших відповідей. Я думаю, що це Productі Reviewє в основному об'єктами передачі даних (DTO). У своєму коді я намагаюся змусити моїх докторів / організацій уникати поведінки. Вони просто приємний API для зберігання поточного стану моєї моделі.

Коли ви говорите про OO та SOLID, ви, як правило, говорите про "об'єкт", який не представляє стан (обов'язково), а натомість представляє якусь послугу, яка відповідає на запитання для вас, або якій ви можете делегувати частину своєї роботи . Наприклад:

interface IProductRepository
{
    void SaveNewProduct(IProduct product);
    IProduct GetProductById(ProductId productId);
    bool TryGetProductByName(string name, out IProduct product);
}

interface IProduct
{
    ProductId Id { get; }
    string Name { get; }
}

class ExistingProduct : IProduct
{
    public ProductId Id { get; private set; }
    public string Name { get; private set; }
}

Тоді ваші фактичний ProductRepositoryповернення буде ExistingProductдля GetProductByProductIdметоду і т.д.

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

Якщо ви зміните схему бази даних, ви можете змінити реалізацію сховища, не змінюючи DTO і т.д.

Отже, коротше кажучи, я думаю, я б не вибрав жоден із ваших варіантів. :)


0

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

Зробіть як product.GetReviews, так і Review.ForProduct методами однієї лінії, які є щось на зразок

new ReviewService().GetReviews(productID);

ReviewService містить все більш складний код і має інтерфейс, призначений для перевірки, але не піддається впливу безпосередньо користувача.

Якщо ви повинні мати 100% охоплення, попросіть ваші тести інтеграції викликати методи продукту / класу огляду.

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

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