Programowanie do przyszłego wykorzystania interfejsów

42

Obok mnie siedzi kolega, który zaprojektował taki interfejs:

public interface IEventGetter {

    public List<FooType> getFooList(String fooName, Date start, Date end)
        throws Exception;
    ....

}

Problem polega na tym, że obecnie nie używamy tego parametru „end” nigdzie w naszym kodzie, po prostu istnieje, ponieważ w przyszłości będziemy musieli go użyć.

Próbujemy go przekonać, że wprowadzanie parametrów do interfejsów, które w tej chwili nie są przydatne, jest kiepskim pomysłem, ale wciąż nalega, aby dużo pracy trzeba było wykonać, jeśli za jakiś czas zastosujemy datę zakończenia. później i wtedy trzeba dostosować cały kod.

Moje pytanie brzmi: czy są jakieś źródła, które zajmują się takim tematem jak „szanowani” guru kodujący, z którymi możemy go połączyć?

sveri
źródło
29
„Prognozy są bardzo trudne, szczególnie w odniesieniu do przyszłości”.
Jörg W Mittag
10
Częścią problemu jest to, że nie jest to najlepszy interfejs na początek. Pobieranie i filtrowanie pianek to dwa osobne problemy. Ten interfejs zmusza do filtrowania listy w jeden szczególny sposób; znacznie bardziej ogólnym podejściem jest przekazanie funkcji, która decyduje, czy należy uwzględnić Foo. Jest to jednak eleganckie tylko wtedy, gdy masz dostęp do Java 8.
Doval
5
Przeciwdziałanie ma sens. Po prostu spraw, aby ta metoda akceptowała niestandardowy typ, który na razie zawiera tylko to, czego potrzebujesz, i może ostatecznie dodać endparametr do tego obiektu, a nawet domyślnie, aby nie łamać kodu
Rémi
3
W przypadku domyślnych metod Java 8 nie ma powodu, aby dodawać niepotrzebne argumenty. Metoda może być dodane później z realizacji domyślnej że proste połączenia zdefiniowanego zz powiedzmy null. Klasy implementujące mogą następnie zastąpić w razie potrzeby.
Boris the Spider
1
@Doval Nie wiem o Javie, ale w .NET czasami widzisz takie rzeczy, jak unikanie narażania się na dziwactwa IQueryable(można przyjmować tylko niektóre wyrażenia) na kod poza DAL
Ben Aaronson

Odpowiedzi:

62

Poproś go, aby dowiedział się o YAGNI . Uzasadnienie strony Wikipedii może być szczególnie interesujące tutaj:

Według tych, którzy opowiadają się za podejściem YAGNI, pokusa pisania kodu, który w tej chwili nie jest konieczny, ale może być w przyszłości, ma następujące wady:

  • Czas poświęcony jest na dodawanie, testowanie lub ulepszanie niezbędnej funkcjonalności.
  • Nowe funkcje muszą być debugowane, dokumentowane i obsługiwane.
  • Każda nowa funkcja nakłada ograniczenia na to, co można zrobić w przyszłości, więc niepotrzebna funkcja może uniemożliwić dodanie potrzebnych funkcji w przyszłości.
  • Dopóki ta funkcja nie jest faktycznie potrzebna, trudno jest w pełni zdefiniować, co powinna zrobić i przetestować. Jeśli nowa funkcja nie zostanie poprawnie zdefiniowana i przetestowana, może nie działać poprawnie, nawet jeśli w końcu będzie potrzebna.
  • Prowadzi to do wzdęcia kodu; oprogramowanie staje się większe i bardziej skomplikowane.
  • Jeśli nie ma specyfikacji i kontroli wersji, funkcja ta może nie być znana programistom, którzy mogliby z niej skorzystać.
  • Dodanie nowej funkcji może sugerować inne nowe funkcje. Jeśli te nowe funkcje zostaną również zaimplementowane, może to spowodować efekt kuli śnieżnej w kierunku pełzania funkcji.

