Залепа №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, которые (как обычно) лучше нас с вами знают что и как должно работать.
Адрес заметки: http://fit-media.com/post_1204892317.html
Если вы не можете отправить комментарий, то прочтите как это исправить здесь
Обязательные для заполнения поля помечены карандашом.
email при указании не будет опубликован.
Адреса с http:// преобразуются в ссылки автоматически.
Для этого отделяйте их от текста ПРОБЕЛАМИ с обеих концов.
Теги запрещены.