Какие плавные интерфейсы вы сделали или видели в C #, которые были очень ценными? Что в них такого хорошего? - PullRequest
17 голосов
/ 27 марта 2009

«Свободные интерфейсы» - довольно актуальная тема в наши дни. В C # 3.0 есть несколько приятных функций (особенно методов расширения), которые помогут вам их создавать.

К вашему сведению, свободный API означает, что каждый вызов метода возвращает что-то полезное, часто тот же объект, для которого вы вызывали метод, так что вы можете продолжать связывать вещи. Мартин Фаулер обсуждает это с примером Java здесь . Концепция выглядит примерно так:

var myListOfPeople = new List<Person>();

var person = new Person();
person.SetFirstName("Douglas").SetLastName("Adams").SetAge(42).AddToList(myListOfPeople);

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

Итак, я хотел бы узнать больше, посмотрев и обсудив несколько прекрасных примеров. Итак, Какие отличные беглые интерфейсы вы создали или видели в C #, и что сделало их такими ценными?

Спасибо.

Ответы [ 11 ]

9 голосов
/ 27 марта 2009

Это первый раз, когда я услышал термин "свободный интерфейс". Но на ум приходят два примера: LINQ и неизменные коллекции.

Под прикрытием LINQ - это серия методов, большинство из которых являются методами расширения, которые принимают как минимум один IEnumerable и возвращают другой IEnumerable Это позволяет использовать очень мощный метод создания цепочек

var query = someCollection.Where(x => !x.IsBad).Select(x => x.Property1);

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

var array = ImmutableCollection<int>.Empty.Add(42).Add(13).Add(12);
8 голосов
/ 27 марта 2009

Спасибо за проверку параметров метода, вы дали мне новую идею для наших свободно работающих API. Я все равно ненавидел наши проверки на предварительные условия ...

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

MenuBarController mb;
// ...
mb.Add(Resources.FileMenu, x =>
{
  x.Executes(CommandNames.File);
  x.Menu
    .AddButton(Resources.FileNewCommandImage, Resources.FileNew, Resources.FileNewTip, y => y.Executes(CommandNames.FileNew))
    .AddButton(null, Resources.FileOpen, Resources.FileOpenTip, y => 
    {
      y.Executes(CommandNames.FileOpen);
      y.Menu
        .AddButton(Resources.FileOpenFileCommandImage, Resources.OpenFromFile, Resources.OpenFromFileTop, z => z.Executes(CommandNames.FileOpenFile))
        .AddButton(Resources.FileOpenRecordCommandImage, Resources.OpenRecord, Resources.OpenRecordTip, z => z.Executes(CommandNames.FileOpenRecord));
     })
     .AddSeperator()
     .AddButton(null, Resources.FileClose, Resources.FileCloseTip, y => y.Executes(CommandNames.FileClose))
     .AddSeperator();
     // ...
});

И вы можете настроить все доступные команды следующим образом:

Command(CommandNames.File)
  .Is<DummyCommand>()
  .AlwaysEnabled();

Command(CommandNames.FileNew)
  .Bind(Shortcut.CtrlN)
  .Is<FileNewCommand>()
  .Enable(WorkspaceStatusProviderNames.DocumentFactoryRegistered);

Command(CommandNames.FileSave)
  .Bind(Shortcut.CtrlS)
  .Enable(WorkspaceStatusProviderNames.DocumentOpen)
  .Is<FileSaveCommand>();

Command(CommandNames.FileSaveAs)
  .Bind(Shortcut.CtrlShiftS)
  .Enable(WorkspaceStatusProviderNames.DocumentOpen)
  .Is<FileSaveAsCommand>();

Command(CommandNames.FileOpen)
  .Is<FileOpenCommand>()
  .Enable(WorkspaceStatusProviderNames.DocumentFactoryRegistered);

Command(CommandNames.FileOpenFile)
  .Bind(Shortcut.CtrlO)
  .Is<FileOpenFileCommand>()
  .Enable(WorkspaceStatusProviderNames.DocumentFactoryRegistered);

Command(CommandNames.FileOpenRecord)
  .Bind(Shortcut.CtrlShiftO)
  .Is<FileOpenRecordCommand>()
  .Enable(WorkspaceStatusProviderNames.DocumentFactoryRegistered);

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

Workspace
  .Observe(control1)
  .Observe(control2)

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

Это помогло нам значительно сократить код установки и сделать его еще более читабельным.


Я забыл рассказать о библиотеке, которую мы используем в наших презентаторах моделей WinForms MVP для проверки представлений: FluentValidation . Очень легко, действительно проверяемо, очень приятно!

7 голосов
/ 27 марта 2009

Мне нравится свободный интерфейс в CuttingEdge.Conditions .

Из их образца:

