Проблема с ООД

Ребята, изучаю ООП сам по книге, одновременно пишу проект для себя, чтобы разобраться в деталях. Пишу на C# + SQL Express, но не буду углубляться в используемые технологии, проблема с дизайном.

Вкратце опишу сущности, чтобы было более понятно. Проект это система проката автомобилей. Роли: оператор (с админ правами и без), клиент, номер счета клиента, автомобили, инвойс.
У меня вопрос по определению действий сущности. Например, оплата инвойса к кому должна относиться — к клиенту (погашение баланса) или к номеру счету (он умеет контролировать свой баланс)? Или метод сдачи действия авто в аренду — оператор должен уметь сдавать его в аренду или авто имеет метод сдачи в аренду?

Возможно глупые вопросы, но я начинающий, простите.

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті

👍ПодобаєтьсяСподобалось0
До обраногоВ обраному0
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Как сделаете, так и будет работать )

От этих вопросов голова пухнет у всех, а ответов на эти вопросы нет.
Предлагаю всегда начинать проектирования с схемы базы данных. Потому что схема данных — это уже часть ООП дизайна и она подскажет, как надо огранизовать классы. Отражая таблицы на классы, вы уже получаете некоторую структуру.
Этого уже по большому счету достаточно, чтобы разрабатывать приложение, которое довольно просто изменять.
Далее, если хотите продолжать, думаете, что у вас — композиции или агрегации. Далее, кто кому подчиняется. И ответов на эти вопросы не существует, потому что вы в коде описываете свое представление о мире. Может оператор быть ответственным за аренду. Может клиент. А по базе они оба имеют ссылку на один объект. Может аренда иметь и тех и других )), можно вообще вывернуть ортогонально и рассматривать процессы как объекты.
Главное, чтобы дизайн был попроще и время не стало фиолетовым.

ООП хорошо в теории, когда в книгах пишут удобные примеры. А на практике у вас часто есть: молоток, яблоко, зарплата и время проведенное в банке. Вещи несоизмеримые. И вопрос стоит, в каких попугаях их измерять. Если ваши представления о задаче смогут упростить и найдут способ сложить это в единую шкалу: дерзайте. Если же не получается, то не стоит слишком много тратить на это усилий.

да, интересная идея. я читал вроде что entity framework может генерировать классы на основе схемы базы данных.
сейчас читаю про patterns, тут советовали, чтобы не изобретать велосипеды. велосипеды все равно будут, так как вы правильно отметили про теорию — в книгах одно, в реальности сложно представить как это будет работать.

Есть старый анекдот:

Он: Убери лишнее слово из списка: вода, сок, водка, булочка??
Она: Ммм... вода...
Он: Почему?
Она: Че то у тебя с соображалкой туго... Вода без каллорий! Еще подобные тупые вопросы будут?

По сути, это оно и есть. У вас обычно есть набор неких чего-то там (не хочу говорить, сущности или даже штуки, потому что совершенно разные в кучу смешанные объекты, явления, измерения). ООП классически заставляет выбрать некоторую ось, единицу измерения. И вы на всю эту хрень пытаетесь найти проекцию.
База данных не заставляет вообще находить проекции. Там просто механически всё разложено по таблицам.
С проекциями, очевидно, могут быть серьезные проблемы. Для кого-то важны объекты, которые покупают: молоток и яблоко. Для других, деньги, потраченные при покупке, обмене товаров. А яблоки/молотки — средство. Для третьих время.
Если ваша задача предполагает какие-то явные оси, то можно попытаться так оформить дизайн. Если нет, то возможно не стоит и стараться. Сделать на основе базы данных объекты без поведения, а потом над ними операции — это уже такой подход.

Наверное, важнее всего сделать так, чтобы было понятно читать код. Чтобы это получилось, надо помнить о таких штуках, как слои/уровни абстракции. Или распределение обязанностей, что ли.

Да, есть процессы, которые являются простыми функциями или процедурами. Заказ авто — это процесс. Тело этой функции должно отражать суть процесса, т.е. последовательно перечислять операции, относящиеся к текущему уровню абстракции. Например: создать order, назначить свободного оператора, отослать уведомление куда-то, т.п. Надо понимать, что когда процесс создания заказа авто изменится (в реальном мире/компании), его структуру будут менять именно в этой функции.

