Jakie interfejsy wykonałeś lub widziałeś w C#, które były bardzo cenne? Co było w nich takiego wspaniałego?

"Fluent interfaces" to w dzisiejszych czasach dość gorący temat. C# 3.0 ma kilka fajnych funkcji (szczególnie metody rozszerzeń), które pomagają je tworzyć.

Dla twojej wiadomości, płynne API oznacza, że każde wywołanie metody zwraca coś użytecznego, często ten sam obiekt, na którym wywołałeś metodę, więc możesz nadal łączyć rzeczy. Martin Fowler omawia to na przykładzie Javy tutaj . Koncepcja ma coś takiego:

var myListOfPeople = new List<Person>();

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

Widziałem kilka niesamowicie przydatnych interfejsów w C# (jednym z przykładów jest płynne podejście do walidacji parametrów znalezionych w wcześniejszym pytaniu Stoskoverflow, które zadałem . To mnie rozwaliło. Był w stanie podać bardzo czytelną składnię do wyrażania reguł walidacji parametrów, a także, jeśli nie było WYJĄTKÓW, był w stanie uniknąć tworzenia instancji żadnych obiektów! Więc dla "normalnego przypadku" było bardzo mało kosztów. Ta jedna ciekawostka nauczyła mnie ogromnej kwoty w krótkim czasie. Chcę znaleźć więcej takich rzeczy).

Więc, Chciałbym dowiedzieć się więcej, patrząc na kilka doskonałych przykładów i omawiając je. Więc, jakie są doskonałe, płynne interfejsy, które stworzyłeś lub widziałeś w C# i co uczyniło je tak cennymi?

Dzięki.
Author: Community, 2009-03-27

11 answers

Wyrazy uznania za walidację parametrów metody, dałeś mi nowy pomysł na nasze płynne API. I tak nienawidziłem naszych czeków wstępnych...

Zbudowałem system rozszerzalności dla nowego produktu w fazie rozwoju, w którym można płynnie opisać dostępne polecenia, elementy interfejsu użytkownika i wiele innych. Działa to na bazie StructureMap i FluentNHibernate, które są również ładnymi 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();
     // ...
});

I możesz skonfigurować wszystkie dostępne polecenia tak:

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

Nasz pogląd konfiguracja kontrolek dla standardowych poleceń menu edycji za pomocą usługi przekazanej im przez obszar roboczy, gdzie po prostu każą mu je obserwować:

Workspace
  .Observe(control1)
  .Observe(control2)

Jeśli użytkownik przechodzi do kontrolek, obszar roboczy automatycznie otrzymuje odpowiedni adapter do kontrolki i wykonuje operacje cofania/ponawiania i schowka.

To pomogło nam znacznie zredukować kod konfiguracji i uczynić go jeszcze bardziej czytelnym.


Zapomniałem powiedzieć o bibliotece, której używamy w naszych WinForms Prezenterzy modelu MVP do walidacji widoków: FluentValidation. Naprawdę łatwe, naprawdę testowalne, naprawdę ładne!

 8
Author: grover,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-03-27 22:11:35

Po raz pierwszy słyszę określenie " płynny interfejs."Ale dwa przykłady, które przychodzą na myśl to LINQ i immutable collections.

Pod okładkami LINQ znajduje się szereg metod, z których większość to metody rozszerzenia, które pobierają co najmniej jedną liczbę IEnumerable i zwracają inną liczbę IEnumerable. Pozwala to na bardzo wydajne łączenie metod

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

Typy niezmienne, a dokładniej zbiory mają bardzo podobny wzór. Niezmienne zbiory zwracają nową zbiór dla tego, co normalnie byłoby operacją mutującą. Tak więc budowanie kolekcji często zamienia się w serię łańcuchowych wywołań metod.

var array = ImmutableCollection<int>.Empty.Add(42).Add(13).Add(12);
 9
Author: JaredPar,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-03-27 04:40:03

Uwielbiam płynny interfejs w CuttingEdge.Warunki .

Z ich próbki:

{[0]}

Odkryłem, że jest o wiele łatwiejszy do odczytania i sprawia, że znacznie skuteczniej sprawdzam moje warunki wstępne (i warunki postu) w metodach niż gdy mam 50 instrukcji if do obsługi tych samych kontroli.

 7