// Check all preconditions: id.Requires("id") .IsNotNull() // throws ArgumentNullException on failure .IsInRange(1, 999) // ArgumentOutOfRangeException on failure .IsNotEqualTo(128); // throws ArgumentException on failure

Я обнаружил, что его намного легче читать, и он намного эффективнее проверяет мои предварительные условия (и условия публикации) в методах, чем когда у меня 50 операторов if для обработки тех же проверок.

4 голосов
/ 27 марта 2009

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

Во-первых, немного предыстории. Я недавно узнал (здесь, в StackOverflow) способ передачи значения методу, чтобы метод мог определять как имя , так и метод. значение . Например, одно общее использование для проверки параметров. Например:

public void SomeMethod(Invoice lastMonthsInvoice)
{
     Helper.MustNotBeNull( ()=> lastMonthsInvoice);
}

Обратите внимание, что нет строки, содержащей "lastMonthsInvoice", что хорошо, потому что строки отстой для рефакторинга. Однако сообщение об ошибке может сказать что-то вроде «Параметр lastMonthsInvoice не должен быть нулевым». Вот пост , который объясняет, почему это работает, и указывает на пост в блоге парня.

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

Console.WriteLine("The property 'lastMonthsInvoice' has the value: " + lastMonthsInvoice.ToString());

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

ConsoleHelper.WriteProperty( ()=> lastMonthsInvoice );

И получите этот вывод:

Property [lastMonthsInvoice] is: <whatever ToString from Invoice

производит>

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

Я хотел, чтобы ConsoleHelper.WriteProperty принимал массив params, чтобы он мог выводить много таких значений свойств в консоль. Для этого его подпись будет выглядеть так:

public static void WriteProperty<T>(params Expression<Func<T>>[] expr)

Чтобы я мог сделать это:

ConsoleHelper.WriteProperty( ()=> lastMonthsInvoice, ()=> firstName, ()=> lastName );

Однако не работает из-за логического вывода типа. Другими словами, все эти выражения не возвращают один и тот же тип. lastMonthsInvoice является счетом. firstName и lastName являются строками. Их нельзя использовать в одном и том же вызове WriteProperty, потому что T не одинаков во всех из них.

Вот где беглый подход пришел на помощь. Я сделал WriteProperty () вернуть что-то. Возвращаемый тип - это то, что я могу назвать And (). Это дает мне такой синтаксис:

ConsoleHelper.WriteProperty( ()=> lastMonthsInvoice)
     .And( ()=> firstName)
     .And( ()=> lastName);

Это тот случай, когда беглый подход позволил что-то, что в противном случае было бы невозможно (или, по крайней мере, не удобно).

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

public static class ConsoleHelper
{
    // code where idea came from ...
    //public static void IsNotNull<T>(Expression<Func<T>> expr)
    //{
    // // expression value != default of T
    // if (!expr.Compile()().Equals(default(T)))
    // return;

    // var param = (MemberExpression)expr.Body;
    // throw new ArgumentNullException(param.Member.Name);
    //}

    public static PropertyWriter WriteProperty<T>(Expression<Func<T>> expr)
    {
        var param = (MemberExpression)expr.Body;
        Console.WriteLine("Property [" + param.Member.Name + "] = " + expr.Compile()());
        return null;
    }

    public static PropertyWriter And<T>(this PropertyWriter ignored, Expression<Func<T>> expr)
    {
        ConsoleHelper.WriteProperty(expr);
        return null;
    }

    public static void Blank(this PropertyWriter ignored)
    {
        Console.WriteLine();
    }
}

public class PropertyWriter
{
    /// <summary>
    /// It is not even possible to instantiate this class. It exists solely for hanging extension methods off.
    /// </summary>
    private PropertyWriter() { }
}
3 голосов
/ 27 марта 2009

Метод именования

Свободные интерфейсы обеспечивают удобочитаемость, если имена методов выбираются разумно.

Имея это в виду, я бы хотел назначить этот конкретный API как "антибеглый":

System.Type.IsInstanceOfType

Это член System.Type, он принимает объект и возвращает true, если объект является экземпляром типа. К сожалению, вы, естественно, склонны читать это слева направо так:

o.IsInstanceOfType(t);  // wrong

Когда на самом деле все наоборот:

t.IsInstanceOfType(o);  // right, but counter-intuitive

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

Инициализаторы объектов

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

Но в C # есть функция языка, которая очень часто делает это ненужным - синтаксис инициализатора объекта:

var myObj = new MyClass
            {
                SomeProperty = 5,
                Another = true,
                Complain = str => MessageBox.Show(str),
            };

Возможно, это объясняет, почему опытные пользователи C # менее знакомы с термином «свободный интерфейс» для объединения вызовов в одном и том же объекте - это не так часто требуется в C #.

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

Ограничения:

  • Установщик свойств может принимать только один аргумент
  • Установщик свойств не может быть универсальным

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