Inne możliwe argumenty:

  • „80% kosztów użytkowania oprogramowania przypada na konserwację” . Napisanie kodu w samą porę obniża koszty utrzymania: trzeba zachować mniej kodu i można skupić się na rzeczywiście potrzebnym kodzie.

  • Kod źródłowy jest pisany raz, ale czytany dziesiątki razy. Dodatkowy argument, nigdzie nie używany, prowadziłby do zmarnowanego czasu na zrozumienie, dlaczego istnieje argument, który nie jest potrzebny. Biorąc pod uwagę, że jest to interfejs z kilkoma możliwymi implementacjami, wszystko staje się jeszcze trudniejsze.

  • Oczekuje się, że kod źródłowy sam się dokumentuje. Rzeczywisty podpis jest mylący, ponieważ czytelnik mógłby pomyśleć, że endma to wpływ na wynik lub wykonanie metody.

  • Osoby piszące konkretne implementacje tego interfejsu mogą nie rozumieć, że nie należy używać ostatniego argumentu, co prowadziłoby do różnych podejść:

    1. Nie potrzebuję end, więc po prostu zignoruję jego wartość,

    2. Nie potrzebuję end, więc rzucę wyjątek, jeśli nie jest null,

    3. Nie potrzebuję end, ale spróbuję jakoś z tego skorzystać,

    4. Napiszę dużo kodu, który może być później użyty, kiedy endbędzie potrzebny.

Ale pamiętaj, że twój kolega może mieć rację.

Wszystkie poprzednie punkty oparte są na tym, że refaktoryzacja jest łatwa, więc dodanie argumentu później nie będzie wymagało dużego wysiłku. Ale to jest interfejs i jako interfejs może być używany przez kilka zespołów przyczyniających się do innych części twojego produktu. Oznacza to, że zmiana interfejsu może być szczególnie bolesna, w takim przypadku YAGNI tak naprawdę nie ma tutaj zastosowania.

Odpowiedź hjk daje dobre rozwiązanie: dodanie metody do już używanego interfejsu nie jest szczególnie trudne, ale w niektórych przypadkach wiąże się również ze znacznymi kosztami:

  • Niektóre frameworki nie obsługują przeciążeń. Na przykład i jeśli dobrze pamiętam (popraw mnie, jeśli się mylę), WCF platformy .NET nie obsługuje przeciążeń.

  • Jeśli interfejs ma wiele konkretnych implementacji, dodanie metody do interfejsu wymagałoby przejrzenia wszystkich implementacji i dodania tam również metody.

Arseni Mourzenko
źródło
4
Myślę, że ważne jest również, aby rozważyć, jak prawdopodobne jest pojawienie się tej funkcji w przyszłości. Jeśli wiesz na pewno, zostanie on dodany, ale nie teraz, z jakiegokolwiek powodu, powiedziałbym, że to dobry argument, o którym warto pamiętać, ponieważ nie jest to tylko funkcja „na wszelki wypadek”.
Jeroen Vannevel
3
@JeroenVannevel: oczywiście, ale to bardzo spekulacyjne. Z mojego doświadczenia wynika, że ​​większość funkcji, które moim zdaniem zostaną wdrożone, zostały na zawsze anulowane lub opóźnione. Obejmuje to funkcje, które pierwotnie zostały podkreślone przez interesariuszy jako bardzo ważne.
Arseni Mourzenko
26

ale wciąż nalega, aby wiele pracy trzeba było wykonać, jeśli jakiś czas później zaimplementujemy użycie daty „zakończenia” i będziemy musieli dostosować cały kod.

(jakiś czas później)

public class EventGetter implements IEventGetter {

    private static final Date IMPLIED_END_DATE_ASOF_20140711 = new Date(Long.MAX_VALUE); // ???

    @Override
    public List<FooType> getFooList(String fooName, Date start) throws Exception {
        return getFooList(fooName, start, IMPLIED_END_DATE_ASOF_20140711);
    }

