Как разработать свободный интерфейс (для обработки исключений)? - PullRequest
2 голосов
/ 22 февраля 2012

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

Вот пример того, как это должно работать:

For<TException>.RegisterPolicy<TPolicy>(a lambda expression that describes the detail);

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


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

if(exp.GetType()==typeof(expType1))
{
    if(exp.Message.Include("something went bad"))
    // do list of things things like perform logging to database 
    // and translating/reporting it to user
}
else if (exp.GetType()==typeof(expType2))
{
    //do some other list of things...
    ...
}

Ответы [ 3 ]

2 голосов
/ 28 января 2013

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

if(exp.GetType()==typeof(expType1))
{
    if(exp.Message.Include("something went bad"))
    {
      if(exp.InnerException.Message == "Something inside went bad as well";
      {
        DoX();
        DoY();
      }
    }
}
else if (exp.GetType()==typeof(expType2))
{
  DoZ();
  DoV();
}

Теперь рассмотрим, что вы создали цепочечный API, который выглядит так:

var handlingManager = new ExceptionHandlingManager();
handlingManager
 .For<Exception>()
   .HavingAMessage(message => message.Includes("something went bad"))
   .WithInnerException<SomeInnerException>()
     .HavingAMessage(innerMessage => innerMessage == "Something inside went bad as well")
   .Perform(() => 
   {
     DoX();
     DoY();
   });

Или даже что-то похожее на это:

var handlingManager = new ExceptionHandlingManager();
handlingManager
 .For<Exception>()
   .HavingAMessageThatIncludes("something went bad")
   .WithInnerException<SomeInnerException>()
     .HavingAMessageEqualTo("Something inside went bad as well")
   .Perform(() => 
   {
     DoX();
     DoY();
   });

Оба они на самом деле ничего вам не покупают.Давайте быстро перечислим две функции для встраиваемых домен-специфических языков:

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

Теперь важно то, что если вы создали язык для настройки действий на основе некоторых свойств объекта (как два примера, которые я вам далвыше), он будет соответствовать пункту 1, но не будет соответствовать пункту 2. Если сравнить «текущие» версии с «обычным C #», то «обычный C # один» на самом деле более выразителен (меньше символов) и более читабелен(Я уже знаю C #, но я еще не знаю ваш API), хотя «свободная» версия более многословна (но DSL и свободные интерфейсы не о многословности, они о выразительности и удобочитаемости).

Другими словами, «свободная» версия ожидает от меня большего (изучите новый API), но не дает никаких преимуществ взамен («свободный» API не более выразителен, чем простой C #), что делаетя никогда не хочу даже пробовать "свободную" версию.

Кроме того, вы говорите, что хотите избавиться от вложенного ifs.Почему это так?Во многих встроенных DSL мы стремимся для вложения, где оно лучше отражает структуру решения (см. Первый пример из http://broadcast.oreilly.com/2010/10/understanding-c-simple-linq-to.html - это встроенный DSL от Microsoft для написания XML).Кроме того, взгляните на оба моих примера - я намеренно сделал интервал, чтобы показать вам, что вложение не исчезает, когда вы переключаетесь на dotted (). Notation ().

Другая вещь,«Свободная» версия может создать у вас иллюзию, что вы более «декларативны», предварительно настроив объект с правилами, которые он будет выполнять при необходимости, но это на самом деле ничем не отличается от использования «простой версии C #» и помещения ее вотделите объект или метод и вызовите этот метод, когда возникнет такая необходимость.Сопровождаемость точно такая же (на самом деле, версия "обычного C #", вероятно, будет более удобной в обслуживании, потому что в "беглой" версии вам придется расширять API новыми методами каждый раз, когда вы сталкиваетесь с делом, которое еще не обработанопо API).

Итак, мой вывод таков: если вам нужен универсальный DSL для запуска действий, основанных на сравнении некоторых объектов, то остановитесь - C # с его "if", "else", "Попытки «и« поймать »уже хороши в этом, и выигрыш от того, чтобы сделать его« беглым », является иллюзией.Специфичные для домена языки используются для переноса специфичных для домена операций за выразительным API, и ваш случай выглядит не так.

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

if(situationA)
{
  throw Exception("XYZ");
}
else if (situationB)
{
  throw Exception("CVZF");
}

сделайте это:

if(situationA)
{
  throw ExceptionXYZ();
}
else if (situationB)
{
  throw ExceptionCVZF();
}

Тогда вам не понадобятся вложенные ifs - ваша обработка исключений будет:

try
{
  XYZ()
}
catch(ExceptionXYZ)
{
  DoX();
  DoY();
}
catch(ExceptionCVZF)
{
  DoZ();
  DoV();
}
2 голосов
/ 09 декабря 2013

Я написал простой беглый обработчик исключений. Это легко растяжимо. Вы можете посмотреть его здесь: http://thetoeb.wordpress.com/2013/12/09/fluent-exceptionhandling/ возможно, его можно настроить под вашу цель.

0 голосов
/ 27 января 2013

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

В своем вопросе вы говорите следующее:

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

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

Ответ будет длинным, и я буду ссылатьсяс некоторыми качествами кода, так что если вам нужны дальнейшие объяснения, просто дайте мне знать.Поскольку в принятии такого решения участвуют многие переменные (например, стоимость изменений, владение кодом и т. Д.), Я постараюсь предоставить вам самое «чистое» решение, а затем решение, требующее минимальных усилий.

ВВ таких ситуациях хорошо бы воспользоваться рекомендациями Gang of Four, авторов классических шаблонов проектирования.Этот совет: «Инкапсулируйте то, что меняется».в вашем случае это обработка ошибок, и это зависит от типа исключения.Как бы я применил это здесь?

Первое решение - полностью изменить запах кода

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

Первое место - это место, где вы его выбросили (вы должны знать, какое исключение выбрасывать) - очень упрощенно, это может выглядетьэто:

 if(someSituationTakesPlace())
 {
   throw new ExpType1();
 }
 else if(someOtherSituationTakesPlace()
 {
   throw new ExpType2();
 }

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

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

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

public class Failure : Exception
{
  IFailureHandling _handling;

  public Failure(IFailureHandling handling)
  {
    //we're injecting how the failure should be handled
    _handling = handling;
  }
  //If you need to provide additional info from 
  //the place where you catch, you can use parameter list of this method
  public void Handle() 
  {
    _handling.Perform();
  }
}

Затем я бы создал фабрику, которая создает такие исключения, связывая их, уже в этой точке , собработчики.Например:

public class FailureFactory
{
  IFailureHandling _handlingOfCaseWhenSensorsAreDown,
  IFailureHandling _handlingOfCaseWhenNetworkConnectionFailed

  public FailureFactory(
    IFailureHandling handlingOfCaseWhenSensorsAreDown,
    IFailureHandling handlingOfCaseWhenNetworkConnectionFailed
    //etc.
    )
  {
    _handlingOfCaseWhenSensorsAreDown 
      = handlingOfCaseWhenSensorsAreDown;
    _handlingOfCaseWhenNetworkConnectionFailed 
      = handlingOfCaseWhenNetworkConnectionFailed;
    //etc.
  }

  public Failure CreateForCaseWhenSensorsAreDamaged()
  {
    return new Failure(_handlingOfCaseWhenSensorsAreDown);
  }

  public Failure CreateForCaseWhenNetworkConnectionFailed()
  {
    return new Failure(_handlingOfCaseWhenNetworkConnectionFailed);
  }
}

Обычно вы создаете только одну такую ​​фабрику для всей системы и делаете это там, где вы создаете экземпляры всех долго работающих объектов (как правило, в приложении есть одно такое место), поэтому, покасоздавая фабрику, вы должны иметь возможность передавать все объекты, которые вы хотите, чтобы она использовала через конструктор (как это ни странно, это создаст очень примитивный беглый интерфейс. Помните, что беглые интерфейсы - это читабельность и поток, а не только вставка .a.dot.every.method.call: -):

var inCaseOfSensorDamagedLogItToDatabaseAndNotifyUser
  = InCaseOfSensorDamagedLogItToDatabaseAndNotifyUser(
      logger, translation, notificationChannel);
var inCaseOfNetworkDownCloseTheApplicationAndDumpMemory 
  = new InCaseOfNetworkDownCloseTheApplicationAndDumpMemory(
      memoryDumpMechanism);

var failureFactory = new FailureFactory(
  inCaseOfSensorDamagedLogItToDatabaseAndNotifyUser,
  inCaseOfNetworkDownCloseTheApplicationAndDumpMemory
);

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

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

if(sensorsFailed())
{ 
  throw _failureFactory.CreateForCaseWhenSensorsAreDamaged();
}

И место, гдевы ловите все эти исключения будут выглядеть так:

try
{
  PerformSomeLogic();
} 
catch(Failure failure)
{
  failure.Handle();
}

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

Второе решение - использовать обработчик

В случае, если вам не принадлежит код, который генерирует исключения, или было бы слишком дорого или слишком рискованно внедрять решение, описанное выше, я бы использовал объект Handler, который выглядел бы как FailureFactory, но вместо этого создания объектов, он сам выполняет обработку:

public class FailureHandlingMechanism
{
  _handlers = Dictionary<Type, IFailureHandling>();

  public FailureHandler(Dictionary<Type, IFailureHandling> handlers)
  {
    _handlers = handlers;
  }

  public void Handle(Exception e)
  {
    //might add some checking whether key is in dictionary
    _handlers[e.GetType()].Perform();
  }
}

Создание такого механизма обработки уже дало бы вам очень примитивный свободный интерфейс:

var handlingMechanism = new HandlingMechanism(
  new Dictionary<Type, IFailureHandling>()
  {
    { typeof(NullPointerException), new LogErrorAndCloseApplication()}},
    { typeof(ArgumentException}, new LogErrorAndNotifyUser() }
  };

Если вы хотите еще более плавный и менее шумный способ настройки такого механизма обработки, вы можете создать конструктор на основе HandlingMechanism, который имеет методы для добавления ключей и значений в словарь и метод с именем Build (), который создал объект для вы:

var handlingMechanismThatPerforms = new HandlingMechanismBuilder();
var logErrorAndCloseApplication = new LogErrorAndCloseApplication();
var logErrorAndNotifyUser = new LogErrorAndNotifyUser();

var handlingMechanism = handlingMechanismThatPerforms
  .When<NullPointerException>(logErrorAndCloseApplication)
  .When<ArgumentException>(logErrorAndNotifyUser)
  .Build();

И это все. Дайте мне знать, поможет ли это вам как-нибудь!

...