Kiedy jechać płynnie w C #?

78

Pod wieloma względami bardzo podoba mi się pomysł płynnych interfejsów, ale ze wszystkimi nowoczesnymi funkcjami C # (inicjalizatory, lambdy, nazwane parametry) zastanawiam się, czy warto? posługiwać się?". Czy ktoś mógłby dać mi, jeśli nie zaakceptowaną praktykę, przynajmniej własne doświadczenie lub matrycę decyzyjną na temat tego, kiedy zastosować wzór Płynny?

Wniosek:

Kilka dobrych zasad z dotychczasowych odpowiedzi:

  • Płynne interfejsy znacznie pomagają, gdy masz więcej akcji niż seterów, ponieważ połączenia korzystają bardziej z przekazywania kontekstu.
  • Płynne interfejsy należy uważać za warstwę nad interfejsem API, a nie za jedyny sposób użycia.
  • Nowoczesne funkcje, takie jak lambda, inicjalizatory i nazwane parametry, mogą współpracować ze sobą, dzięki czemu płynny interfejs jest jeszcze bardziej przyjazny.

Oto przykład tego, co rozumiem przez nowoczesne funkcje, które sprawiają, że wydaje się mniej potrzebny. Weźmy na przykład (być może kiepski przykład) Płynny interfejs, który pozwala mi stworzyć pracownika, takiego jak:

Employees.CreateNew().WithFirstName("Peter")
                     .WithLastName("Gibbons")
                     .WithManager()
                          .WithFirstName("Bill")
                          .WithLastName("Lumbergh")
                          .WithTitle("Manager")
                          .WithDepartment("Y2K");

Można go łatwo napisać za pomocą inicjatorów, takich jak:

Employees.Add(new Employee()
              {
                  FirstName = "Peter",
                  LastName = "Gibbons",
                  Manager = new Employee()
                            {
                                 FirstName = "Bill",
                                 LastName = "Lumbergh",
                                 Title = "Manager",
                                 Department = "Y2K"
                            }
              });

W tym przykładzie mogłem również użyć nazwanych parametrów w konstruktorach.

Andrew Hanlon
źródło
1
Dobre pytanie, ale myślę, że to bardziej pytanie typu wiki
Ivo
Twoje pytanie jest oznaczone jako „biegły-nhibernate”. Więc próbujesz zdecydować, czy stworzyć płynny interfejs, czy też użyć płynnej konfiguracji nhibernate vs. XML?
Ilya Kogan,
1
Głosowano na migrację do Programmers.SE
Matt Ellen,
@Ilya Kogan, myślę, że tak naprawdę jest on oznaczony jako „płynny interfejs”, który jest ogólnym znacznikiem dla płynnego wzoru interfejsu. To pytanie nie dotyczy nhibernacji, ale jak powiedziałeś tylko, czy stworzyć płynny interfejs. Dzięki.
1
Ten post zainspirował mnie do wymyślenia sposobu użycia tego wzorca w C. Moja próba znajduje się na siostrzanej stronie Code Review .
otto

Odpowiedzi:

28

Napisanie płynnego interfejsu (którym się nim zajmowałem ) wymaga więcej wysiłku, ale przynosi spłatę, ponieważ jeśli zrobisz to dobrze, intencja wynikowego kodu użytkownika jest bardziej oczywista. Zasadniczo jest to forma języka specyficznego dla domeny.

Innymi słowy, jeśli Twój kod jest odczytywany znacznie więcej niż jest napisany (a jakiego kodu nie ma?), Powinieneś rozważyć stworzenie płynnego interfejsu.

