Какие четкие интерфейсы вы сделали или видели на С#, которые были очень ценными? Что в них было так здорово?
"Свободные интерфейсы" в наши дни являются довольно горячей темой. В С# 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 научил меня огромная сумма за короткое время. Я хочу найти больше таких вещей).
Итак, я хотел бы узнать больше, посмотрев и обсудив некоторые отличные примеры. Итак, какие превосходные интерфейсы, которые вы сделали или видели на С#, и что сделало их такими ценными?
Спасибо.
Ответы
Ответ 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.
Ответ 8
Новый HttpClient Предварительный просмотр стартового набора WCF REST 2 - отличный API. см. мой пост в блоге для образца http://bendewey.wordpress.com/2009/03/14/connecting-to-live-search-using-the-httpclient/
Ответ 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>();