Каков рекомендуемый подход для связи контекста с исключениями, генерируемыми в приложении пользовательского интерфейса? - PullRequest
2 голосов
/ 04 мая 2011

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

  • База данных находится в автономном режиме.
  • Файл (ранее отсканированный в базу данных) не найден.
  • Файл заблокирован другим пользователем / процессом и недоступен.
  • Атрибут файла доступен только для чтения и файл не может быть изменен.
  • Разрешения безопасности запрещают доступ к файлу (чтение или запись).

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

Допустим, для целей обсуждения у нас есть один метод, который должен выполнять различные действия с файлом; он должен прочитать файл в память, изменить данные и записать данные обратно, как показано в следующем примере:

private void ProcessFile(string fileName)
{
    try
    {
        string fileData = ReadData(fileName);

        string modifiedData = ModifyData(fileData);

        WriteData(fileName, modifiedData);
    }
    catch (UnauthorizedAccessException ex)
    {
        // The caller does not have the required permission.
    }
    catch (PathTooLongException ex)
    {
        // The specified path, file name, or both exceed the system-defined maximum length.
        // For example, on Windows-based platforms, paths must be less than 248 characters
        // and file names must be less than 260 characters.
    }
    catch (ArgumentException ex)
    {
        // One or more paths are zero-length strings, contain only white space, or contain
        // one or more invalid characters as defined by InvalidPathChars.
        // Or path is prefixed with, or contains only a colon character (:).
    }
    catch (NotSupportedException ex)
    {
        // File name is in an invalid format.
        // E.g. path contains a colon character (:) that is not part of a drive label ("C:\").
    }
    catch (DirectoryNotFoundException ex)
    {
        // The path specified is invalid. For example, it is on an unmapped drive.
    }
    catch (FileNotFoundException ex)
    {
        // File was not found
    }
    catch (IOException ex)
    {
        // Various other IO errors, including network name is not known.
    }
    catch (Exception ex)
    {
        // Catch all for unknown/unexpected exceptions
    }
}

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

В приведенном выше примере со всеми предложениями catch для исключения мы все равно не знаем, какое действие (контекст) приведет к исключению. Возникла ли исключительная ситуация при открытии и чтении файла, при изменении данных или при записи изменений обратно в файловую систему?

Один из подходов - переместить блок try / catch в каждый из этих методов «действия». Это будет означать копирование / повторение одной и той же логики обработки исключений во все три метода. И, конечно, чтобы избежать повторения одной и той же логики в нескольких методах, мы могли бы инкапсулировать обработку исключений в другой общий метод, который вызвал бы перехват универсального System.Exception и его передачу.

Другой подход заключается в добавлении «enum» или других средств определения контекста, чтобы мы знали, где произошло исключение, следующим образом:

public enum ActionPerformed
{
    Unknown,
    ReadData,
    ModifyData,
    WriteData,
    ...
}

private void ProcessFile(string fileName)
{
    ActionPerformed action;

    try
    {
        action = ActionPerformed.ReadData;
        string fileData = ReadData(fileName);

        action = ActionPerformed.ModifyData;
        string modifiedData = ModifyData(fileData);

        action = ActionPerformed.WriteData;
        WriteData(fileName, modifiedData);
    }
    catch (...)
    {
        ...
    }
}

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

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

Ответы [ 5 ]

1 голос
/ 05 мая 2011

Мое мнение таково, что подход MS к локализации свойства сообщения об исключениях совершенно неверен.Поскольку существуют языковые пакеты для .NET Framework, вы получаете из загадочных (например, Mandarin) китайских сообщений об установке.Как я должен отлаживать это как разработчик, который не является носителем языка развернутого продукта?Я бы зарезервировал свойство сообщения об исключении для текста сообщения, ориентированного на технического разработчика, и добавил бы пользовательское сообщение в его свойство Data.

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

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

  1. Форма входа
  2. Веб-служба
  3. Серверная часть
  4. База данныхУровень доступа

    1. IResult res = WebService.LoginUser ("user", "pwd");
    2. IResult res = RemoteObject.LoginUser ("user", "pwd");
    3. string pwd = QueryPasswordForUser ("user");
    4. User user = NHibernate.Session.Get ("user");-> SQLException выбрасывается

База данных выдает SQLException, поскольку БД находится в режиме сопровождения.

В этом случаеbackend (3) все еще имеет достаточно контекста для решения проблем с БД, но он также знает, что пользователь попытался войти в систему.

Пользовательский интерфейс получит через веб-сервис другой объект исключения, поскольку идентификация типа не можетбыть сохраненным через границы AppDomain / Process.Более глубокая причина заключается в том, что на удаленном клиенте не установлены NHibernate и SQL-сервер, что делает невозможным передачу стека исключений посредством сериализации.

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

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

 catch(SqlException ex)  
 {
    if( ex.ErrorCode == DB.IsInMaintananceMode ) 
       Display("Database ??? on server ??? is beeing maintained. Please wait a little longer or contact your administrator for server ????");
    ....

Из-за границы веб-сервиса в действительности это будет больше похоже на

 catch(Exception ex)
 {
       Excepton first = GetFirstException(ex);
       RemoteExcepton rex = first as RemoteExcepton;
       if( rex.OriginalType == "SQLException" )
       {
           if( rex.Properties["Data"] == "DB.IsMaintainanceMode" )
           {
              Display("Database ??? on server ??? is beeing maintained. Please wait a little longer or contact your administrator for server ????");

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

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

   catch(SQLException ex)
   {
       ex.Data["UserMessage"] = MapSqlErrorToString(ex.ErrorCode, CurrentHostName, Session.Database)'
       throw;
   }

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

Ваш, Алоис Краус

1 голос
/ 04 мая 2011

Обычно мы регистрируем исключение (с помощью log4net или nlog), а затем генерируем пользовательское исключение с дружественным сообщением, которое может понять пользователь.

1 голос
/ 04 мая 2011

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

0 голосов
/ 04 мая 2011

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

0 голосов
/ 04 мая 2011

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

catch (NotSupportedException ex)
    {
        YourCustomExceptionClass exception = new YourCustomExceptionClass(ex.message);
        throw exception;
    }

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

EDIT:

На самом деле, вы можете сделать исключение членом вашего класса пользовательских исключений и сделать это.

catch (NotSupportedException ex)
{  
    YourCustomExceptionClass exception = new YourCustomExceptionClass(ex.message);
    exception.yourExceptionMemberofTypeException = ex;
    throw exception;
}

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...