Zasada najmniejszego zdziwienia (POLA) i interfejsy

17

Dobra ćwierć wieku temu, kiedy uczyłem się C ++, nauczono mnie, że interfejsy powinny wybaczać i, w miarę możliwości, nie przejmować się kolejnością wywoływania metod, ponieważ konsument może nie mieć dostępu do źródła lub dokumentacji zamiast to.

Jednak za każdym razem, gdy mentorowałem młodszych programistów i starszych deweloperów, słyszeli mnie, reagowali ze zdziwieniem, co spowodowało, że zastanawiałem się, czy to naprawdę było coś, czy też właśnie wyszło z mody.

Czysty jak błoto?

Rozważ interfejs z tymi metodami (do tworzenia plików danych):

OpenFile
SetHeaderString
WriteDataLine
SetTrailerString
CloseFile

Teraz możesz oczywiście po prostu przejrzeć je w kolejności, ale powiedz, że nie obchodzi Cię nazwa pliku (pomyśl a.out) lub jaki nagłówek i ciąg przyczepy zostały uwzględnione, możesz po prostu zadzwonić AddDataLine.

Mniej ekstremalnym przykładem może być pominięcie nagłówków i przyczep.

Jeszcze innym może być ustawianie ciągów nagłówka i przyczepy przed otwarciem pliku.

Czy jest to zasada rozpoznawania interfejsu, czy tylko POLA, zanim nadano mu nazwę?

Uwaga: nie daj się wciągnąć w szczegóły tego interfejsu, jest to tylko przykład dla tego pytania.

Robbie Dee
źródło
10
Zasada „najmniejszego zdziwienia” jest znacznie bardziej rozpowszechniona w projektowaniu interfejsu użytkownika niż w projekcie „interfejsu programisty aplikacji”. Powodem jest to, że od użytkownika witryny internetowej lub programu nie można w ogóle przeczytać żadnych instrukcji przed ich użyciem, podczas gdy programista powinien, przynajmniej w zasadzie, przeczytać dokumentację API przed programowaniem z nimi.
Kilian Foth
7
@KilianFoth: Jestem prawie pewien, że Wikipedia nie ma racji w tej kwestii - POLA nie dotyczy wyłącznie projektowania interfejsu użytkownika, termin „zasada najmniejszego zaskoczenia” (który jest taki sam) jest również używany przez Boba Martina do projektowania funkcji i klas w jego Książka „Czysty kod”.
Doc Brown
2
Często niezmienny interfejs i tak jest lepszy. Możesz określić wszystkie dane, które chcesz ustawić w czasie budowy. Nie ma dwuznaczności, a klasa staje się prostsza do napisania. (Czasami ten schemat nie jest oczywiście możliwy).
usr
4
Całkowicie się nie zgadzaj, że POLA nie stosuje się do interfejsów API. Odnosi się do wszystkiego, co człowiek tworzy dla innych ludzi. Kiedy rzeczy działają zgodnie z oczekiwaniami, łatwiej jest je konceptualizować, a tym samym generują mniejsze obciążenie poznawcze, pozwalając ludziom robić więcej rzeczy przy mniejszym wysiłku.
Gort the Robot

Odpowiedzi:

25

Jednym ze sposobów, w jaki możesz trzymać się zasady najmniejszego zdziwienia, jest rozważenie innych zasad, takich jak ISP i SRP , a nawet DRY .

W podanym konkretnym przykładzie sugeruje się, że istnieje pewna zależność od kolejności manipulowania plikiem; ale interfejs API kontroluje zarówno dostęp do pliku, jak i format danych, który pachnie trochę jak naruszenie SRP.

Edycja / aktualizacja: sugeruje również, że sam interfejs API prosi użytkownika o naruszenie DRY, ponieważ będzie musiał powtarzać te same kroki za każdym razem, gdy użyje interfejsu API .

