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

Преглеждам част от кодова база и стигам до частта за обработка на изключения, която е наистина объркана. Бих искал да го заменя с нещо по-елегантно. Тогава си помислих, че може би не е лоша идея, ако мога да разполагам с плавен интерфейс, който да ми помогне да регистрирам някаква политика за списък с изключения и да оставя 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
Мартин Фаулър категоризира fluent интерфейсите като тип 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)


Това е вторият ми опит да отговоря на въпроса ви. Както разбирам правилно, вие се опитвате да избягате от вложените if и else от код като този:

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), въпреки че "fluent " версията е по-подробна (но DSL и плавните интерфейси не се отнасят до многословност, а до изразителност и четливост).

С други думи, "fluent" версията очаква повече от мен (научете нов API), като същевременно не предоставя предимства в замяна ("fluent" API не е по-изразителен от обикновения C#), което ме кара никога да не искам дори да опитам "течната" версия.

Освен това казвате, че искате да излезете от вложените if. Защо така? В много вградени DSL ние се стремим към влагане там, където по-добре отразява структурата на решението (вижте първия пример от http://broadcast.oreilly.com/2010/10/understanding-c-simple-linq-to.html - това е вграденото на Microsoft DSL за писане на XML). Също така, разгледайте и двата ми примера - умишлено направих разстоянието такова, каквото е, за да ви покажа, че влагането наистина не изчезва, когато превключите на dotted().notation().

Още нещо. „Fluent“ версията може да ви създаде илюзия, че сте по-„декларативни“, като предварително конфигурирате обект с правила, които той ще упражнява, когато е необходимо, но това всъщност не е по-различно от това да вземете „обикновената C#“ версия и да я поставите в отделете обект или метод и извикайте този метод, когато възникне нужда. Поддържаемостта е абсолютно същата (всъщност версията "обикновен C#" вероятно би била по-поддържаема, защото с версията "fluent" ще трябва да разширявате 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, авторите на класическите Design Patterns. Този съвет е: „Инкапсулирайте това, което варира“. във вашия случай обработката на грешки е това, което варира и вариацията се основава на типа изключение. Как да го приложа тук?

Първо решение - преработете напълно миризмите от кода

Първият въпрос, който бих задал, е следният: свободен ли сте да променяте кода, който хвърля изключенията? Ако е така, бих се опитал да капсулирам не вътре в кода, където улавяте изключенията, но вътре в кода, който ги хвърля. Това може да изглежда странно за вас, но това може да ви позволи да избегнете излишък. Както е, вашият код в момента е свързан с тип изключение на две места.

Първото място е мястото, където го хвърляте (трябва да знаете кое изключение да хвърлите) - силно опростено, може да изглежда така:

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

И това е. Кажете ми, ако ви помага по някакъв начин!

person Grzesiek Galezowski    schedule 27.01.2013