Залепа №13. О грамотном проектировании и неграмотном IDE.
Эту статью без мата в адрес мелкомягких писать сложно :( Но я все же постараюсь.
Есть некая программа, работающая с базами данных. Естественно в ней куча таблиц, большинство из которых представляют собой достаточно простые справочники. Например, это могут быть справочники телефонных кодов, типов продукции, видов доставки, да чего угодно. Для простоты будем рассматривать только один из таких справочников - справочник пользователей.
Каждая запись справочника содержит данные об одном пользователе: его логин, пароль для входа в систему и какие-то дополнительные атрибуты. Например его права доступа. В программе, кроме основных функций, должна быть возможность как редактирования списка пользователей так и изменения параметров отдельных пользователей.
Задача, согласитесь, тривиальна и встречается на каждом шагу. Попробуем ее решить средствами C#. Вариантов решений может быть масса, но нас интересует грамотный подход, т.е. такое решение, при котором наша программа не превратится в кашу из кучи классов, лишних открытых методов, свойств и форм.
Здравый смысл подсказывает, что справочник должен быть представлен неким классом (назовем его CUserMgr), который позволяет основной программе получить все необходимые данные о пользователе и скрывает всю внутреннюю структуру внутри себя.
Опять же для простоты решим, что программе нужно получать только лишь логин и пароль пользователя по его идентификатору. Ну и конечно вызывать редактор пользователей. Таким образом у нас получается примерно такой открытый интерфейс нашего класса:
public class CUserMgr
{
// получить идентификатор по логину
public int GetID(string login) {...}
// получить логин по идентификатору
public string GetLogin(int id) {...}
// получить пароль по идентификатору
public string GetPassword(int id) {...}
// вызвать редактор пользователей
public void ShowEditor() {...}
}
Так же класс должен содержать еще набор закрытых методов для редактирования пользователей: добавление нового пользователя, удаление существующего, изменение параметров пользователя, загрузку и сохранение списка пользователей и т.д.
Редактор пользователей должен отобразить на экране окно списка пользователей, в котором можно добавить нового пользователя и удалить или отредактировать существующего. Для целей добавления/редактирования будет использоваться еще одно окошко - форма редактирования пользователя. Итак, получаем как минимум две формы, которые должны напрямую обращаться к внутренней структуре класса CUserMgr, т.е. к его закрытым методам.
Решение "в лоб", т.е. создание глобальных классов нужных нам форм ни к чему хорошему не приведет:
- о существовании этих форм должен знать только CUserMgr, остальные о них не должны даже подозревать, т.к. никому кроме CUserMgr они не нужны. У нас же они получаются глобальными, т.е. видимыми всеми классами. Это плохо.
- Чтобы формы могли обратиться к внутренней структуре класса CUserMgr придется внутреннюю структуру сделать открытой, т.к. друзья (friend) в C# не предусмотрены (об этом уже писалось здесь). Открытая структура класса менеджера приводит к путанице и соблазну использовать эти методы (которые по логике должны быть закрытыми) не по назначению. А как же инкапсуляция??? В этом случае ее просто невозможно реализовать! Ужас!
Все это можно перетерпеть если вы создаете класс, который будет использоваться только в вашей программе и код которого никогда не будет использован где-либо еще. А как же быть, если вы создаете коммерческую библиотеку? Неужели придется вот так вывалить на всеобщее обозрение потроха ваших классов? Это уже очень плохо. Получается, что в "самом объектно-ориентированном языке современности" (а именно так microsoft позиционирует его на рынке) использовать всю мощь этой самой объектно-ориентированности и не получится!
Изрядно поломав голову, я смог найти еще два решения: первое в стиле майкрософта - корявое и громоздкое, второе - единственно верное для C# и правильное с точки зрения ООП, но имеющее изрядный косяк при использовании IDE Microsoft Visual Studio. Разберем оба варианта.
Вариант номер раз (стиль программирования a'la Microsoft)
Суть заключается в избавлении от необходимости использовать одним классом внутренней структуры другого класса. Решение сводится к созданию классов, представляющих сущности используя в качестве базы класс формы. Мудрено? Да!
Нам необходимо создать отдельный класс, представляющий собой запись из БД (информацию об отдельном пользователе). Причем этот класс должен быть порожден от класса Form, чтобы он мог отобразить себя для редактирования данных пользователя. Набросаем примерную структуру:
public class CUser : Form
{
private string Login;
private string Password;
private int ID;
...
// конструктор
public CUser(int id, string login, string passw) {...}
public string GetLogin() { return Login; }
public string GetPassword() { return Password; }
public int GetID() { return ID; }
// вызов редактора пользователя
public void Edit() {...}
// загрузка и сохранение данных о пользователе
public bool Load() {...}
public bool Save() {...}
}
В общем нечто такое. Теперь нашему окну редактирования пользователя (классу CUser) уже не нужно обращаться к классу CUserMgr за информацией - все необходимое он содержит в себе.
Идем далее. Наш класс менеджера также должен быть наследником класса формы (Form). В этом случае он сможет так же отобразить себя для редактирования общего списка пользователей. И ему также не нужно будет обращаться к внутренней структуре других классов, т.к. список пользователей (объектов класса CUser) он содержит внутри себя. Он их будет создавать и удалять (редактирование списка пользователей), получать из них информацию и передавать ее в основную программу.
Вот примерно такой подход.
Согласитесь, что такая убогая симуляция дружественности классов имеет массу недостатков:
- Нормальной инкапсуляции все равно не получится, т.к. в реальной программе некоторые закрытые методы как CUser так и CUserMgr все же придется открыть.
- Класс CUser все еще глобален и видим любым и каждым со всеми вытекающими отсюда отрицательными последствиями.
- Для справочника пользователей, которых не больше сотни этот подход будет работать более-менее нормально. Но если это будет справочник артикулов продукции, которых может быть несколько десятков тысяч, то мы получи дикий перерасход памяти, ведь для каждого объекта будет создано отдельное (пусть и скрытое) окно. Это вызовет не только расход памяти, но и будет существенно тормозить программу.
Короче, это не выход, а скорее его имитация. Выкрутас, временная отмазка. В реальных крупных программах не советую его использовать.
Вариант номер два (реально объектно-ориентированный)
Второй вариант использует возможность C# создавать вложенные классы. Видимо их ввели в язык только с одной целью - как решение при отказе от дружественности классов. Тут надо сделать некоторые пояснения.
В C# введена возможность определить класс внутри другого класса. При этом встроенный класс получает статус "члена класса-оболочки", т.е. все объекты вложенного класса (кем бы и где бы они не были созданы) имеют ПОЛНЫЙ доступ к членам своего класса-оболочки: они могут использовать и защищенные и даже закрытые методы класса-оболочки. Поразмыслите над эти на досуге и найдете эту возможность достаточно удобной, хотя и несколько путанной в применении.
Как же будет выглядеть решение нашей задачи при использовании вложенных классов? А примерно вот так:
public class CUserMgr
{
class CUser
{
private string Login;
private string Password;
private int ID;
...
}
class CUserEdit : Form
{
public void Edit();
...
}
class CUserList : Form
{
public void ShowList() {...}
...
}
// закрытые методы и свойства
...
// "внешний интерфейс"
public bool Load() {...}
public bool Save() {...}
public void ShowEditor() {...}
}
Итак, вот что мы получаем при таком подходе:
- программа "видит" только класс CUserMgr, о существовании внутренних классов она даже не подозревает и естественно не может получить к ним доступ. Она может использовать только открытый "внешний интерфейс" класса CUserMgr. Внутренняя структура нашего менеджера пользователей полностью скрыта (инкапсуляция торжествует!).
- Можно как угодно изменять внутреннюю структуру CUserMgr и вложенных классов. Если внешний интерфейс не изменится, то эти модификации никак не скажутся на работе основной программы, использующей наш класс.
- Встроенные классы могут без проблем обращаться к внутренним (закрытым) методам класса CUserMgr, что позволяет в ряде случаев сильно упростить программирование этих классов.
В общем мы добились нормального решения поставленной задачи. Поздравляю.
"А где же косяк?" - спросите вы - "неужели microsoft наконец-то сделала все правильно?" :)
А косяк все же есть. Но на этот раз он не в языке, а в IDE Visual Studio.
Классы, созданные по описанной выше "технологии" будут нормально компилироваться и работать (а как же иначе - все ведь сделано верно, в соответствии с требованиями и возможностями языка C#). Но в IDE вы не сможете использовать визуальный редактор для форм, которые являются вложенными в другие классы (в нашем случае это CUserEdit и CUserList) - IDE вместо отображения формы просто будет материться по-английски. Поэтому проектирование пользовательского интерфейса превратится при описанном подходе в настоящую муку. :(
Почему так происходит, ведь с точки зрения языка все верно и никакого "криминала" в таком вложении нет? Этот вопрос нужно адресовать разработчикам Visual Studio, которые (как обычно) лучше нас с вами знают что и как должно работать.
С Днем 8-го марта!
Поздравляю всех женщин-читателей этого блога (если таковые конечно имеются) с прекрасным весенним праздником -
Хочу пожелать всем Вам много здоровья, счастья, всегда отличного настроения. И чтобы у Вас рядом было надежное мужское плечо, на которое вы всегда смогли бы положиться.
С праздником, милые женщины. С Днем 8-го марта!
Различные Женщины ходят по свету.
Буквально два слова - на тему на эту.
Во-первых, есть Женщины - рыбки и птички.
Есть зайки и пупсики. Пышки и спички.
Драконы и змеи. И тысячи кисок.
(Тут крик из толпы: 'Огласите весь список!')
Есть Женщина-смирно. Есть Женщина-вольно.
Есть Женщина-'Нива' и Женщина-'Вольво'.
Есть Женщина-плазма. Женщина-лёд.
Женщина-лезвие. Женщина-йод.
Есть Женщины-девы и Женщины-овны.
Есть Женщины-где-вы? и Женщины-вот-мы!
Вот Женщина-финка. Вот Женщина-полька.
Вот Женщина-фиг-вам! и Женщина-сколько?
Два мира, два полюса: Женщина-хмель
И Девочка-помнишь-я-нес-твой-портфель?
Манящая Девушка-бригантина
И Женщина-где-ты-шатался-скотина?
Есть Женщина-повесть. Есть Женщина-строчка.
И просто конец всему - Женщина-точка.
Нет двух одинаковых Женщин на свете.
Мы кое-что поняли в этом предмете.
Мягки и податливы Женщины-клецки.
Болтает без умолку Женщина-Троцкий.
Всегда предсказуема Женщина-эхо.
И в семьдесят женственна Женщина-Пьеха.
У Женщины-бабы - простая натура.
А Женщина-пуля - как водится, дура.
Все время колеблется Женщина-синус.
И есть свои плюсы у Женщины-минус.
У Женщины-червы - огромное сердце.
Душа на защелке - у Женщины-дверцы.
Прекрасны на Женщине-иве сережки.
У Женщины-тумбочки - чудные ножки.
Сильна интуицией Женщина-Глоба.
Сильна конституцией Женщина-сдоба.
Пунцовые губы - у Женщины-вамп.
Песцовые шубы - у Женщины-вам-бы.
В беседках беседы - у Женщины-ямб.
Но сладость победы - у Женщины-штамп.
Ведут себя Женщины неодинаково.
Уж мы-то от них навидалися всякого.
С рассветом поднимет нас Женщина-утро,
А вечером - Женщина-Камасутра.
От снега, от ливня, от града укроет
Надежная Женщина-рубероид.
Лишит разом разума Женщина-ром.
На место поставит нас Женщина-бром.
Наделает крошек нам Женщина-плюшка.
По шайбе нащелкает Женщина-клюшка.
Согреет до косточек Женщина-юг.
Бесследно поглотит нас Женщина-люк.
Вот Женщина-свечка сгорает от страсти.
Стирает старательно Женщина-ластик.
Вот Женщина-блузка куда-то слиняла.
А Женщина-раз нас зачем поменяла?
Чего надо Женщине-не-уходи-же?
Куда навострилися Женщины-лыжи?
Доколь подстрекать будут нас на поступки
Зовущие, жадные Женщины-губки?
Застряв, словно в джунглях, в словах непролазных,
Мы славим всех Женщин - хороших и разных!
Заметим от имени всех мужиков:
Вы - всё, что нам нужно во веки веков!
А именно: мама, супруга, подруга,
Лекарство, подушка, мечта, Джомолунгма,
Оазис в пустыне и плот в океане,
Картошка в кастрюле, грибы на поляне,
Заботы о частном и мысли о вечном,
Часовня на Красном и звезды на Млечном.
Вам скажут и прапорщики, и поэты:
Зимою и летом все мысли - про это.
А именно: здорово, хоть и непросто,
Объять необъятную Женщину-космос,
Разжать крепко сжатую Женщину-фигу
И выиграть Женщину-высшую-лигу,
В постель положить с собой Женщину-книжку,
Все деньги поставить на Женщину-фишку,
И высшее счастье - вскочить среди ночи
С заботой о Женщине-тамагочи.
Вежливый интерфейс, или принципы создания диалогов
Дорогой Тог,
Хочу предложить новое правило разработки интерфейсов, которое я не нашел среди ваших. Оно о программах, состязающихся за внимание пользователя. Бесчисленное количество раз во время набора текста (щелчков мышью, и т.д.) меня прерывала программа, выпрыгнувшая на первый план только для того, чтобы сообщить, что она начала или закончила какую-то работу. Конечно, иногда я хотел бы, чтобы меня уведомляли, но не отрывая от моей текущей работы.
Правило можно назвать "знай свое место" или "не прерывай пользователя". Оно связано с тем, что вы писали о фоновом выполнении задач, но я думаю, что его нужно сделать отдельным правилом.
Посоветуйте, как следовать этому правилу наилучшим образом? Может быть, ждать пока я не перестану печатать и двигать мышью?
С наилучшими пожеланиями, Par
--- Par Olsson, Icon Medialab
Действительно, как верно отметил Par, нужно знать свое место.
Ошибки, смены состояний, и все остальные сообщения могут быть более вежливыми, чем их делают большинство из нас. Разрабатывая очередную программу, учитывайте следующие принципы:
- Не прерывайте пользователя модальным диалогом до тех пор, пока он не будет в состоянии продолжить работу, не предприняв некоторого действия.
- Предлагайте пользователю обратную связь, не прерывая его.
Например, представим , что вы произвели поиск по запросу пользователя и теперь должны сообщить о результате. Представим, что этот поиск необходим для заполнения одного из полей на форме пользователя, как например адрес человека, кому вы должны послать ее, полученный из адресной книги. Вместо того, чтобы трубить об успешном результате, просто заполните это поле. Если требуется дальнейшая обратная связь, сделайте желтую иконку, мигающую во время поиска. В случае успешного результата смените цвет на зеленый, в случае неудачи - на красный. Если форма достаточно большая, пользователь может в это время находиться в другом разделе, поэтому поместите где-нибудь индикатор состояния для всей формы.Индикатор статуса в форме иконки может обозначать следующее: "где-то на этой форме поле помечено красным. Нажмите, чтобы найти его". Когда пользователь закончит заполнять форму и увидит зеленый индикатор, он поймет, что можно идти дальше.
- Используйте само-срабатывающие диалоги.
Например, диалоги печати спрашивают пользователя, сколько копий ему нужно, и т.д. Затем они сидят на экране в течении следующих трех дней в ожидании ответа. Пользователь ушел на обед, забыв, что должен появиться этот глупый диалог, и ждет что к его приходу 500-страничный документ будет напечатан. Попробуйте изменить это! Предположите, что пользователю нужна одна копия. Когда пройдет пара минут, начните печатать. Даже если будет один шанс из милиона, что пользователю нужны две копии, он всегда может отпечатать еще одну, или даже сделать копию на копировальном аппарате. В любом случае, время не будет потеряно.
Те из вас, кто видел мой фильм "Starfire" наверное заметил диалоговое окно, появившееся перед Джулией, с сообщением "Словесная команда не распознана". Через пару секунд окно исчезает. Как оно узнало о том, что нужно исчезнуть? Система отслеживала движение ее глаз, и увидев, что она прочитала сообщение, закрыла окно.
Мы еще очень далеки от этого (хотя технология уже достигла такого уровня). Тем не менее, думайте о своих сообщениях, как о советах ценного помошника. Делайте их вежливыми, полезными и прерывающими пользователя только если это необходимо.
По материалам сети интернет