Rozważ alternatywny interfejs API, w którym operacje IO są niezależne od operacji na danych. a gdzie sam API „jest właścicielem” zamówienia:

ContentBuilder

SetHeader( ... )
AddLine( ... )
SetTrailer ( ... )

FileWriter

Open(filename) 
Write(content) throws InvalidContentException
Close()

Przy powyższej separacji ContentBuildernie trzeba „robić” niczego poza przechowywaniem wierszy / nagłówków / zwiastunów (może także ContentBuilder.Serialize()metodą znającą kolejność). Przestrzeganie innych zasad SOLID nie ma już znaczenia, czy ustawisz nagłówek czy zwiastun przed czy po dodaniu wierszy, ponieważ nic w ContentBuilderpliku nie jest zapisywane do pliku, dopóki nie zostanie przekazane FileWriter.Write.

Ma również tę dodatkową zaletę, że jest nieco bardziej elastyczny; na przykład przydatne może być zapisanie zawartości do rejestratora diagnostycznego lub przekazanie jej przez sieć zamiast zapisywania bezpośrednio w pliku.

Projektując interfejs API, należy również wziąć pod uwagę raportowanie błędów, niezależnie od tego, czy jest to stan, wartość zwracana, wyjątek, wywołanie zwrotne lub coś innego. Użytkownik interfejsu API prawdopodobnie spodziewa się, że będzie w stanie programowo wykryć wszelkie naruszenia jego umów lub nawet inne błędy, których nie może kontrolować, takie jak błędy we / wy pliku.

Ben Cottrell
źródło
Właśnie tego szukałem - dziękuję! Z artykułu ISP: „(ISP) stwierdza, że ​​żaden klient nie powinien być zmuszany do polegania na metodach, których nie używa”
Robbie Dee,
5
To nie jest zła odpowiedź, ale mimo to program do tworzenia treści może zostać zaimplementowany w sposób, w którym kolejność wywołań SetHeaderlub ma AddLineznaczenie. Aby wyeliminować tę zależność od zamówienia, nie jest to ani ISP, ani SRP, to po prostu POLA.
Doc Brown
Gdy zamówienie ma znaczenie, nadal można spełnić wymagania POLA, definiując operacje w taki sposób, że wykonywanie późniejszych kroków wymaga wartości zwracanej z poprzednich kroków, a tym samym egzekwowania kolejności w systemie typów. FileWritermoże następnie wymagać wartości z ostatniego ContentBuilderkroku Writemetody, aby upewnić się, że cała zawartość wejściowa jest kompletna, co czyni ją InvalidContentExceptionniepotrzebną.
Dan Lyons,
@ DanLyons Wydaje mi się, że jest to dość zbliżone do sytuacji, której pytający próbuje uniknąć; gdzie użytkownik interfejsu API musi wiedzieć lub dbać o zamówienie. Idealnie, sam interfejs API powinien egzekwować zamówienie, w przeciwnym razie potencjalnie prosi użytkownika o naruszenie DRY. To jest powód podziału ContentBuilderi umożliwienia FileWriter.Writekapsułkowania tej odrobiny wiedzy. Wyjątek byłby konieczny na wypadek, gdyby coś było nie tak z treścią (np. Brakujący nagłówek). Zwrot może również działać, ale nie jestem fanem przekształcania wyjątków w kody powrotne.
Ben Cottrell,
Ale zdecydowanie warto dodać więcej odpowiedzi na temat OSUSZANIA i zamawiania do odpowiedzi.
Ben Cottrell,
12

Nie chodzi tylko o POLA, ale także o zapobieganie nieprawidłowemu stanowi jako możliwemu źródłu błędów.

Zobaczmy, jak możemy wprowadzić pewne ograniczenia do Twojego przykładu bez konkretnej implementacji:

Pierwszy krok: nie zezwalaj na wywołanie czegokolwiek przed otwarciem pliku.

CreateDataFileInterface
  + OpenFile(filename : string) : DataFileInterface