А то, как именно создается заказ, описывается внутри класса заказа. Он может триггерить какие-то события в системе (например сообщать о том, что он создался), чтобы какой-нибудь класс истории перехватывал эти сообщения и писал логи о создании ордеров, если надо. Но это уже все находится в других слоях абстракции. Он может использовать, создавать другие объекты. Главное, чтобы все это попадало под определение «создание именно заказа».

Пример запуска:
А сам процесс запускается (хотя это зависит от системы) живым клиентом или менеджером через (пускай web) HTML форму, сабмит которой просто вызывает банальную функцию. Функция на своем уровне проводит валидацию данных, если все ок, отдает слою бизнес логики, в котором запускается уже функция-процесс orderCar(client, other_params...)

А вообще, есть тысяча и один вариант, и все они будут неправильными.

Возможно глупые вопросы, но я начинающий, простите.
Не волнуйся, 99% программистов не знают ответов на эти вопросы, но стесняются спрашивать.
Например, оплата инвойса к кому должна относиться — к клиенту (погашение баланса) или к номеру счету (он умеет контролировать свой баланс)? Или метод сдачи действия авто в аренду — оператор должен уметь сдавать его в аренду или авто имеет метод сдачи в аренду?

Оплата инвойса и/или сдача авто в аренду это не действия. Это процессы, которые состоят из некоторого количества других процессов и действий, которые производятся разными сущностями. Для того, чтобы это понять, нужно просто посмотреть на реальность (хотя это не интересно для программиста).

Оператор не сдает авто в аренду. Он во всем процессе делает, грубо говоря, только две вещи — запускает процесс и выдает ключи. Манагер выдает задания подчиненным, бухгалтерия проверяет платежеспособность клиента, служба безопасности проверяет адекватность, механик снимает показания спидометра, менеджер подписывает договор — там куча действий, к которым оператор не имеет ровно никакого отношения. Да, эти все процессы автоматизированы, вмешательство людей не требуется, но они всё равно есть. Скажем проверка на платежеспособность сведена к блокированию средств на кредитной карте, но это не действия оператора — оператор может только провести картой по считывателю.

Поэтому у оператора остается два метода — StartProcess(CreditCard client.Card) и GiveKeys(Client client). Остальное разноси по сущностям адекватно реальности (представь, как это делается без компьютера), чтобы потом не запутаться.

PS К клиенту не относится ничего, это внешний фактор, действия которого ты не можешь контролировать. Поэтому у клиента методов вообще не должно быть.

Насчет процессов согласен. У меня более упрощенная модель — абстрактный класс Работник, далее два дочерних класса Менеджер и Оператор. В плане действий (процессов) у них одинаковые обязанности + менеджер имеет права на редактирование информации, смену паролей на вход в систему (процесс управления пользователями системы), добавление-редактирование автомобилей в системе (процесс управления автомобилями). Проверку авто проводят тоже они сами. Представим, что это небольшой прокат из двух человек, все остальные действия (бухгалтерия, ремонт авто) на аутсорсе.
Процесс выдачи в аренду (новый заказ) примерно такой: проверка наличия авто, изменение баланса счета клиента в соответствии со стоимость арендны машины на указанный срок, получение денег от клиента и погашение долга (пока неважно каким способом, наличными, картой, всё равно), после этого баланс счета меняется на нулевой, смена статуса авто со «свободно» на «занято».
Процесс получения авто по окончании аренды: проверка авто (изменение баланса, если есть повреждения, надо доплатить), смена статуса авто из «занято» в «свободно», смена пробега авто (чтобы в будущем учитывать амортизацию).
Что касается клиента, я думал о простейшей CRM (процесс управления клиентами), чтобы там была вся информация о клиенте, его водительских правах, балансе, и тд. Это важная часть, думаю любая простая система проката должна это иметь. Это потребуется если надо будет разыскать человека, который не вернул машину, предложить ему более выгодные условия аренды в следующий раз и тд. Я понимаю, что клиент сам не работает с CRM, это процессы связанные с Работниками компании.
Попробую переосмыслить дизайн с точки зрения реальности, без компьютера.