Author: Reed Copsey,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-06-21 15:51:25

Oto jeden, który zrobiłem wczoraj. Dalsza myśl może doprowadzić mnie do zmiany podejścia, ale nawet jeśli tak," płynne " podejście pozwoliło mi osiągnąć coś, czego inaczej nie mógłbym mieć.

Najpierw trochę historii. ostatnio nauczyłem się (tutaj na StackOverflow) sposobu przekazywania wartości do metody tak, że metoda będzie w stanie określić zarówno nazwę , jak i wartość . Na przykład jednym z powszechnych zastosowań jest Walidacja parametrów. Na przykład:

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

Uwaga nie ma łańcucha zawierającego "lastMonthsInvoice" , co jest dobre, ponieważ ciągi są do bani dla refaktoryzacji. Jednak komunikat o błędzie Może powiedzieć coś w stylu "parametr' lastMonthsInvoice ' nie może być null."oto post który wyjaśnia, dlaczego to działa i wskazuje na post na blogu faceta.

Ale to tylko tło. używam tego samego pojęcia, ale w inny sposób. Piszę testy jednostkowe i chcę zrzucić niektóre wartości właściwości są wysyłane do konsoli, więc pojawiają się na wyjściu testu jednostkowego. Znudziło mi się pisanie tego:

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

... ponieważ muszę nazwać właściwość jako ciąg znaków, a następnie odnieść się do niej. Więc zrobiłem to gdzie mogłem to wpisać:

ConsoleHelper.WriteProperty( ()=> lastMonthsInvoice );

I uzyskaj to wyjście:

Property [lastMonthsInvoice] is: <whatever ToString from Invoice

Produkuje>

Tutaj płynne podejście pozwoliło mi zrobić coś, czego inaczej nie mógłbym zrobić.

Chciałem zrobić ConsoleHelper.WriteProperty weź tablicę params, aby mogła ona zrzucić wiele takich wartości właściwości do konsoli. Aby to zrobić, jego podpis wyglądałby tak:

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

Więc mogę to zrobić:

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

Jednak to nie działa z powodu wnioskowania typu. innymi słowy, wszystkie te wyrażenia nie zwracają tego samego typu. lastMonthsInvoice jest fakturą. firstName i lastName są ciągami znaków. Nie mogą być używane w tym samym wywołaniu do WriteProperty, ponieważ T nie jest taki sam we wszystkich oni.

Tutaj płynne podejście przyszło na ratunek. Sprawiłem, że writeproperty() zwróciło coś. Typ, który zwrócił jest czymś, co mogę wywołać i() na. To daje mi taką składnię:

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

Jest to przypadek, w którym płynne podejście pozwalało na coś, co w przeciwnym razie nie byłoby możliwe (a przynajmniej nie było wygodne).

Oto pełna implementacja. Jak już mówiłem, napisałem to wczoraj. Prawdopodobnie zobaczysz miejsce na poprawę, a może nawet lepiej zbliża się. Cieszę się z tego.

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() { }
}
 4
Author: Charlie Flowers,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 10:29:36

Oprócz tych podanych tutaj, popuplar RhinoMocks Unit test mock Framework używa płynnej składni, aby określić oczekiwania na obiektach mock:

// 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);
 3
Author: Judah Himango,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-03-27 17:36:31

SubSonic 2.1 ma przyzwoity dla query API:

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

Tweetsharp używa również płynnego API:

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

I Fluent NHibernate jest ostatnimi czasy wściekłością:

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 też ich używa, ale nie mogłem szybko znaleźć przykładu.

 2
Author: John Sheehan,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-03-27 16:43:29

Nazwa Metody

Interfejsy płynne nadają się do czytelności, o ile nazwy metod są rozsądnie wybierane.

W związku z tym chciałbym nominować ten konkretny API jako "anty-fluent":]}

System.Typ.IsInstanceOfType

Jest członkiem System.Type i pobiera obiekt i zwraca true, jeśli obiekt jest instancją tego typu. Niestety, naturalnie czytasz to od lewej do prawej jak to:

o.IsInstanceOfType(t);  // wrong

Kiedy faktycznie jest odwrotnie:

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

