Исключения
От: faa  
Дата: 14.05.13 10:22
Оценка:
Доброго времени суток!

В настоящий момент я разрабатываю библиотеку классов предоставляющую API для логирования сообщений в таблицу БД MS SQL Server.
Изучив тему обработки исключений в C# саомостоятельно по книге Герберта Шилдта: "С# 4.0 Полное руководство" и засучив рукава начал применять полученные знания на своей рабочей задачи, но позже наткнулся на раздел MSDN: http://msdn.microsoft.com/ru-ru/library/ms229014%28v=vs.100%29.aspx и понял, что всё сделаное мной противоречит практически всем основным рекомендация там приведённым. Вот кратко основные из них:

1. Создавайте и генерируйте пользовательские исключения, если у Вас возникает ошибка, которую можно обработать программным путем иным способом, чем существующие исключения. В противном случае генерируйте одно из существующих исключений.
2. Не создавайте и не генерируйте новых исключений, просто чтобы иметь свои собственные исключения.
3. Генерируйте наиболее специфичные (самые отдаленные от базовых) исключения, которые подходят в конкретной ситуации. Например, если метод принимает null-аргумент (Nothing в Visual Basic), он должен генерировать исключение System.ArgumentNullException вместо базового типа System.ArgumentException.
4. Особые исключения, создаваемые кодом нижнего уровня, рекомендуется заключать в оболочку более подходящего исключения, если исключение нижнего уровня не имеет смысла в контексте операции высокого уровня.
5. Не перехватывайте исключение System.Exception или System.SystemException в своем коде, только если не планируется его повторный вызов. Избегайте перехватов исключений System.Exception или System.SystemException, за исключением обработчиков исключений высокого уровня.

Теперь фрагмент из моего проекта:


 public Category GetCategory(string CategoryName)
        {
            try
            {
                if( !DataBase.ValidateArgument(CategoryName) )
                    throw new JournalProvaiderException("Ошибка получения категории журнала. " +
                                                        "Не верный формат имени категории журнала.", null);
                
                //Если экземпляр запрошенной категории журнала уже создан,
                //возвращаем ссылку на него 
                if (_CatInstances.ContainsKey(CategoryName))
                    return _CatInstances[CategoryName];

                if (!IsConnected)
                    throw new JournalProvaiderException("Ошибка получения категории журнала. " +
                                                        "Не установлено подключение к серверу журналов.", null);
                
                //Проверка существоваония категории журнала в БД сервера журналов
                if (!IsJournalExist(CategoryName))
                    throw new JournalProvaiderException("Катгория журнала с именем: <" + CategoryName + 
                                                        "> не существует на сервере журналов.", null);
                
                Category _Category = null;

                //Выбор блока обработки категории журнала
                switch (CategoryName)
                {                
                    case ("Информационные сообщения"): 
                
                        //Входные параметры хранимой процедуры
                        SQLParametr[] InputParamList = new SQLParametr[1];
                        InputParamList[0].Name = "@CatName";
                        InputParamList[0].Value = CategoryName;

                        //Возвращаемое хранимой процедурой значение
                        SqlParameter ReturnValue;

                        //Получить описание категории журнала из БД
                        using (SqlDataReader Reader = DataBase.ExecuteSQL(_SQLConnection, "sp_GetCategories",
                                                                          InputParamList, out ReturnValue, false))
                        {
                            if (!Reader.HasRows)
                            {
                                Reader.Close();
                                throw new JournalProvaiderException("Не удалось получить описание категории журнала <" + CategoryName +
                                                                    "> от сервера журналов! (sp_GetCategories return " +
                                                                    ReturnValue.Value.ToString() + " ).", null);
                            }
                            //Получить аттрибуты записи Категори журнала
                            if (Reader.Read())
                                _Category = new Category(
                                                            Reader.GetSqlString(0).Value,
                                                            Reader.GetSqlString(1).Value,
                                                            Reader.GetSqlString(2).Value,
                                                            Reader.GetSqlBoolean(3).Value,
                                                            Reader.GetSqlInt32(4).Value,
                                                            Reader.GetSqlInt32(5).Value
                                                         );
                            else
                                throw new JournalProvaiderException("Сбой при получении аттрибутов записи " +
                                                                    "категории журнала <" + CategoryName + ">.", null);
                            Reader.Close();
                        }
                    break;
             
                    default:                
                        throw new JournalProvaiderException("Запрошенная категория журнала: <" + CategoryName + 
                                                            "> не поддерживается бибилотекой провайдера.", null);                  
                }
                
                //Добавляем ссылку на категорию журнала в коллекцию 
                _CatInstances.Add(CategoryName, _Category);
                return _Category;            
            }
            catch(SystemException e)
            {
                throw new JournalProvaiderException("Ошибка получения данных категории журнала.", e);
            }
        }