var myObj = new MyClass
            {
                SomeProperty = 5,
                Another = true,
                Complain = str => MessageBox.Show(str),
                DoSomething()
                Click += (se, ev) => MessageBox.Show("Clicked!"),
            };

И почему такой блок модификаций должен применяться только сразу после постройки? Мы могли бы иметь:

myObj with
{
    SomeProperty = 5,
    Another = true,
    Complain = str => MessageBox.Show(str),
    DoSomething(),
    Click += (se, ev) => MessageBox.Show("Clicked!"),
}

* * * * * * * * * * * * * * * * * * * * * * with * * * * * * * * * * * *1049* * * * * * * * * * * * * * * * * * * * * *1049* будет новым ключевым словом, которое работает с объектом некоторого типа и производит тот же объект и тип - обратите внимание, что это будет выражение , а не выражение . Так что это точно отражает идею объединения в «свободный интерфейс».

Таким образом, вы можете использовать синтаксис в стиле инициализатора независимо от того, получили ли вы объект из выражения new или из метода IOC или фабричного метода и т. Д.

На самом деле вы можете использовать with после полного new, и это будет эквивалентно текущему стилю инициализатора объекта:

var myObj = new MyClass() with
            {
                SomeProperty = 5,
                Another = true,
                Complain = str => MessageBox.Show(str),
                DoSomething(),
                Click += (se, ev) => MessageBox.Show("Clicked!"),
            };

И, как Чарли указывает в комментариях:

public static T With(this T with, Action<T> action)
{
    if (with != null)
        action(with);
    return with;
}

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

Эквивалент инициализатора, но с включением события:

var myObj = new MyClass().With(w =>
            {
                w.SomeProperty = 5;
                w.Another = true;
                w.Click += (se, ev) => MessageBox.Show("Clicked!");
            };

И по заводскому методу вместо new:

var myObj = Factory.Alloc().With(w =>
            {
                w.SomeProperty = 5;
                w.Another = true;
                w.Click += (se, ev) => MessageBox.Show("Clicked!");
            };

Я не смог удержаться и от проверки на "null" в стиле "возможно, монады", поэтому, если у вас есть что-то, что может вернуть null, вы все равно можете применить к нему With и затем проверить его на null -ness.

3 голосов
/ 27 марта 2009

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

// Expect mock.FooBar method to be called with any paramter and have it invoke some method
Expect.Call(() => mock.FooBar(null))
    .IgnoreArguments()
    .WhenCalled(someCallbackHere);

// Tell mock.Baz property to return 5:
SetupResult.For(mock.Baz).Return(5);
2 голосов
/ 27 марта 2009

SubSonic 2.1 имеет достойный для API запросов:

DB.Select()
  .From<User>()
  .Where(User.UserIdColumn).IsEqualTo(1)
  .ExecuteSingle<User>();

tweetsharp также широко использует свободно API:

var twitter = FluentTwitter.CreateRequest()
              .Configuration.CacheUntil(2.Minutes().FromNow())
              .Statuses().OnPublicTimeline().AsJson();

И Свободный NHibernate в последнее время в моде:

public class CatMap : ClassMap<Cat>  
{  
  public CatMap()  
  {  
    Id(x => x.Id);  
    Map(x => x.Name)  
      .WithLengthOf(16)  
      .Not.Nullable();  
    Map(x => x.Sex);  
    References(x => x.Mate);  
    HasMany(x => x.Kittens);  
  }  
}  

Ninject тоже использует их, но я не смог быстро найти пример.

1 голос
/ 01 августа 2012

Я написал небольшую свободную оболочку для System.Net.Mail, которая, по моему мнению, делает код электронной почты более читабельным (и легче запоминает синтаксис).

var email = Email
            .From("john@email.com")
            .To("bob@email.com", "bob")
            .Subject("hows it going bob")
            .Body("yo dawg, sup?");

//send normally
email.Send();

//send asynchronously
email.SendAsync(MailDeliveredCallback);

http://lukencode.com/2010/04/11/fluent-email-in-net/

1 голос
/ 27 марта 2009

Criteria API в NHibernate имеет удобный интерфейс, который позволяет вам делать такие крутые вещи, как это

Session.CreateCriteria(typeof(Entity))
    .Add(Restrictions.Eq("EntityId", entityId))
    .CreateAlias("Address", "Address")
    .Add(Restrictions.Le("Address.StartDate", effectiveDate))
    .Add(Restrictions.Disjunction()
        .Add(Restrictions.IsNull("Address.EndDate"))
        .Add(Restrictions.Ge("Address.EndDate", effectiveDate)))
    .UniqueResult<Entity>();
1 голос
/ 27 марта 2009

Новый HttpClient WCF REST Starter Kit Preview 2 - это отличный API. см. мой блог для образца http://bendewey.wordpress.com/2009/03/14/connecting-to-live-search-using-the-httpclient/

...