Płynne interfejsy dotyczą bardziej kontekstu i są czymś więcej niż tylko sposobem konfigurowania obiektów. Jak widać w powyższym linku, użyłem płynnego interfejsu API, aby osiągnąć:

  1. Kontekst (więc gdy zwykle wykonujesz wiele akcji w sekwencji z tą samą rzeczą, możesz połączyć akcje bez konieczności deklarowania kontekstu w kółko).
  2. Wykrywalność (kiedy przejdziesz do objectA.Intellisense daje ci wiele wskazówek. W moim przypadku powyżej plm.Led.daje ci wszystkie opcje sterowania wbudowaną diodą LED i plm.Network.daje ci to, co możesz zrobić z interfejsem sieci. plm.Network.X10.Daje podzbiór akcje sieciowe dla urządzeń X10. Nie dostaniesz tego przy inicjalizatorach konstruktora (chyba że chcesz zbudować obiekt dla każdego rodzaju akcji, co nie jest idiomatyczne).
  3. Odbicie (nieużywane w powyższym przykładzie) - możliwość przechwycenia wyrażenia LINQ i manipulowania nim jest bardzo potężnym narzędziem, szczególnie w niektórych interfejsach API pomocnika, które zbudowałem do testów jednostkowych. Mogę przekazać wyrażenie gettera właściwości, zbudować całą masę przydatnych wyrażeń, skompilować i uruchomić je, a nawet użyć gettera właściwości do skonfigurowania mojego kontekstu.

Jedną rzeczą, którą zazwyczaj robię, jest:

test.Property(t => t.SomeProperty)
    .InitializedTo(string.Empty)
    .CantBeNull() // tries to set to null and Asserts ArgumentNullException
    .YaddaYadda();

Nie rozumiem, jak możesz zrobić coś takiego bez płynnego interfejsu.

Edycja 2 : Możesz także wprowadzić naprawdę interesujące ulepszenia czytelności, takie jak:

test.ListProperty(t => t.MyList)
    .ShouldHave(18).Items()
    .AndThenAfter(t => testAddingItemToList(t))
    .ShouldHave(19).Items();
Scott Whitlock
źródło
Dziękuję za odpowiedź, jednak jestem świadomy powodu, dla którego warto używać Fluent, ale szukam bardziej konkretnego powodu, aby użyć go zamiast czegoś takiego jak mój nowy przykład powyżej.
Andrew Hanlon,
Dziękuję za rozszerzoną odpowiedź. Myślę, że nakreśliłeś dwie dobre ogólne zasady: 1) Używaj płynnie, gdy masz wiele połączeń, które korzystają z „przejścia” kontekstu. 2) Pomyśl o płynności, gdy masz więcej połączeń niż seterów.
Andrew Hanlon,
2
@ach, w tej odpowiedzi nie widzę nic o „większej liczbie połączeń niż ustawiających”. Czy jesteś zdezorientowany jego stwierdzeniem o „kodzie [który] czyta się o wiele więcej niż napisano”? Że nie chodzi o pobierające / ustawiające własności, chodzi o ludzi czytających kod vs. ludzi piszących kod - kod o co łatwe dla ludzi do czytania, bo zazwyczaj o wiele częściej, niż nam modyfikować ją przeczytać daną linię kodu.
Joe White,
@Joe White, być może powinienem przeformułować termin „wezwanie” do „działania”. Ale pomysł wciąż istnieje. Dzięki.
Andrew Hanlon,
Refleksja do testowania jest zła!
Adronius
24

Scott Hanselman mówi o tym w odcinku 260 swojego podcastu Hanselminutes z Jonathanem Carterem. Wyjaśniają, że płynny interfejs bardziej przypomina interfejs użytkownika w interfejsie API. Nie powinieneś zapewniać płynnego interfejsu jako jedynego punktu dostępu, ale raczej dostarczać go jako swego rodzaju interfejsu użytkownika na górze „zwykłego interfejsu API”.

Jonathan Carter mówi także trochę o projektowaniu API na swoim blogu .

Kristof Claes
źródło
Bardzo dziękuję za linki informacyjne, a interfejs użytkownika na interfejsie API to dobry sposób na obejrzenie tego.
Andrew Hanlon,
14

Płynne interfejsy to bardzo potężne funkcje, które można udostępniać w kontekście kodu, gdy podano „właściwe” rozumowanie.

Jeśli Twoim celem jest po prostu utworzenie masywnych jednowierszowych łańcuchów kodu jako rodzaju pseudo-czarnej skrzynki, prawdopodobnie prawdopodobnie szczekasz na niewłaściwe drzewo. Z drugiej strony, jeśli używasz go do dodawania wartości do interfejsu API, zapewniając metodę łączenia wywołań metod i poprawiając czytelność kodu, to przy dobrym planowaniu i wysiłku uważam, że wysiłek jest tego wart.

Unikałbym podążania za czymś, co wydaje się być powszechnym „wzorcem” podczas tworzenia płynnych interfejsów, w których wszystkie swoje płynne metody nazywamy „za pomocą” -sośc, ponieważ okrada to potencjalnie dobry interfejs API jego kontekstu, a zatem i jego wewnętrznej wartości .

Kluczem jest pomyślenie o płynnej składni jako specyficznej implementacji języka specyficznego dla Domeny. Jako naprawdę dobry przykład tego, o czym mówię, spójrz na StoryQ, który wykorzystuje płynność jako środek do wyrażania DSL w bardzo cenny i elastyczny sposób.

S.Robins
źródło
Dzięki za odpowiedź, nigdy nie jest za późno, aby udzielić przemyślanej odpowiedzi.
Andrew Hanlon,
Nie przeszkadza mi prefiks „with” dla metod. To odróżnia je od innych metod, które nie zwracają obiektu do łączenia. Np position.withX(5)versusposition.getDistanceToOrigin()
LegendLength
5

Uwaga wstępna: Kwestionuję jedno założenie w pytaniu i wyciągam z tego moje konkretne wnioski (na końcu tego postu). Ponieważ prawdopodobnie nie jest to pełna, obejmująca odpowiedź, zaznaczam to jako CW.

Employees.CreateNew().WithFirstName("Peter")…

Można go łatwo napisać za pomocą inicjatorów, takich jak:

Employees.Add(new Employee() { FirstName = "Peter",  });

