Застосування принципів SOLID


13

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

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

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

Для початку я вирішив поглянути лише на таблиці та обмеження. Якщо ви ігноруєте стовпці, ви можете вказати це наступним чином:

Обмеження є частиною таблиці (а точніше, частиною оператора CREATE TABLE), а обмеження може також посилатися на іншу таблицю.

Спочатку я поясню, як зараз виглядає програма (не застосовуючи SOLID):

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

Очевидно, це взагалі не стосується SOLID. Наприклад, існують кругові залежності, які роздувають код з точки зору необхідних методів "додати" / "вилучити" та деяких великих деструкторів об'єктів.

Отже, є кілька питань:

  1. Чи слід вирішити кругові залежності за допомогою введення залежностей? Якщо так, я вважаю, що обмеження має отримати власнику (і необов'язково посилану) таблицю у своєму конструкторі. Але як я міг тоді перебігти список обмежень для однієї таблиці?
  2. Якщо клас "Таблиця" зберігає сам стан (наприклад, ім'я таблиці, коментар до таблиці тощо) та посилання на обмеження, чи є це одна чи дві "відповідальності", думаючи про єдиний принцип відповідальності?
  3. У випадку 2. правильно, я повинен просто створити новий клас на рівні логічного бізнесу, який керує посиланнями? Якщо так, 1. це, очевидно, більше не має значення.
  4. Чи повинні методи «createStatement» входити до класів «Таблиця / обмеження» чи я також повинен їх перемістити? Якщо так, то куди? Один клас менеджера для кожного класу зберігання даних (тобто таблиця, обмеження, ...)? А точніше створити клас менеджера за посиланням (подібний до 3.)?

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

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


3
Якою мовою ви користуєтесь? Чи можете ви опублікувати хоч якийсь код скелета? Дуже важко обговорити якість коду та можливі оновлення, не побачивши фактичного коду.
Péter Török

Я використовую C ++, але я намагався запобігти його обговоренню, оскільки ви могли мати цю проблему будь-якою мовою
Тім Меєр

Так, але застосування шаблонів і рефакторинга залежить від мови. Напр. @ Back2dos запропонував AOP у своїй відповіді нижче, що, очевидно, не стосується C ++.
Péter Török

Будь ласка, зверніться до програмістів.stackexchange.com/
questions/155852/…,

Відповіді:


8

Ви можете почати з іншої точки зору, щоб застосувати тут "Принцип єдиної відповідальності". Що ви нам показали, це (більш-менш) лише модель даних вашої заявки. SRP тут означає: переконайтеся, що ваша модель даних відповідає лише за збереження даних - ні менше, ні більше.

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

[XML] -> ("Read XML") -> [Data model of DB definition] -> ("Write SQL") -> [SQL]

Тому єдине місце , де XML конкретний код повинен бути поміщений клас з ім'ям, наприклад, Read_XML. Єдиним місцем для специфічного коду SQL повинен бути такий клас Write_SQL. Звичайно, можливо, ви збираєтеся розділити ці 2 завдання на більше підзадач (і розділити свої класи на кілька класів менеджерів), але ваша "модель даних" не повинна нести відповідальності з цього рівня. Тому не додайте ані createStatementдо одного із класів вашої моделі даних, оскільки це надає вашій моделі даних відповідальність за SQL.

Я не бачу жодної проблеми, коли ви описуєте, що Таблиця відповідає за збереження всіх її частин (ім'я, стовпці, коментарі, обмеження ...), це ідея, що стоїть за моделлю даних. Але ви описали, що "Таблиця" також відповідає за управління пам'яттю деяких її частин. Це специфічна проблема C ++, з якою ви не зіткнетеся так легко в таких мовах, як Java або C #. Спосіб позбавлення від відповідальності C ++ - це використання розумних покажчиків, делегування права власності на інший рівень (наприклад, бібліотеку підвищення або власний "розумний" шар вказівника). Але будьте обережні, ваші циклічні залежності можуть "дратувати" деякі інтелектуальні вказівки.

Щось більше про SOLID: ось приємна стаття

http://cre8ivethought.com/blog/2011/08/23/software-development-is-not-a-jenga-game

пояснення SOLID невеликим прикладом. Спробуємо застосувати це до вашого випадку:

  • вам знадобляться не лише класи Read_XMLта Write_SQL, а й третій клас, який керує взаємодією цих двох класів. Давайте назвемо це ConversionManager.

  • Застосування принципу DI може означати тут: ConversionManager не повинен створювати екземпляри Read_XMLі Write_SQLсамі по собі. Натомість ці об'єкти можна вводити через конструктор. І конструктор повинен мати такий підпис

    ConversionManager(IDataModelReader reader, IDataModelWriter writer)

де IDataModelReaderінтерфейс, з якого Read_XMLуспадковується, і IDataModelWriterте саме для Write_SQL. Це робить ConversionManagerвідкритим для розширень (ви дуже легко надаєте різним читачам чи письменникам), не змінюючи його, тому у нас є приклад принципу "Відкрити / закрити". Подумайте про те, що вам доведеться змінити, коли ви хочете підтримати іншого постачальника баз даних - як правило, вам не потрібно нічого змінювати у своїй модельній формі даних, просто надайте інший SQL-Writer замість цього.


Хоча це дуже розумна вправа SOLID, (звернено до уваги) зауважте, що вона порушує "стару школу Kay / Holub OOP", вимагаючи геттерів та сетерів для досить анемічної моделі даних. Це також нагадує мені сумнозвісну розпусту Стіва Йегге .
user949300

2

Ну, ви повинні застосувати S в такому випадку S SOLID.

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

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

Щоб розбити його на дуже спрощену версію:

class Table {
      void addConstraint(Constraint constraint) { ... }
      bool removeConstraint(Constraint constraint) { ... }
      Iterator<Constraint> getConstraints() { ... }
}
class Constraint {
      //actually I am not so sure these two should be exposed directly at all
      void addReference(Table to) { ... }
      bool removeReference(Table to) { ... }
      Iterator<Table> getReferencedTables() { ... }
}
class Database {
      void addTable(Table table) { ... }
      bool removeTable(Table table) { ... }
      Iterator<Table> getTables() { ... }
}
class Index {
      Iterator<Constraint> getConstraintsReferencing(Table target) { ... }
}

Щодо реалізації індексу, то є три шляхи:

  • getContraintsReferencingметод може дійсно просто сканувати всі Databaseдля Tableпримірників і сканувати їх Constraintз , щоб отримати результат. Залежно від того, наскільки це дорого коштує і як часто вам це потрібно, це може бути варіант.
  • він також може використовувати кеш. Якщо модель вашої бази даних може змінитися, колись визначена, ви можете підтримувати кеш, випускаючи сигнали з відповідних Tableта Constraintекземплярів, коли вони змінюються. Трохи простішим рішенням буде Indexскласти «індекс знімків» цілого, Databaseз яким працювати, з яким ви потім відкинете. Це, звичайно, можливо лише в тому випадку, якщо ваша програма розрізняє "час моделювання" і "час запиту". Якщо ці двома шансами зробити це одночасно, то це неможливо.
  • Іншим варіантом буде використання AOP для перехоплення всіх викликів створення та відповідно індексації.

Дуже детальна відповідь, мені подобається ваше рішення поки! Що ви думаєте, якби я виконував DI для класу Table, надаючи йому список обмежень під час будівництва? У мене в будь-якому випадку є клас TableParser, який може діяти як фабрика або працювати разом із заводом для цього випадку.
Тім Меєр

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

1

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

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

Але, як ви підозрюєте, найкраще рішення цієї проблеми може не вимагати відстеження об'єктних відносин. Якщо ви хочете лише перевести XML в SQL, то вам не потрібно представлення в пам'яті графіка обмеження. Графік обмежень було б добре, якби ви хотіли запустити алгоритми графів, але ви цього не згадали, тому я припускаю, що це не є вимогою. Вам просто потрібен список таблиць і список обмежень та відвідувач для кожного діалекту SQL, який ви хочете підтримати. Створіть таблиці, а потім генеруйте обмеження, зовнішні для таблиць. Поки вимоги не змінилися, у мене не виникне жодних проблем із з'єднанням SQL-генератора з XML DOM. Збережіть завтра на завтра.


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