Ale nie wszystkie metody mogłyby być nazwane (lub umieszczone w BCL), aby przewidzieć, jak mogą pojawić się w" pseudo-angielskim " kodzie, więc nie jest to tak naprawdę krytyka. Zwracam tylko uwagę na inny aspekt płynnych interfejsów - wybór nazw metod w celu wywołania jak najmniejszego zaskoczenia.

Inicjalizatory Obiektów

Z wielu przykładów podanych tutaj, jedynym powodem, dla którego biegły interfejs jest używany tak, że kilka właściwości nowo przydzielonego obiektu może być zainicjalizowanych w ramach jednego wyrażenia.

W języku C# istnieje wiele funkcji, które często sprawiają, że składnia inicjalizatora niepotrzebnego obiektu:]}
var myObj = new MyClass
            {
                SomeProperty = 5,
                Another = true,
                Complain = str => MessageBox.Show(str),
            };

To może wyjaśniać, dlaczego użytkownicy C# są mniej zaznajomieni z terminem "fluent interface" do łączenia łańcuchów wywołań tego samego obiektu - nie jest to tak często potrzebne w C#.

Ponieważ właściwości mogą mieć ustawiacze kodowane ręcznie, To jest możliwością wywołania kilku metod na nowo zbudowanym obiekcie, bez konieczności zwracania każdej metody tego samego obiektu.

Ograniczenia to:

  • Ustawiacz właściwości może przyjąć tylko jeden argument
  • Ustawiacz właściwości nie może być ogólny

Chciałbym, abyśmy mogli wywoływać metody i włączać się w zdarzenia, a także przypisywać do właściwości wewnątrz bloku inicjalizacji obiektu.

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

I dlaczego taki blok modyfikacji mają zastosowanie tylko natychmiast po budowie? Możemy mieć:

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

with będzie nowym słowem kluczowym, które działa na obiekcie jakiegoś typu i tworzy ten sam obiekt i typ-zauważ, że będzie to wyrażenie , a nie instrukcja . Tak więc dokładnie uchwyciłby ideę łączenia w "płynnym interfejsie".

Więc możesz użyć składni w stylu inicjalizatora niezależnie od tego, czy otrzymałeś obiekt z new wyrażenia, czy z IOC lub metoda fabryczna itp.

W rzeczywistości można użyć with po całkowitym new i byłoby to równoważne aktualnemu stylowi inicjalizacji obiektu:

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

I jak wskazuje Charlie w komentarzach:

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

Powyższy wrapper po prostu wymusza nieodwracalne działanie, aby coś zwrócić, a hej presto-wszystko może być "płynne" w tym sensie.

Odpowiednik inicjalizatora, ale z włączeniem zdarzenia:

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

I na metodzie fabrycznej zamiast new:

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

Nie mogłem się oprzeć, aby sprawdzić w stylu"maybe monad"dla null, więc jeśli masz coś, co może zwrócić null, nadal możesz zastosować With do niego, a następnie sprawdzić go pod kątem null-ness.

 2
Author: Daniel Earwicker,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-03-27 23:30:22

Criteria API w NHibernate ma ładny płynny interfejs, który pozwala robić fajne rzeczy takie jak:

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
Author: lomaxx,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-03-27 05:18:18

Nowy HttpClient WCF Rest Starter Kit Preview 2 jest świetnym płynnym API. zobacz mój wpis na blogu, aby uzyskać próbkę http://bendewey.wordpress.com/2009/03/14/connecting-to-live-search-using-the-httpclient/

 1
Author: bendewey,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-03-27 16:42:27

Napisałem trochę płynnego wrappera do System. Net. Mail, który moim zdaniem sprawia, że kod pocztowy jest dużo bardziej czytelny(i łatwiej zapamiętać składnię).

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/

 1
Author: Luke Lowrey,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-08-01 07:49:54

Jak wspomniał @ John Sheehan , Ninject używa tego typu API do określania wiązań. Oto przykładowy kod z ich user guide :

Bind<IWeapon>().To<Sword>();
Bind<Samurai>().ToSelf();
Bind<Shogun>().ToSelf().Using<SingletonBehavior>();
 0
Author: Tormod Fjeldskår,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:26:29