Если ваша цель — создать систему, которую в дальнейшем можно будет развивать и использовать, а не чисто с целью обучения основам ООД, и если не привязываться к технологиям, то я бы рекомендовал следующее:

абстрактный класс Работник, далее два дочерних класса
Можете взять еще более абстрактный класс — Аккаунт, который будет обозначать пользователя системы или субъекта, который имеет непосредственное отношение к прокату авто (клиент т.е.). Клиент, менеджер и оператор — это роли, которые могут назначаться аккаунтам. Кроме этого, у вас будут сущности, которые представляют объекты учета — авто, счет, инвойс и др. Как заметил Roman Brunets, вам стоит сконцентрироваться на основных сценариях — сдачу в аренду, прием из аренды, и т.д. — разбить их на меньшие(неделимые, в конечном итоге) операции над данными, прописать взаимодействие между аккаунтами, в рамках этих процессов. Операция описывается как отдельная сущность(отдельная иерархия классов, отдельная таблица в бд и т.д.), т.е. используйте шаблон Command. При определении операций выяснится и система прав на них(какие роли какие операции могу проводить).
Такой подход позволит вам иметь более гибкую систему, которая легче адаптировать к будущим изменениям в бизнес-модели.

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

Сергей, мои цели научиться ООП а также создать устойчивую к изменения структуру классов.
это задача не для новичка. что бы получались такие вещи и так как вы хотите, надо поработать с проектами подобного рода, в которых все сделано более менее правильно более опытными разработчиками/архитекторами. прежде было бы неплохо набраться опыта в более локальных задачах применяя паттерны ООП, практики и принципы ООД, разобраться с нормализацией в реляционных БД(очень помогает правильно проектировать предметную область). Что бы вам показывали ваши ошибки, давали рекомендации. Пока этого не пройдете, будете долго набивать шишки на комплексных задачах, имея кучу избыточности/дубликатов в коде и ненужных тесных связей, из-за которых не сможете нормально расширять систему.

Тогда рекомендую сразу начать изучать шаблоны проектирования, прежде чем приступить к созданию такой системы. И принципы ООП будете прояснять по ходу, и велосипеды изобретать не понадобится. Классика жанра — GoF «Design Patterns», и книги Мартина Фаулера (в вашем случае, «Шаблоны корпоративных приложений» в первую очередь)

Сергей, спасибо за конкретику!
А как вы проходили этот путь? Сначала паттерны, потом уже знание языка (инструмент для реализации)? Паттерны ведь не зависят от языка?

Я начинал с изучения и применения языков программирования. Книги по паттернам появились позже.

Паттерны ведь не зависят от языка?
Да, не зависят.
У меня вопрос по определению действий сущности. Например, оплата инвойса к кому должна относиться — к клиенту (погашение баланса) или к номеру счету (он умеет контролировать свой баланс)? Или метод сдачи действия авто в аренду — оператор должен уметь сдавать его в аренду или авто имеет метод сдачи в аренду?
У меня поначалу были такие-же вопросы. Потому что начинал с «классического» ООП (Гради Бутч), где объект это данные + поведение. За этот подход ООП очень сильно критиковали (особенно поклонники функционального программирования), потому что все крутится вокруг состояния объекта и это не гибко и плохо для многопоточности.
Современный C# уже не чистый ООП язык: в него добавили очень много других полезных концепций (LINQ, промизы, декларативное программирование, модел-дривен подход и т.д.).
Поэтому лучше разделять данные (сущности предметной области) и процессы (операции над данными). В Вашем примере получатся интерфейсы: оператор, авто и процесс «сдача в аренду» (который внутри состоит из отдельных операций — шагов).
Еще один совет касательно ООД для .Net: на этапе ООД создаем только интерфейсы и схемы! Никаких классов и, желательно, никаких таблиц в базе. Это очень помогает отрешиться от мыслей о деталях реализации и сосредоточиться на предметной области и бизнес-логике.
Да, еще: помимо «классического» ООП для современного дизайна обязательно знать и понимать SOLID принципы:
ru.wikipedia.org/...ограммирование

