Подтвердить что ты не робот

Какие четкие интерфейсы вы сделали или видели на С#, которые были очень ценными? Что в них было так здорово?

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

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

var myListOfPeople = new List<Person>();

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

Я видел некоторые невероятно полезные интерфейсы в С# (один из примеров - свободный подход для проверки параметров, найденных в qaru.site/info/177624/.... он смог дать очень читаемый синтаксис для выражения правил проверки параметров, а также, если не было исключений, он смог избежать создания экземпляров любых объектов! Поэтому для "нормального случая" было очень мало накладных расходов. tidbit научил меня огромная сумма за короткое время. Я хочу найти больше таких вещей).

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

Спасибо.

4b9b3361

Ответ 1

Претензии для проверки параметров метода, вы дали мне новую идею для наших беглых 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. Действительно легкий, действительно проверяемый, очень хороший!

Ответ 2

Это первый раз, когда я услышал термин "свободный интерфейс". Но два примера, которые приходят на ум, - это 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);

Ответ 3

Мне нравится свободный интерфейс в 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

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

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

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

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

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

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

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

ConsoleHelper.WriteProperty( ()=> lastMonthsInvoice );

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

Property [lastMonthsInvoice] is: <whatever ToString from Invoice

производит >

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

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

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() { }
}

Ответ 5

В дополнение к тем, которые указаны здесь, popuplar RhinoMocks unit test mock framework использует свободный синтаксис для определения ожиданий на макетных объектах:

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

Ответ 6

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

И Fluent 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 также использует их, но я не мог быстро найти пример.

Ответ 7

Именование методов

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

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

System.Type.IsInstanceOfType

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

o.IsInstanceOfType(t);  // wrong

Если это действительно так:

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

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

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

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

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

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

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

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

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

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

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

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 будет новым ключевым словом, которое работает с объектом какого-либо типа и выдает тот же объект и тип - обратите внимание, что это будет выражение, а не утверждение. Таким образом, это точно отражает идею цепочки в "свободном интерфейсе".

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

Фактически вы могли бы использовать 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;
}

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

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

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

И в factory вместо new:

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

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

Ответ 9

API-интерфейс Criteria в 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>();

Ответ 10

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

var email = Email
            .From("[email protected]")
            .To("[email protected]", "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/

Ответ 11

Как упоминалось @John Sheehan, Ninject использует этот тип API для указания привязок. Вот пример кода из руководство пользователя:

Bind<IWeapon>().To<Sword>();
Bind<Samurai>().ToSelf();
Bind<Shogun>().ToSelf().Using<SingletonBehavior>();