• Главная
  • Оглавление
  • Обратная связь
  • Лента RSS
  • Правила
Что здесь уже нашли

Залепа №13. О грамотном проектировании и неграмотном IDE.

07 марта 2008, 07:18

Эту статью без мата в адрес мелкомягких писать сложно :( Но я все же постараюсь.

Задача:

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

Каждая запись справочника содержит данные об одном пользователе: его логин, пароль для входа в систему и какие-то дополнительные атрибуты. Например его права доступа. В программе, кроме основных функций, должна быть возможность как редактирования списка пользователей так и изменения параметров отдельных пользователей.

Задача, согласитесь, тривиальна и встречается на каждом шагу. Попробуем ее решить средствами C#. Вариантов решений может быть масса, но нас интересует грамотный подход, т.е. такое решение, при котором наша программа не превратится в кашу из кучи классов, лишних открытых методов, свойств и форм.

Здравый смысл подсказывает, что справочник должен быть представлен неким классом (назовем его CUserMgr), который позволяет основной программе получить все необходимые данные о пользователе и скрывает всю внутреннюю структуру внутри себя.

Опять же для простоты решим, что программе нужно получать только лишь логин и пароль пользователя по его идентификатору. Ну и конечно вызывать редактор пользователей. Таким образом у нас получается примерно такой открытый интерфейс нашего класса:

Набросок класса
public class CUserMgr
{
	// получить идентификатор по логину
	public int GetID(string login) {...}
	
	// получить логин по идентификатору
	public string GetLogin(int id) {...}
	
	// получить пароль по идентификатору
	public string GetPassword(int id) {...}
	
	// вызвать редактор пользователей
	public void ShowEditor() {...}
}

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

Редактор пользователей должен отобразить на экране окно списка пользователей, в котором можно добавить нового пользователя и удалить или отредактировать существующего. Для целей добавления/редактирования будет использоваться еще одно окошко - форма редактирования пользователя. Итак, получаем как минимум две формы, которые должны напрямую обращаться к внутренней структуре класса CUserMgr, т.е. к его закрытым методам.

Решение "в лоб", т.е. создание глобальных классов нужных нам форм ни к чему хорошему не приведет:

  1. о существовании этих форм должен знать только CUserMgr, остальные о них не должны даже подозревать, т.к. никому кроме CUserMgr они не нужны. У нас же они получаются глобальными, т.е. видимыми всеми классами. Это плохо.
  2. Чтобы формы могли обратиться к внутренней структуре класса 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) он содержит внутри себя. Он их будет создавать и удалять (редактирование списка пользователей), получать из них информацию и передавать ее в основную программу.

Вот примерно такой подход.

Согласитесь, что такая убогая симуляция дружественности классов имеет массу недостатков:

  1. Нормальной инкапсуляции все равно не получится, т.к. в реальной программе некоторые закрытые методы как CUser так и CUserMgr все же придется открыть.
  2. Класс CUser все еще глобален и видим любым и каждым со всеми вытекающими отсюда отрицательными последствиями.
  3. Для справочника пользователей, которых не больше сотни этот подход будет работать более-менее нормально. Но если это будет справочник артикулов продукции, которых может быть несколько десятков тысяч, то мы получи дикий перерасход памяти, ведь для каждого объекта будет создано отдельное (пусть и скрытое) окно. Это вызовет не только расход памяти, но и будет существенно тормозить программу.

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

Вариант номер два (реально объектно-ориентированный)

Второй вариант использует возможность 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() {...}
}

Итак, вот что мы получаем при таком подходе:

  1. программа "видит" только класс CUserMgr, о существовании внутренних классов она даже не подозревает и естественно не может получить к ним доступ. Она может использовать только открытый "внешний интерфейс" класса CUserMgr. Внутренняя структура нашего менеджера пользователей полностью скрыта (инкапсуляция торжествует!).
  2. Можно как угодно изменять внутреннюю структуру CUserMgr и вложенных классов. Если внешний интерфейс не изменится, то эти модификации никак не скажутся на работе основной программы, использующей наш класс.
  3. Встроенные классы могут без проблем обращаться к внутренним (закрытым) методам класса CUserMgr, что позволяет в ряде случаев сильно упростить программирование этих классов.

В общем мы добились нормального решения поставленной задачи. Поздравляю.

"А где же косяк?" - спросите вы - "неужели microsoft наконец-то сделала все правильно?" :)

А косяк все же есть. Но на этот раз он не в языке, а в IDE Visual Studio.