Вообще-то, как практик предпочитаю не погружаться в дебри оптимизации ОО дизайна. Лучшее — враг хорошего, и если что-то прилично работает, то и спорить не о чем. Но ваш дизайн сразу вызывает вопросы — как соотносится клиент и номер его счета? Если это one to one mapping, зачем тогда две сущности? Что до вопроса, кому отдать то или иное действие.метод, ответ часто зависит от того, чьи преимущественно атрибуты этот метод собирается использовать / изменять. В вашем случае сдача авто в аренду не особо изменяет само авто, за исключением ожидаемого износа, если модель вообще учитывает износ и падение стоимости. Оператора сдача авто тоже не особо меняет, разве что немножко обогатит. Так что оба кандидата в собственники метода сдачи в аренду мне не кажутся удачными. Но если, к примеру, единственным результатом метода сдачи авто является выставка маркера «сдано», чтобы избежать его повторной сдачи, такой метод вполне может принадлежать сущности авто.

Анатолий, я тоже уже понял, что улучшением (оптимизацией) можно заниматься бесконечно, более того она ломает уже написанный код. Но к сожалению, я замечаю, что тем больше пишу, тем это больше похоже на процедурное программирование.
У меня возникла схожая проблема, когда описывал действия с базой данных. К примеру «добавить нового клиента» в методах класса я прописал запрос к базе. Потом написал отдельный класс SQLHelper, в нем все методы для работы с базой, он принимает данные нового клиента как аргументы. На мой взгляд это правильнее.
Кстати, есть ли необходимость постоянно создавать новые классы и в них энкапсулировать уже существующие классы и методы? Я понимаю, что структура классов должна в точности соответствовать поведению программы, но и тут можно долго изобретать.
Что касается клиента и его счета. Счет это тоже сущность, у него есть номер, у него есть владелец, у него есть методы изменения баланса счета. Оно не может быть просто свойством оператора.
Согласен, что метод сдачи авто не особо влияет на оба класса. Но выполнение метода не только меняет метку в авто, но это изменение баланса счета клиента, возможно что-то еще, пока в голову не приходит.

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

А как определить потребность в создании новых классов? Имеется в виду, что сегодня я могу обойтись одним классом, а завтра пришел заказчик, попросил новый функционал, я понял что в рамках этого класса я не могу это сделать, создал новый класс, решил проблему?
Я бы например, мог не делать класс SQLHelper а прописать общение с базой данных в том же классе, в его методах. И это тоже будет работать. По сути можно вообще одним классом обойтись, но как потом такое поддерживать? Я не уверен что через месяцы сам смогу в этом разобраться.

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

Система — эволюционирующая сущность пропорционально требованиям и чтобы поддерживать ее, приходится постоянно переписывать те или иные (иногда значительные части), делать изначально чрезмерно гибкую архитектуру «про запас» — гиблое дело, 80% задумок никогда не потребуются и будут лишь отягощать.

Совет: Используйте TDD и выращивайте систему как соляные кристаллы ложатся на нить, каждая фича (фича, а не куски кода) покрыты тестами, это позволит переписывать внутренности не боясь за поведение, а также позволит избавиться от хрупких тестов. Поменялись требования — дописываете тесты, те которые валятся — подправляете функционал, ну и далее по итерации.

А как определить потребность в создании новых классов?
Никак, это приходит с опытом и здесь не существует правильных ответов. Все зависит от конкретных требований. Не знаю как в С++, но в Java я бы полностью отказался от наследования и использовал паттерн «Стратегия» для ваших автомобилей, это позволит задать автомобилю любое поведение и у вас не получится вырожденной иерархии наследования, когда в наследнике n-го порядка понадобится сделать летающий автомобиль, а ваш абстрактный класс его не поддерживает и приходится делать велосипеды и дикие хаки.

Да, пожалуй соглашусь, что постоянное тестирование позволит удержать функционал от разваливания, я тесты никогда не писал, считал это лишним.
Что касается иерархии для класса автомобилей, я не планировал тут ничего усложнять, мне достаточно одного класса с его свойствами. Не думаю, что есть необходимость в абстрактном классе.

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

Ну как я понимаю, тесты всего лишь проверяют как это работает, а не как это сделано. Хоть процедурно описать, хоть через ооп, тесты будут вести себя одинаково.