    @Override
    public List<FooType> getFooList(String fooName, Date start, Date end) throws Exception {
        // Final implementation goes here
    }
}

To wszystko, czego potrzebujesz, przeciążenie metody. Dodatkową metodę w przyszłości można wprowadzić w sposób przejrzysty, bez wpływu na wywołania istniejącej metody.

hjk
źródło
6
Tyle że twoja zmiana oznacza, że ​​nie jest to już interfejs i nie będzie można jej użyć w przypadkach, gdy obiekt pochodzi już z obiektu i implementuje interfejs. Jest to trywialny problem, jeśli wszystkie klasy implementujące interfejs znajdują się w tym samym projekcie (błędy kompilacji chase). Jeśli jest to biblioteka używana w wielu projektach, dodanie pola powoduje sporadyczne zamieszanie i pracę dla innych osób w ciągu następnych x miesięcy / lat.
Kieveli
1
Jeśli zespół OP (uratuj kolegę) naprawdę wierzy, że endparametr jest teraz niepotrzebny i nadal korzysta z endmetody „-less” w całym projekcie (projektach), to aktualizacja interfejsu jest najmniejszym zmartwieniem, ponieważ staje się koniecznością aktualizacji klasy implementacyjne. Moja odpowiedź dotyczy konkretnie części „dostosuj cały kod”, ponieważ wygląda na to, że kolega myślał o konieczności zaktualizowania wywołań oryginalnej metody w całym projekcie (ach) w celu wprowadzenia trzeciego parametru domyślnego / zastępczego .
hjk
18

[Czy są] „szanowani” guru kodujący, z którymi możemy go powiązać [aby go przekonać]?

Apele do władzy nie są szczególnie przekonujące; lepiej przedstawić argument, który jest ważny bez względu na to, kto to powiedział.

Oto reductio ad absurdum, które powinno przekonać lub wykazać, że twój współpracownik utknął na „racji” niezależnie od wrażliwości:


To, czego naprawdę potrzebujesz

getFooList(String fooName, Date start, Date end, Date middle, 
           Date one_third, JulianDate start, JulianDate end,
           KlingonDate start, KlingonDate end)

Nigdy nie wiadomo, kiedy trzeba będzie internacjonalizować Klingon, więc lepiej się tym teraz zająć, ponieważ będzie to wymagało dużo pracy, a Klingony nie są znane z ich cierpliwości.

msw
źródło
22
You never know when you'll have to internationalize for Klingon. Dlatego powinieneś po prostu przekazać Calendari pozwolić, aby kod klienta zdecydował, czy chce wysłać a GregorianCalendarlub KlingonCalendar(założę się, że niektórzy już to zrobili). Duh. :-p
SJuan76
3
Co z StarDate sdStarti StarDate sdEnd? W końcu nie możemy zapomnieć o Federacji ...
WernerCD
Wszystkie te są oczywiście albo podklasami, albo można je zamieniać Date!
Darkhogg,
Gdyby to było takie proste .
msw
@msw, nie jestem pewien, czy prosił o „odwołanie do władzy”, kiedy poprosił o linki do „szanowanych guru kodowania”. Jak mówisz, potrzebuje „argumentu, który jest ważny bez względu na to, kto to powiedział”. Ludzie typu „guru” znają wiele z nich. Też zapomniałeś HebrewDate starti HebrewDate end.
trysis
12

Z punktu widzenia inżynierii oprogramowania uważam, że właściwym rozwiązaniem tego rodzaju problemów jest wzorzec konstruktora. Jest to zdecydowanie link od autorów „guru” do twojego kolegi http://en.wikipedia.org/wiki/Builder_pattern .

We wzorcu konstruktora użytkownik tworzy obiekt zawierający parametry. Ten parametr kontener zostanie następnie przekazany do metody. Zapobiegnie to wszelkim rozszerzeniom i przeciążeniu parametrów, których kolega będzie potrzebował w przyszłości, a jednocześnie sprawi, że wszystko będzie bardzo stabilne, gdy konieczne będzie wprowadzenie zmian.