Я размышлял так:
1. Так API библиотеки абстрагирует пользователя библиотеки от БД вводя в свою очередь собственную абстракцию "Сервер жураналов" то
информационные сообщения должны описывать ситуацию в контексте сущности "Сервер журналов", являясь оболочкой для исключений СУБД.
Поэтому создал собственный класс исключения:


    //Определяет исключительную ситуацию
    //в работе провайдера журналов
    public class JournalProvaiderException: Exception
    {
        public JournalProvaiderException() : base() {}
        public JournalProvaiderException(string message) : base(message) { }
        public JournalProvaiderException(string message, Exception innerException) : base(message, innerException) { }
        public JournalProvaiderException(SerializationInfo info, StreamingContext context) : base(info, context) { }
        public override string ToString()
        {
            return "JournalProvaiderException: < " + base.Message + " >\n" +
                    "Source: < " + base.Source + " >\n" +
                    "TargetSite: < " + base.TargetSite + " >\n" +
                    "StackTrace: < " + base.StackTrace + " >\n" + "\n" +
                    //Внутреннее исключение
                    "InnerException: \n<\n" + base.InnerException + "\n>";
        }
    }


2. Все исключения возбуждаемые внути моей библиотеки перехватываются на уровне SystemException и "заворачиваются" в мой класс JournalProvaiderException, чтобы в клиентском коде достаточно было ставить только один "фирменый" перехватчик:



try
{
   //Клиентский код 
}
catch (JournalProvaiderException e)
{
    ...
}



Внутри библиотеки я начал ставить несколько блоков перехвата, так:

            
            try
            {

            }
            catch (SqlNullValueException e)
            {
            }
            catch (OutOfMemoryException e)
            {
            }
            catch (InvalidOperationException e)
            {
            }
            ...


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



            try
            {

            }
            catch (SystemExceptione e)
            {
               throw new JournalProvaiderException("Ошибка получения данных из журнала.", e);
            }


Объясните пожалуйста почему поступать так плохо?
Почему Microsoft даёт рекомендации так не делать (см. выше П.5) ?
Что значит рекомендация: " Создавайте и генерируйте пользовательские исключения, если у Вас возникает ошибка, которую можно обработать программным путем иным способом, чем существующие исключения. В противном случае генерируйте одно из существующих исключений." ?

Помогите разобраться . Заранее благодарен!
Re: Исключения
От: HowardLovekraft  
Дата: 14.05.13 10:45
Оценка:
Здравствуйте, faa, Вы писали:

faa>В настоящий момент я разрабатываю библиотеку классов предоставляющую API для логирования сообщений в таблицу БД MS SQL Server.

Встречный вопрос — зачем? Чем не устроили готовые логгеры?
Re[2]: Исключения
От: faa  
Дата: 14.05.13 10:58
Оценка:
Здравствуйте, HowardLovekraft, Вы писали:

HL>Здравствуйте, faa, Вы писали:


faa>>В настоящий момент я разрабатываю библиотеку классов предоставляющую API для логирования сообщений в таблицу БД MS SQL Server.

HL>Встречный вопрос — зачем? Чем не устроили готовые логгеры?

Начальник задачу поставил
Re[3]: Исключения
От: HowardLovekraft  
Дата: 14.05.13 11:04
Оценка: +1
Здравствуйте, faa, Вы писали:

faa>Начальник задачу поставил

Бывает. Пусть, например, это глянет: https://github.com/nlog/NLog/wiki/Database-target.
Логгер — не такая простая штука, как кажется на первый взгляд. Написание своего велосипеда, который будет стабильно работать, может занять довольно много времени.
Re[3]: Исключения
От: Sinix  
Дата: 14.05.13 11:40
Оценка:
Здравствуйте, faa, Вы писали:

faa>Начальник задачу поставил


Лучше допилить стандартный TraceSource, написать свои листенеры + добавить к нему методы-расширения для логирования операций. Что-то вроде
public static IDisposable BeginOperation(this TraceSource ...) ...


По теме — ловить имеет смысл только если вы ожидаете именно это конкретное исключение и знаете как его обработать. Т.е. вы ожидаете, что вызов вашего логгера может выглядеть примерно так:
try
{
  SomeCode();
  SomeLoggerCode();
  AnotherCode();
}
catch (JournalProviderException ex) // у вас опечатка была;)
{
  // Тут обрабатываем ошибку работы с логгером, все остальные исключения нас не волнуют.
}


При таком сценарии использования никакого криминала в оборачивании исключений нет. В п5 кривой перевод. Вот тут

Не перехватывайте исключение System.Exception или System.SystemException в своем коде, только если не планируется его повторный вызов.

очевидно подразумевался повторный throw отловленного исключения.

P.S. System.SystemException ловить не имеет смысла вобще. Некоторые "системные" исключения наследуются от System.Exception напрямую, проще ловить его.
Re: Исключения
От: Yoriсk  
Дата: 14.05.13 12:46
Оценка:
Здравствуйте, faa, Вы писали:

faa>В настоящий момент я разрабатываю библиотеку классов предоставляющую API для логирования сообщений в таблицу БД MS SQL Server.


Log4net умеет искаробки. Информации в инете — валом.

faa> throw new JournalProvaiderException("Ошибка получения категории журнала. " +

faa> "Не верный формат имени категории журнала.", null);

Формат имени гуляет с чужой форматкой! И это при живой жене! Какой позор!

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


То и означает. Ошибки сети, подключения к БД и т.п. к "журналу" никакого отношения не имеют.
Вообще, зачем вы так тщательно оборачиваете системные ошибки? Вам(разработчику) кроме стектрейса(ну и параметров возможно) ничего не надо а пользователю пофигу, ошибка коннекта по таймауту или инвалид логин/пассворд, для него это "программа не работает".
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.