Как разбить код на компоненты... большие классы? небольшие классы?
Это очень простой материал, но здесь. Я считаю, что я никогда не могу согласиться с самим собой: способ, которым я разделять большие классы на более мелкие, делает вещи более удобными или менее обслуживаемыми. Я знаком с шаблонами проектирования, хотя и не подробно, а также с концепциями объектно-ориентированного дизайна. Все вникающие правила и рекомендации в сторону, я ищу, чтобы выбрать ваши мозги в отношении того, что мне не хватает с очень простым, примерным сценарием. По существу, по строкам: "... этот проект сделает его более трудным" и т.д.... Все, чего я не ожидаю из-за отсутствия опыта, по существу.
Скажем, вам нужно написать базовый класс стиля "файловый читатель/файл" для обработки определенного типа файла. Позвоните в файл файл YadaKungFoo. Содержимое файлов YadaKungFoo по существу похоже на файлы INI, но с небольшими различиями.
Существуют разделы и значения:
[Sections]
Kung,Foo
Panda, Kongo
[AnotherSection]
Yada,Yada,Yada
Subtle,Difference,Here,Yes
[Dependencies]
PreProcess,SomeStuffToPreDo
PreProcess,MoreStuff
PostProcess,AfterEight
PostProcess,TheEndIsNear
PostProcess,TheEnd
ОК, поэтому это может привести к 3 основным классам:
public class YadaKungFooFile
public class YadaKungFooFileSection
public class YadaKungFooFileSectionValue
Два последних класса по существу представляют собой только структуры данных с переопределением ToString(), чтобы выплескивать список строк значений, хранящихся в нескольких общих списках. Этого достаточно для реализации функциональных возможностей сохранения YadaKungFooFile.
Таким образом, со временем YadaYadaFile начинает расти. Несколько перегрузок для сохранения в разных форматах, включая XML и т.д., И файл начинает нажимать на 800 строк или около того. Теперь реальный вопрос. Мы хотим добавить функцию для проверки содержимого файла YadaKungFoo. Первое, что приходит на ум, очевидно, добавить:
var yada = new YadaKungFooFile("C:\Path");
var res = yada .Validate()
И мы закончили (мы можем даже вызвать этот метод из конструктора). Проблема в том, что проверка довольно сложна и делает класс очень большим, поэтому мы решили создать новый класс следующим образом:
var yada = new YadaKungFooFile("C:\Path");
var validator = new YadaKungFooFileValidator(yada);
var result = validator.Validate();
Теперь этот пример, очевидно, ужасно простой, тривиальный и несущественный. Любой из двух способов выше, вероятно, не будет иметь большого значения, но мне не нравится:
- Класс YadaKungFooFileValidator и класс YadaKungFooFile, по-видимому, очень сильно связаны этим дизайном. Кажется, изменение в одном классе, скорее всего, вызовет изменения в другом.
- Я знаю, что такие фразы, как "Validator", "Controller", "Manager" и т.д.... указывают класс, который связан с состоянием других объектов, в отличие от его "собственного бизнеса" и, следовательно, нарушает разделение принципов озабоченности и отправки сообщений.
В целом, я думаю, я чувствую, что у меня нет опыта, чтобы понять все аспекты того, почему дизайн плохой, в каких ситуациях это не имеет особого значения и какая озабоченность несет больший вес: меньшие классы или более сплоченных классов. Они кажутся противоречивыми требованиями, но, возможно, я ошибаюсь. Может быть, класс validator должен быть составным?
По сути, я прошу дать комментарии о возможных преимуществах/проблемах, которые могут возникнуть в результате вышеупомянутого проекта. Что можно сделать по-другому? (базовый класс FileValidator, интерфейс FileValidator и т.д. u именовать его). Подумайте о функциональности YadaKungFooFile, которая со временем растет.
Ответы
Ответ 1
Боб Мартин написал серию статей по дизайну классов, которые он называет принципами SOLID:
http://butunclebob.com/ArticleS.UncleBob.PrinciplesOfOod
Принципы:
- Принцип единой ответственности: класс должен иметь одну и только одну причину для изменения.
- Принцип Open-Closed: вы должны иметь возможность расширять поведение классов, не изменяя его.
- Принцип замены Лискова: производные классы должны быть заменяемы для базовых классов.
- Принцип сегрегации интерфейса: создавайте мелкозернистые интерфейсы, специфичные для клиента.
- Принцип инверсии зависимостей: зависит от абстракций, а не от конкреций.
Поэтому в свете этого давайте рассмотрим некоторые из утверждений:
Таким образом, со временем YadaYadaFile начинает расти. Несколько перегрузок для сохранения в разных форматах, включая XML и т.д., И файл начинает нажимать на 800 строк или около того
Это первый большой красный флаг: YadaYadaFile начинает с двух обязанностей: 1) сохранение структуры данных раздела/ключа/значения и 2) знание того, как читать и записывать INI-подобный файл. Итак, первая проблема. Добавление большего количества форматов файлов связывает проблему: YadaYadaFile изменяется, когда 1) изменяется структура данных или 2) изменяется формат файла или 3) добавляется новый формат файла.
Аналогично, наличие единого валидатора для всех трех этих обязанностей ставит слишком большую ответственность за этот единственный класс.
Большой класс - это "запах кода": он не плох сам по себе, но он очень часто возникает из-за чего-то, что действительно плохо - в этом случае класс, который пытается быть слишком большим.
Ответ 2
Я не думаю, что размер класса вызывает беспокойство. Концерн - это больше сплоченности и сцепления. Вы хотите проектировать объекты, которые слабо связаны и сплочены. То есть, они должны быть связаны с одной четко определенной вещью. Поэтому, если вещь окажется очень сложной, класс будет расти.
Вы можете использовать различные шаблоны проектирования, чтобы помочь справиться с сложностью. Например, вы можете создать интерфейс Validator, а класс YadaKunfuFile зависит от интерфейса, а не от Validator. Таким образом, вы можете изменить класс Validator без изменений в классе YadaKungfuFile, пока интерфейс не изменится.
Ответ 3
YadaKungFooFile не должен знать, как читать себя с диска. Он должен знать только, как пройти сам, раскрыть своих детей и т.д. Он также должен предоставить способы добавления/удаления детей и т.д.
Должен быть интерфейс IYadaKungFooReader, который YadaKungFooFile использует в своем методе Load, который он будет использовать для загрузки себя с диска. Плюс несколько реализаций, таких как AbstractKungFooReader, PlainTextYadaKungFooReader, XMLYadaKungFooWriter, которые умеют читать и соответствующие форматы.
То же самое для записи.
Наконец, должен быть YadaKungFooValidatingReader, который будет использовать читатель IYadaKungFooReader, использовать его для чтения и проверки ввода при его чтении. Затем вы передадите проверяющий читатель в YadaKungFooFile.Load всякий раз, когда вы хотите, чтобы он проверял его при чтении с диска.
В качестве альтернативы вы можете сделать читатель активным, а не пассивным классом. Затем вместо того, чтобы передавать его в YadaKungFooFile, вы использовали бы его как factory, который создал бы ваш YadaKungFooFile, используя последние методы обычного доступа. В этом случае ваш читатель должен также реализовать интерфейс YadaKungFooFile, чтобы вы могли связать обычный читатель → проверяющий читатель → YadaKungFooFile. Имеет смысл?
Ответ 4
Я расскажу об этом
Класс YadaKungFooFileValidator и класс YadaKungFooFile кажется очень сильно связанный с этим дизайном. Кажется, изменение в одном классе вероятно, сработают изменения в другом.
Да, поэтому в свете этого выясните, как сделать это как можно более бесшовным. В лучшем случае создайте класс YadaKungFooFile таким образом, чтобы валидатор мог взять на себя это.
Во-первых, сам синтаксис должен быть довольно простым. Я бы не ожидал, что проверка синтаксиса изменится, поэтому вы, вероятно, будете в безопасности, чтобы просто записать жесткий код.
Что касается приемлемых свойств, вы можете вывести Enumerators из класса File, который будет проверять Validator. Если анализируемое значение не входит ни в один из перечислений для данного раздела, выведите исключение.
Итак, и т.д....
Ответ 5
Мой личный способ обработки вещей в этом случае состоял бы в том, чтобы иметь класс YadaKungFooFile, который будет состоять из списка YadaKungFooFileSections, который будет списком YadaKungFooFileValues.
В случае валидатора вы должны вызвать, чтобы каждый класс вызывал метод проверки класса ниже, а нижний класс в иерархии реализовал фактическое мясо метода проверки.
Ответ 6
Если проверка не зависит от всего файла, но вместо этого работает только с отдельными значениями раздела, вы можете разложить валидацию класса YadaKungFooFile, указав набор валидаторов, работающих с значениями раздела.
Если проверка действительно зависит от всего файла, то проверка естественно связана с классом файла, и вы должны рассмотреть вопрос о его размещении в одном классе.
Ответ 7
Размер класса - это не столько проблема, сколько размер метода. Классы - это средство организации, и, хотя есть хорошие и плохие способы организации вашего кода, он не привязан непосредственно к размеру класса.
Размер метода отличается, поскольку метод выполняет фактическую работу. Методы должны иметь очень специфические задания, которые неизбежно ограничивают их размер.
Я считаю, что лучший способ определить размер метода - это написание модульных тестов. Если вы пишете модульные тесты с достаточным охватом кода, тогда это становится очевидным, когда метод слишком велик.
Unit test методы должны быть простыми, иначе вам понадобится тестирование для тестирования ваших модульных тестов, чтобы знать, что они работают правильно. Если ваши методы unit test становятся сложными, это, вероятно, связано с тем, что метод пытается сделать слишком много и должен быть разбит на более мелкие методы с четкими обязанностями.