Twój przykład stanie się:

public interface IEventGetter {
    public List<FooType> getFooList(ISearchFooRequest req) {
        throws Exception;
    ....
    }
}

public interface ISearchFooRequest {
        public String getName();
        public Date getStart();
        public Date getEnd();
        public int getOffset();
        ...
    }
}

public class SearchFooRequest implements ISearchFooRequest {

    public static SearchFooRequest buildDefaultRequest(String name, Date start) {
        ...
    }

    public String getName() {...}
    public Date getStart() {...}
    ...
    public void setEnd(Date end) {...}
    public void setOffset(int offset) {...}
    ...
}
Poinformowano
źródło
3
To dobre ogólne podejście, ale w tym przypadku wydaje się, że przesuwa problem od IEventGetterdo ISearchFootRequest. Zamiast tego zasugerowałbym coś takiego public IFooFilterz członkiem, public bool include(FooType item)który zwraca, czy dołączyć element. Następnie poszczególne implementacje interfejsu mogą zdecydować, w jaki sposób przeprowadzą to filtrowanie
Ben Aaronson,
@Ben Aaronson Po prostu edytuję kod, aby pokazać, jak tworzony jest obiekt parametru żądania
InformedA
Konstruktor jest tutaj niepotrzebny (i szczerze mówiąc, ogólnie nadużywany / zbyt zalecany wzorzec); nie ma skomplikowanych zasad dotyczących konfiguracji parametrów , więc obiekt parametru jest całkowicie w porządku, jeśli istnieje uzasadniona obawa, że ​​parametry metody są złożone lub często zmieniane. Zgadzam się z @BenAaronson, chociaż celem użycia obiektu parametru nie jest uwzględnienie niepotrzebnych parametrów od samego początku, ale ułatwienie ich późniejszego dodania (nawet jeśli istnieje wiele implementacji interfejsu).
Aaronaught
7

Nie potrzebujesz go teraz, więc nie dodawaj go. Jeśli będziesz go później potrzebować, rozszerz interfejs:

public interface IEventGetter {

    public List<FooType> getFooList(String fooName, Date start)
         throws Exception;
    ....

}

public interface IBoundedEventGetter extends IEventGetter {

    public List<FooType> getFooList(String fooName, Date start, Date end)
        throws Exception;
    ....

}
Todd R.
źródło
+1 za brak zmiany istniejącego (i prawdopodobnie już przetestowanego / wysłanego) interfejsu.
Sebastian Godelet
4

Żadna zasada projektowania nie jest absolutna, więc choć w większości zgadzam się z innymi odpowiedziami, pomyślałem, że będę grał w adwokata diabła i omówię niektóre warunki, w których rozważę zaakceptowanie rozwiązania twojego kolegi:

  • Jeśli jest to publiczny interfejs API i przewiduje się, że ta funkcja będzie przydatna dla programistów zewnętrznych, nawet jeśli nie będzie używana wewnętrznie.
  • Jeśli przynosi znaczne natychmiastowe korzyści, nie tylko przyszłe. Jeśli domniemana data zakończenia to Now(), to dodanie parametru usuwa efekt uboczny, który ma zalety w buforowaniu i testowaniu jednostkowym. Może pozwala to na prostszą implementację. A może jest bardziej spójny z innymi interfejsami API w twoim kodzie.
  • Jeśli twoja kultura rozwoju ma problematyczną historię. Jeśli procesy lub terytorium utrudniają zmianę czegoś centralnego, takiego jak interfejs, to co widziałem wcześniej, to ludzie wdrażający obejścia po stronie klienta zamiast zmiany interfejsu, wtedy starasz się utrzymać kilkanaście ad hoc daty końcowej filtry zamiast jednego. Jeśli takie rzeczy zdarzają się często w Twojej firmie, warto włożyć nieco więcej wysiłku w sprawdzanie ich w przyszłości. Nie zrozum mnie źle, lepiej jest zmienić swoje procesy programistyczne, ale zwykle łatwiej to powiedzieć niż zrobić.