Тесты вообще никак себя не ведут, они либо проходят, либо спотыкаются. В последнем случае приходится разбираться в причинах этого безобразия и тратить существенное время на его устранение. Вот это самое время и соответственно зарплата чудакам, которые его тратят, и будут разными в зависимости от качества дизайна и тестов. Можно, конечно, сэкономить на том и другом, направив фонтан кодировщика непосредственно на клиента, тем самым используя его в качестве бесплатного тестера. Очевидные преимущества этой гениальной идеи слегка омрачаются тем, что клиент истошно кричит на невинное существо из саппорта, не может толково объяснить, что не так, и в конце концов сбегает к конкурентам. Когда он начинает отсуживать у нас свои деньги, появляются первые сомнения в целесообразности экономии на тестах.

По сути можно вообще одним классом обойтись
Выше приведена ссылка на SOLID принципы. Первый же из них говорит об ограничении ответственности каждой отдельной сущности до минимума. Это на случай, когда простое житейское «проще-лучше» уже не воспринимается организмом.

Рекомендую отталкиваться не от объектов, а от операций. В данном примере есть операция «cдать авто в аренду», следовательно, будет отдельный класс RentService, который и реализует логику, может ли человек X взять в аренду авто Y.
Вообще, на практике удобнее использовать процедурный подход «алгоритмы+данные»: отдельно классы с данными без логики и отдельно классы с логикой без данных. Так проще писать тесты.

даже так..спасибо :)
Обычно в книжках (ну которые я читал), пишут что класс обычно это как существительное, соответственно методы это действие и тд. Попробую переписать дизайн по вашей рекомендации.

Ну почему же, есть же DDD. На текущем проекте используем эту методологию, помещаем бизнеслогику в Entity ( Domain objects) бины, все отлично, тестировать легко, mock’ать ничего не надо.

Плюс, есть еще интересный паттерн BCE (boundary control entity) который с успехом применяем. Координацией и транзакциями занимается фасад, логика в сущностях, для так называемых cross-cuttings concerns вспомогательные классы.

А ваш процедурный подход с де факто структурам вместо доменной модели хорош для SOA или каких-нибудь CRUD’ов где сложной бизнес-логики нет и быть не может.

Егор, я почитаю про предложенный вами способ. Entity пока использовать не хотелось бы, юзаю ADO.NET, просто не хочу сейчас уходить в дебри изучения Entity. Я понимаю что это более удобный способ работы с объектами, но у меня проблема с дизайном. Ведь по сути все равно что применять, при нормальном дизайне всё равно будет работать.

Про Entity я говорю не в контексте использования Entity Framework ( я вообще джавист), а как об обьекте который мапится на таблицу базы данных. Т.е. персистентный доменный обьект (PDO, persistent domain object)

сорри, не разобрался, очень уж по контексту подходило :)

С Хабра отрывок статьи: DDD не привязанны к конкретной технологии, однако соблюдать DDD будет не так просто, без наличия хороших средств и практик в вашем арсенале, таких как: TDD-фреймворк, ORM, возможность реализации независимости сохраняемости (Persistence Ignorance), IoC-контейнер (Inversion of Control), и возможностей AOP (Аспектно-Ориентированного Программирования), конечно не значит, что все эти инструменты нам понадобятся, однако они приблизят нас к реализации DDD на практике.

Не хило так для начала :)))

Ну, в Java EE все это идет из под коробки. Кроме TDD, но в сочетании с DDD подходом тестировать значительно легче — не нужно мочить кучу зависимостей :)

Ну Java EE всего лишь инструмент для решения проблемы. Мне бы пока теорию поднять, начал читать книгу «Архитектура корпоративных программных приложений».

Читал, немного устарела правда. Лет на 10. Но книга все равно толковая, почитай там про доменную модель.

Вообще, на практике удобнее использовать процедурный подход “алгоритмы+данные”: отдельно классы с данными без логики и отдельно классы с логикой без данных.

Класи з даними (і все ж мінімальними методами, що працюють з приватними даними) називаються Entities; класи з логікою — Services. Все разом — Anemic Domain Model (на противагу Fat/Classic Domain Model, котра зараз у Вас). Просунуті пацани в дворі тільки анемічну модель і застосовують:)

Рекомендую:
Eric Evans — Domain-Driven Design: Tackling Complexity in the Heart of Software

Підписатись на коментарі