Применяя твердые принципы

13

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

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

(Примечание: пожалуйста, воздержитесь от обсуждения, зачем мне это нужно или почему я не использую XSLT и т. Д., Есть причины, но они не по теме.)

Для начала я выбрал только Таблицы и Ограничения. Если вы игнорируете столбцы, вы можете указать это следующим образом:

Ограничение является частью таблицы (или, точнее, частью инструкции CREATE TABLE), и ограничение также может ссылаться на другую таблицу.

Сначала я объясню, как приложение выглядит прямо сейчас (без применения SOLID):

В настоящий момент в приложении имеется класс «Таблица», который содержит список указателей на ограничения, принадлежащие таблице, и список указателей на ограничения, ссылающиеся на эту таблицу. Всякий раз, когда соединение устанавливается, обратное соединение также будет установлено. В таблице есть метод createStatement (), который в свою очередь вызывает функцию createStatement () каждого ограничения. Указанный метод сам будет использовать соединения с таблицей владельца и ссылочной таблицей для получения их имен.

Очевидно, это не относится к SOLID вообще. Например, существуют циклические зависимости, которые раздувают код с точки зрения требуемых методов «добавления» / «удаления» и некоторых деструкторов больших объектов.

Итак, есть пара вопросов:

  1. Должен ли я разрешить циклические зависимости с помощью внедрения зависимостей? Если это так, я полагаю, что ограничение должно получить таблицу владельца (и, возможно, ссылку) в своем конструкторе. Но как я мог тогда запустить список ограничений для одной таблицы?
  2. Если класс Table хранит свое собственное состояние (например, имя таблицы, комментарий к таблице и т. Д.) И ссылки на ограничения, являются ли это одной или двумя «обязанностями», имея в виду принцип единой ответственности?
  3. В случае 2. правильно, я должен просто создать новый класс в логическом бизнес-уровне, который управляет ссылками? Если это так, 1. очевидно больше не будет актуальным.
  4. Должны ли методы «createStatement» быть частью классов Table / Constraint или я должен также удалить их? Если да, то где? Один класс Manager для каждого класса хранения данных (т. Е. Table, Constraint, ...)? Или, скорее, создать класс менеджера по ссылке (аналогично 3.)?

Всякий раз, когда я пытаюсь ответить на один из этих вопросов, я где-то бегаю кругами.

Очевидно, что проблема становится намного более сложной, если вы включите столбцы, индексы и т. Д., Но если вы, ребята, поможете мне с простой вещью Table / Constraint, я могу решить все остальное самостоятельно.

Тим Мейер
источник
3
Какой язык вы используете? Не могли бы вы опубликовать хоть какой-нибудь скелетный код? Очень сложно обсуждать качество кода и возможные рефакторинги, не видя реального кода.
Петер Тёрёк
Я использую C ++, но я старался не допускать этого к обсуждению, поскольку у вас может быть эта проблема на любом языке
Тим Мейер
Да, но применение шаблонов и рефакторингов зависит от языка. Например, @ back2dos предложил AOP в своем ответе ниже, что явно не относится к C ++.
Петер Тёрёк
Пожалуйста, обратитесь к programmers.stackexchange.com/questions/155852/… за дополнительной информацией о принципах SOLID
LCJ

Ответы:

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, но и третий класс , который управляет взаимодействием этих 2 -х классов. Давайте назовем это ConversionManager.

  • Применение принципа DI может означать здесь: ConversionManager не должен создавать экземпляры Read_XMLи Write_SQLсам по себе. Вместо этого эти объекты могут быть введены через конструктор. И конструктор должен иметь такую ​​подпись

    ConversionManager(IDataModelReader reader, IDataModelWriter writer)

где IDataModelReaderинтерфейс, от которого Read_XMLнаследуется, и IDataModelWriterто же самое для Write_SQL. Это ConversionManagerоткрывает возможности для расширений (вы очень легко предоставляете разных читателей или писателей) без необходимости их изменения - поэтому у нас есть пример для принципа Open / Closed. Подумайте об этом, что вам придется изменить, если вы хотите поддержать другого поставщика базы данных - фактически, вам не нужно ничего менять в своей модели данных, просто вместо этого предоставьте другой SQL-Writer.

Док Браун
источник
Хотя это очень разумное упражнение SOLID, (с одобрением) отметьте, что оно нарушает «ОЭП старой школы Kay / Holub», требуя получения и установки для довольно анемичной модели данных. Это также напоминает мне о печально известной напыщенной речи Стива Йегге .
user949300
2

Ну, вы должны применить 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) { ... }
}

Что касается реализации индекса, есть 3 пути:

  • getContraintsReferencingметод может действительно просто сканировать все Databaseдля Tableэкземпляров и сканировать их Constraintс , чтобы получить результат. В зависимости от того, насколько это дорого и как часто вам это нужно, это может быть вариант.
  • он также может использовать кэш. Если ваша модель базы данных может измениться после того, как она будет определена, вы можете поддерживать кэш, генерируя сигналы из соответствующих экземпляров Tableи Constraintэкземпляров, когда они меняются. Немного более простым решением было бы Indexсоздать «индекс моментального снимка» целого, Databaseс которым вы бы потом отказались. Это, конечно, возможно только в том случае, если ваше приложение проводит большое различие между «временем моделирования» и «временем запросов». Если есть вероятность, что эти два шага одновременно, то это нежизнеспособно.
  • Другой вариант - использовать AOP для перехвата всех вызовов создания и соответственно поддерживать индекс.
back2dos
источник
Очень подробный ответ, мне нравится ваше решение! Что бы вы подумали, если бы я выполнил DI для класса Table, предоставив ему список ограничений во время построения? В любом случае, у меня есть класс TableParser, который может действовать как фабрика или работать вместе с фабрикой в ​​этом случае.
Тим Мейер
@ Тим Мейер: DI не обязательно инжектор конструктора. DI также может быть сделано функциями-членами. Если таблица должна получить все свои части через конструктор, зависит от того, хотите ли вы, чтобы эти части добавлялись только во время построения и никогда не изменялись позже, или если вы хотите создать таблицу пошагово. Это должно быть основой вашего дизайнерского решения.
Док Браун
1

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

В любом случае, циклические зависимости всегда можно сломать, введя абстрактный базовый класс. Это типично для представлений графа. Здесь таблицы - узлы, а ограничения внешнего ключа - ребра. Поэтому создайте абстрактный класс Table и абстрактный класс Constraint и, возможно, абстрактный класс Column. Тогда все реализации могут зависеть от абстрактных классов. Это может быть не лучшим из возможных представлений, но это улучшение по сравнению со взаимно связанными классами.

Но, как вы подозреваете, лучшее решение этой проблемы может не потребовать отслеживания отношений между объектами. Если вы хотите перевести только XML в SQL, вам не нужно представление графа ограничений в памяти. График ограничений был бы хорош, если бы вы хотели запустить алгоритмы графа, но вы не упомянули об этом, поэтому я предполагаю, что это не является обязательным требованием. Вам просто нужен список таблиц, список ограничений и посетитель для каждого диалекта SQL, который вы хотите поддерживать. Создайте таблицы, затем сгенерируйте внешние ограничения для таблиц. Пока требования не изменились, у меня не было бы проблем с подключением генератора SQL к XML DOM. Сохранить завтра на завтра.

Кевин Клайн
источник
Вот где «(на самом деле намного больше, чем это, но давайте будем простыми)» вступает в игру. Например, в некоторых случаях мне нужно удалить таблицу, поэтому мне нужно проверить, ссылаются ли какие-либо ограничения на эту таблицу.
Тим Мейер