Moim zdaniem te dwie wersje powinny oznaczać i robić różne rzeczy.

  • W przeciwieństwie do wersji niepłynnej, wersja płynna ukrywa fakt, że nowy Employeejest również Addedytowany w kolekcji Employees- sugeruje tylko, że nowym obiektem jest Created.

  • Znaczenie ….WithX(…)jest niejednoznaczne, szczególnie dla osób pochodzących z F #, które ma withsłowo kluczowe do wyrażeń obiektowych : mogą one interpretować obj.WithX(x)jako nowy obiekt pochodzący z objtego, który jest identyczny z objwyjątkiem jego Xwłaściwości, której wartość jest x. Z drugiej strony w drugiej wersji jest jasne, że nie są tworzone żadne obiekty pochodne i że wszystkie właściwości są określone dla obiektu oryginalnego.

….WithManager().With
  • Ma ….With…to jeszcze inne znaczenie: przełączenie „fokusa” inicjalizacji właściwości na inny obiekt. Fakt, że Twój płynny interfejs API ma dwa różne znaczenia With, utrudnia prawidłową interpretację tego, co się dzieje… dlatego być może użyłeś wcięcia w swoim przykładzie, aby zademonstrować zamierzone znaczenie tego kodu. Byłoby to tak wyraźniejsze:

    (employee).WithManager(Managers.CreateNew().WithFirstName("Bill").…)
    //                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    //                     value of the `Manager` property appears inside the parentheses,
    //                     like with `WithName`, where the `Name` is also inside the parentheses 

Wnioski: „Ukrywanie” funkcji języka prostego new T { X = x }z płynnym API ( Ts.CreateNew().WithX(x)) można oczywiście zrobić, ale:

  1. Należy uważać, aby czytelnicy wynikowego płynnego kodu nadal rozumieli, co dokładnie robi. Oznacza to, że płynny interfejs API powinien być przejrzysty i jednoznaczny. Zaprojektowanie takiego interfejsu API może wymagać więcej pracy niż się spodziewano (może wymagać przetestowania pod kątem łatwości użycia i akceptacji) i / lub…

  2. zaprojektowanie go może wymagać więcej pracy niż jest to konieczne: w tym przykładzie płynny interfejs API zapewnia bardzo niewielki „komfort użytkownika” w stosunku do bazowego interfejsu API (funkcja języka). Można powiedzieć, że płynny interfejs API powinien sprawić, że podstawowa funkcja interfejsu API / języka będzie „łatwiejsza w użyciu”; to znaczy, że programista powinien zaoszczędzić sporo wysiłku. Jeśli jest to po prostu inny sposób pisania tego samego, prawdopodobnie nie jest tego wart, ponieważ nie ułatwia życia programistom, a jedynie utrudnia pracę projektanta (patrz konkluzja nr 1 powyżej).

  3. Oba powyższe punkty cicho zakładają, że płynny interfejs API stanowi warstwę w stosunku do istniejącej funkcji interfejsu API lub języka. To założenie może być kolejną dobrą wytyczną: płynne API może być dodatkowym sposobem robienia czegoś, a nie jedynym. Oznacza to, że dobrym pomysłem może być płynne API jako opcja „opt-in”.

stakx
źródło
1
Dziękuję za poświęcenie czasu na uzupełnienie mojego pytania. Przyznam, że mój wybrany przykład został źle przemyślany. W tamtym czasie faktycznie chciałem użyć płynnego interfejsu do tworzenia zapytań API, które opracowywałem. Uproszczony. Dziękujemy za wskazanie wad i dobre wnioski.
Andrew Hanlon
2

Lubię płynny styl, bardzo wyraźnie wyraża intencję. W przykładzie inicjalizatora obiektów, którego używasz, musisz mieć ustawiające właściwości publiczne, aby używać tej składni, a nie płynny styl. Mówiąc to, na twoim przykładzie nie zyskujesz wiele na publicznych setterach, ponieważ prawie wybrałeś metodę / styl java-esque.

Co prowadzi mnie do drugiego punktu, nie jestem pewien, czy użyłbym płynnego stylu w taki sposób, w jaki masz, z wieloma ustawieniami właściwości, prawdopodobnie użyłbym do tego drugiej wersji, uważam, że lepiej, gdy mieć wiele czasowników do połączenia, lub przynajmniej dużo działań zamiast ustawień.

Ian
źródło
Dzięki za odpowiedź, myślę, że wyraziłeś dobrą zasadę: płynność jest lepsza przy wielu połączeniach przez wielu seterów.
Andrew Hanlon
1

Nie znałem terminu płynny interfejs , ale przypomina mi kilka interfejsów API, z których korzystałem, w tym LINQ .

Osobiście nie rozumiem, w jaki sposób nowoczesne funkcje C # uniemożliwiłyby użyteczność takiego podejścia. Wolałbym powiedzieć, że idą w parze. Np. Jeszcze łatwiej jest osiągnąć taki interfejs za pomocą metod rozszerzenia .

Być może wyjaśnij swoją odpowiedź konkretnym przykładem tego, jak płynny interfejs można zastąpić, używając jednej z wymienionych przez ciebie nowoczesnych funkcji.

Steven Jeuris
źródło
1
Dziękuję za odpowiedź - dodałem podstawowy przykład, który pomaga wyjaśnić moje pytanie.
Andrew Hanlon,