Классы, созданные по описанной выше "технологии" будут нормально компилироваться и работать (а как же иначе - все ведь сделано верно, в соответствии с требованиями и возможностями языка C#). Но в IDE вы не сможете использовать визуальный редактор для форм, которые являются вложенными в другие классы (в нашем случае это CUserEdit и CUserList) - IDE вместо отображения формы просто будет материться по-английски. Поэтому проектирование пользовательского интерфейса превратится при описанном подходе в настоящую муку. :(

Почему так происходит, ведь с точки зрения языка все верно и никакого "криминала" в таком вложении нет? Этот вопрос нужно адресовать разработчикам Visual Studio, которые (как обычно) лучше нас с вами знают что и как должно работать.


№ 1: Залепа 13

Может я чего-то недопонял,но почему нельзя объявить,например,тот же CUser наследником класса CUserMgr?
Зачем его делать вложенным в класс CUserMgr?

Ray (07 марта 2008, 09:08)
№ 2: Без темы

А что даст такое наследование? Ведь при этом получится, что класс "пользователь" порожден из класса "список пользователей", что мягко говоря, глупо.

Аффтор (07 марта 2008, 12:11)
№ 3: Без темы

Непонятно зачем определять класс внутри другого класса.Только для того чтобы члены вложенного класса имели доступ к членам класса-оболочки? А смысл разве не тот же,что при наследовании?

Ray (07 марта 2008, 13:07)
№ 4: Без темы

Конечно не тот!

При определении класса А внутри класса Б, класс А получает доступ ко всем методам класса Б включая закрытые и защищенные (аналогично friend-классам в C++). При этом классы остаются независимыми.

При наследовании класса А от класса Б, класс А наследует методы класса Б. Т.е. в этом случае в класс А как бы встраиваются методы класса Б и класс А фактически становится "классом Б с наворотами"

Подробнее могу объяснить только на бумаге при встрече.

ЗЫ
Блин, разберись с наследованием, а?

Аффтор (07 марта 2008, 15:40)
№ 5: Кошмар!

Нет слов, Аффтар, сплошной мат. И как ОНО после этого (это я про творения мелкомягкой конторы) может работать???

Иван (07 марта 2008, 19:45)
№ 6: Без темы

После объяснения на бумаге все стало на свои места.Теперь понятно.
Вот так вот оно все и работает.Через заднее отверстие...

Ray (09 марта 2008, 11:43)
№ 7: Ага...

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

Алекс (09 февраля 2010, 00:21)
№ 8: Без темы

На самом деле правильный путь это реализация интерфейса IUserManager который дает доступ к хранилищу UserManager , через методы выборки и сохранения объектов User, а Формы редактирования уже должны работать отдельно используя только интерфейс IUserManager наравне с другими частями системы. то есть не должно быть никаких "друзей" и вложенных классов. Весь обмен должен быть ограничен интерфейсом и объектом данных который должен быть ОТДЕЛЕН он логики манипуляции с ним.

Пример:

public interface IUserManager
{
}
public class UserManager : IUserManaer
{
public IUserManager CreateInstance(){ ... }
User IUserManager.GetByLogin( stirng login ) { .... }
}

public UserEditView : Form
{
public void SetManager( IUserManager ){...}

}


Если необходимо спрятать Всю подсистему управления пользователями, то
можно и интерфейс убрать в internal и вынести всё в персональную сборку.
А наружу будет торчать только UserEditView c публичным фарбричным методом.

comm (21 мая 2010, 06:30)
№ 9:

Спасиьбо за комментарий. Ваше решение лежит на повержности, но это тоже самое, что и "решение номер раз" в статье, только с использованием интерфейсов. На мой взгляд, в данном случае использование интерфейса - скорее костыль, нежели элегантное решение. Причина в проектировании структуры классов, а точнее в том, что у нас с Вами разные взгляды на это самое проектирование. Вы придерживаетесь навязываемой майкрософтом системы "отталкивания от окна" - проектирование, где фундаментальным объектом являются части интерфейса - окна и т.д.

Мне же больше по душе проектирование "на основе сущностей", при котором объекты отражают некие (пусть и виртуальные) сущности, но при этом не ориентированы на базирование на чем-либо. Для того чтобы управлять пользователями я не хочу гиспользовать два класса (модель и представление - UserEditView и UserManager) - это излишне сложно, вполне достаточно одного. К тому же в моем решении отпадает необходимость в "ручной инициализации" представления (почему пользователь библиотеки управления пользователями должен заморачиваться на таких ненужных вещах???)...

Вот тут чуть подробнее об этом: http://fit-media.com/post_1199568462.html

Admin of FIT-Media Blog
Адрес заметки: http://fit-media.com/post_1204892317.html
Ваш комментарий к статье

 cod


Примечание:
Все поля обязательны для заполнения!
Введенный email не будет опубликован.
Адреса с http:// преобразуются в ссылки автоматически.
Для этого отделяйте их от текста ПРОБЕЛАМИ с обеих концов.
Теги запрещены.

Календарь

март, 2008
пн вт ср чт пт сб вс
          1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
31            

Меню

  • Главная страница
  • Оглавление блога
  • Лента новостей
  • Обратная связь
  • Правила блога

Анонсы по темам

  • Все посты блога
  • С миру по нитке
  • Мысли вслух
  • Графика и фото
  • Кривизна платформы .NET
  • Грамотные интерфейсы
  • WEB-программирование
  • FlatCMS - шустрая и гибкая
  • Доработки Lasto-блога

Категории

  • Все посты по порядку
  • С миру по нитке
  • Графика и фото
  • Кривизна платформы .NET
  • Грамотные интерфейсы
  • WEB-программирование
  • FlatCMS - шустрая и гибкая
  • Доработки Lasto-блога

Сервисы

  • Поиск по блогу
  • Поиск по всему сайту
  • Шпионское досье

Реклама


Стоимость сайта

Мой вебсайт стоит 865 404,18 руб

Статистика

    Widgetize!
  • Время работы: 0,01721 сек.
  • Память: 4 864 кБт
  • Статистика привратника
Copyright FIT-Media.com, © 2007-2012
Главная | Общее оглавление | Обратная связь | Правила блога | Лента RSS