Biorąc to pod uwagę, z mojego doświadczenia wynika, że ​​największym następstwem YAGNI jest YDKWFYN: Nie wiesz, jakiej formy będziesz potrzebować (tak, właśnie wymyśliłem ten akronim). Nawet jeśli potrzebuje jakiś parametr limit może być stosunkowo przewidywalne, to może przybrać formę limitem strony lub liczbę dni lub logiczną informując korzystania datę zakończenia z tabeli preferencji użytkownika lub dowolną ilość rzeczy.

Ponieważ nie masz jeszcze takiego wymagania, nie możesz wiedzieć, jakiego typu powinien być ten parametr. Często kończy się to albo niezręcznym interfejsem, który nie jest najlepiej dopasowany do twoich wymagań, albo musisz go zmienić.

Karl Bielefeldt
źródło
2

Brak wystarczających informacji, aby odpowiedzieć na to pytanie. To zależy od tego, co getFooListfaktycznie robi i jak to robi.

Oto oczywisty przykład metody, która powinna obsługiwać dodatkowy parametr, używany lub nie.

void CapitalizeSubstring (String capitalize_me, int start_index);

Wdrożenie metody działającej na kolekcji, w której można określić początek, ale nie koniec kolekcji, jest często głupie.

Naprawdę musisz spojrzeć na sam problem i zapytać, czy parametr jest bezsensowny w kontekście całego interfejsu i naprawdę, ile obciążenia narzuca dodatkowy parametr.

Pytanie C.
źródło
1

Obawiam się, że twój kolega może mieć bardzo ważny punkt. Chociaż jego rozwiązanie nie jest najlepsze.

Z jego proponowanego interfejsu jasno to wynika

public List<FooType> getFooList(String fooName, Date start, Date end) throws Exception;

zwraca instancje znalezione w określonym przedziale czasu. Jeśli klienci obecnie nie używają parametru końcowego, nie zmienia to faktu, że oczekują instancji znalezionych w określonym przedziale czasu. Oznacza to po prostu, że obecnie wszyscy klienci używają otwartych interwałów (od początku do wieczności)

Lepszym interfejsem byłoby więc:

public List<FooType> getFooList(String fooName, Interval interval) throws Exception;

Jeśli podajesz interwał za pomocą statycznej metody fabrycznej:

public static Interval startingAt(Date start) { return new Interval(start, null); }

wtedy klienci nie będą nawet odczuwać potrzeby określania czasu zakończenia.

Jednocześnie twój interfejs bardziej poprawnie przekazuje to, co robi, ponieważ getFooList(String, Date)nie komunikuje, że dotyczy to interwału.

Zauważ, że moja sugestia wynika z tego, co obecnie robi metoda, a nie z tego, co powinna lub może zrobić w przyszłości, i dlatego zasada YAGNI (która jest naprawdę bardzo ważna) nie ma tutaj zastosowania.

Bowmore
źródło
0

Dodanie nieużywanego parametru jest mylące. Ludzie mogą nazwać tę metodę, zakładając, że ta funkcja będzie działać.

Nie dodałbym tego. Późniejsze dodanie go za pomocą refaktoryzacji i mechanicznej naprawy stron wywoławczych jest banalne. W statycznym języku jest to łatwe do zrobienia. Nie trzeba go chętnie dodawać.

Jeśli nie jest to powód, aby tego parametru można uniknąć nieporozumień: Dodawanie dochodzić w realizacji tego interfejsu do wyegzekwowania, że ten parametr jest przekazywana jako wartość domyślną. Jeśli ktoś przypadkowo użyje tego parametru, przynajmniej natychmiast zauważy podczas testowania, że ​​ta funkcja nie jest zaimplementowana. To eliminuje ryzyko wrzucenia błędu do produkcji.

usr
źródło