DataFileInterface
  + SetHeaderString(header : string) : void
  + WriteDataLine(data : string) : void
  + SetTrailerString(trailer : string) : void
  + Close() : void

Teraz powinno być oczywiste, że CreateDataFileInterface.OpenFilenależy wywołać, aby pobrać DataFileInterfaceinstancję, w której można zapisać rzeczywiste dane.

Drugi krok: upewnij się, że hedery i przyczepy są zawsze ustawione.

CreateDataFileInterface
  + OpenFile(filename : string, header: string, trailer : string) : DataFileInterface

DataFileInterface
  + WriteDataLine(data : string) : void
  + Close() : void

Teraz musisz podać wszystkie wymagane parametry z góry, aby uzyskać DataFileInterface: nazwę pliku, nagłówek i zwiastun. Jeśli ciąg zwiastuna nie jest dostępny, dopóki nie zostaną zapisane wszystkie wiersze, możesz również przenieść ten parametr do Close()(ewentualnie zmienić nazwę metody WriteTrailerAndClose()), aby przynajmniej pliku nie można było zakończyć bez ciągu zwiastuna.


Aby odpowiedzieć na komentarz:

Lubię separację interfejsu. Ale jestem skłonny myśleć, że twoja sugestia na temat egzekwowania (np. WriteTrailerAndClose ()) ogranicza się do naruszenia SRP. (Jest to coś, z czym zmagałem się wiele razy, ale twoja sugestia wydaje się być możliwym przykładem.) Jak byś zareagował?

Prawdziwe. Nie chciałem koncentrować się bardziej na przykładzie, niż jest to konieczne, aby wyrazić swoje zdanie, ale to dobre pytanie. W tym przypadku myślę, że nazwałbym to Finalize(trailer)i twierdzę, że nie robi to zbyt wiele. Napisanie zwiastuna i zamknięcie są jedynie szczegółami implementacyjnymi. Ale jeśli się nie zgadzasz lub masz podobną sytuację, w której jest inaczej, oto możliwe rozwiązanie:

CreateDataFileInterface
  + OpenFile(filename : string, header : string) : IncompleteDataFileInterface

IncompleteDataFileInterface
  + WriteDataLine(data : string) : void
  + FinalizeWithTrailer(trailer : string) : CompleteDataFileInterface

CompleteDataFileInterface
  + Close()

Tak naprawdę nie zrobiłbym tego na tym przykładzie, ale pokazuje, w jaki sposób przeprowadzić technikę.

Nawiasem mówiąc, założyłem, że metody muszą być wywoływane w tej kolejności, na przykład, aby sekwencyjnie zapisywać wiele wierszy. Jeśli nie jest to wymagane, zawsze wolałbym budowniczego, jak zasugerował Ben Cottrel .

Fabian Schmengler
źródło
1
Niestety wpadłeś w pułapkę, której wyraźnie ostrzegałem od samego początku. Nazwa pliku nie jest wymagana - ani nagłówek, ani zwiastun. Ale ogólny temat podziału interfejsu jest dobry, więc +1 :-)
Robbie Dee
Och, wtedy źle cię zrozumiałem, myślałem, że to opisuje intencję użytkownika, a nie implementację.
Fabian Schmengler
Lubię separację interfejsu. Ale jestem skłonny myśleć, że twoja sugestia dotycząca egzekwowania prawa (np. WriteTrailerAndClose()) Zbliża się do naruszenia SRP. (Jest to coś, z czym zmagałem się wiele razy, ale twoja sugestia wydaje się być możliwym przykładem.) Jak byś zareagował?
kmote 14.04.16
1
@kmote odpowiedź była zbyt długa, aby skomentować, zobacz moją aktualizację
Fabian Schmengler
1
Jeśli nazwa pliku jest opcjonalna, możesz podać OpenFileprzeciążenie, które jej nie wymaga.
5gon12eder