Как разработать свободный интерфейс (для обработки исключений)?

Я просматриваю часть базы кода и перехожу к части обработки исключений, которая действительно запутана. Я бы хотел заменить его на что-то более элегантное. Затем я подумал, что было бы неплохо иметь гибкий интерфейс, который помог бы мне зарегистрировать некоторую политику для списка исключений и позволить 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...
    ...
}

person Beatles1692    schedule 22.02.2012    source источник
comment
Что бы вы купили за беглый интерфейс?   -  person R0MANARMY    schedule 22.02.2012
comment
andrevianna .com/blog/index.php/2010/08/   -  person Robert Harvey    schedule 22.02.2012
comment
это поможет мне предотвратить много вложенных if, и код будет легко поддерживаться.   -  person Beatles1692    schedule 22.02.2012
comment
Вы можете добиться большего успеха, удалив обработку исключений. Разместите несколько примеров, и мы посмотрим. Кроме того, при чем тут DSL?   -  person John Saunders    schedule 22.02.2012
comment
Мартин Фаулер классифицирует плавные интерфейсы как тип DSL.   -  person Beatles1692    schedule 22.02.2012
comment
Ну, я думаю, Мартин прав не во всем.   -  person John Saunders    schedule 22.02.2012
comment
Мы говорим о предметно-ориентированных языках, не так ли? :)   -  person Beatles1692    schedule 22.02.2012
comment
@JohnSaunders: Это форма DSL для бедняков для нас, людей, у которых нет возможности макросов во время выполнения (например, программистов на C#).   -  person Robert Harvey    schedule 22.02.2012


Ответы (3)


Это моя вторая попытка ответить на ваш вопрос. Как я правильно понимаю, вы пытаетесь уйти от вложенных 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. Если вы сравните " fluent" с "обычным C#", "обычный C#" на самом деле более выразительный (меньше символов) и более читабельный (я уже знаю C#, но еще не знаком с вашим API), хотя "беглый версия более многословна (но DSL и плавные интерфейсы — это не многословие, а выразительность и удобочитаемость).

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

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

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

Итак, мой вывод таков: если вам нужен DSL общего назначения для запуска действий, основанных на некоторых сравнениях объектов, тогда остановитесь — C# с его операторами «if», «else», «try» и «catch» уже хорош для этого. это и выгода от того, чтобы сделать его «беглым», - иллюзия. Языки, специфичные для предметной области, используются для обертывания специфичных для предметной области операций за выразительным 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();
}
person Grzesiek Galezowski    schedule 28.01.2013

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

person toeb    schedule 09.12.2013

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

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

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

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

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

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

Первое решение — полностью убрать запахи из кода.

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

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

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

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

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

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

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();

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

person Grzesiek Galezowski    schedule 27.01.2013