Dwa redundantne interfejsy, czy jeden złożony - wasze opinie?

1

Witam, często borykam się z problemem zaprojektowania dobrego API.

Teraz własnie mam taki case:

  • Wystawiam interfejs dla wielu adapterów. Interfejs wygląda tak, że adapter wywołuje jakąś akcję, inicjalizuje ją, i będzie musiał kiedyś ją zakończyć, ewentualnie przekazując dane.

Robię to tak że

class ExampleAdapter {
  public void doStuff(MyApi api) {
    Consumer<Param> endOperation = api.initializeOperation(param1, param2, param3);

    // do stuff

    endOperation.accept(param4);
  }
}

Ale niektóre adaptery, nie potrzebują tej "dualności" osobnej inicjalizacji i finalizacji, i mogłyby zrobić wszystko w jednym callu, wyglądają tak:

class SimpleAdapter {
  public void doStuff(MyApi api) {
    api.initializeOperation(param1, param2, param3).accept(param4);
  }
}

I teraz się zastanawiam, zostawić tak jak jest? Czy dodać do MyApi prostszą wersję tej metody, którą możnaby zawołać tak

class SimpleAdapter {
  public void doStuff(MyApi api) {
    api.operation(param1, param2, param3, param4);
  }
}
2

To java?
To zrób metodę domyślną w interfejsie na prostszy przypadek.

0
jarekr000000 napisał(a):

To zrób metodę domyślną w interfejsie na prostszy przypadek.

No przecież nie pytam o to, jak to zrobić -.-

Zastanawiam się czy dwie metody w API, jedna prostsza druga skomplikowana to dobre wyjście; czy lepiej zostawić jedną. Bo jakby nie patrzeć, ona będzie redundantna.

0

Jak da się zaprojektować ten kod w taki sposób, że tylko jedna metoda wystarczy to nawet nie ma o co pytać: im kod prostszy i trudniejszy do popsucia tym lepiej. Takie rozkładanie interfejsu na poszczególne fazy ma sens, jak poprawia to performance z jakiegoś powodu (np. możesz dzięki temu coś reużyć). W innym przypadku KISS i tyle

0
slsy napisał(a):

Jak da się zaprojektować ten kod w taki sposób, że tylko jedna metoda wystarczy to nawet nie ma o co pytać: im kod prostszy i trudniejszy do popsucia tym lepiej. [...] W innym przypadku KISS i tyle

No niby tak, tylko tych caseów kiedy to jest nie potrzebne jest wiele. Więc ta prosta, krótka metoda może być tym S w KISS.

2

Jeden interfejs z dwoma wykluczającymi się zestawami metod wygląda jak coś bardzo nieintuicyjnego.

0
somekind napisał(a):

Jeden interfejs z dwoma wykluczającymi się zestawami metod wygląda jak coś bardzo nieintuicyjnego.

No własnie wiem.

Ale z drugiej strony, to coś jak w bibliotekach do HTTP możesz zrobić zarówno .request('GET', url) jak i .get(url), albo w CLI możesz zrobić print(foo + "\n") albo println(foo). Różne funkcje, robią podobne rzeczy, ale jedna jest kapkę prostsza niż druga.

Stąd mój dylemat.

2

No to pytanie - czy piszesz jakąś bibliotekę narzędziową, czy kod biznesowy. Jeśli to pierwsze, to powinno dawać więcej możliwości, jeśli to drugie, to powinno być bardziej jednoznaczne.

0
somekind napisał(a):

No to pytanie - czy piszesz jakąś bibliotekę narzędziową, czy kod biznesowy. Jeśli to pierwsze, to powinno dawać więcej możliwości [..]

Ale no właśnie, bo ta pierwsza metoda daje większe możliwości.

Nie mogę jej ograniczyć, bo niektóre adaptery jej wymagają, ona musi być.

Pytanie czy dodać drugą, prostszą, która byłaby używana w 90% case'ów.

1

Myślę, że to zależy od konkretnego przypadku. Dajmy na to przykład z print:

//pseudokod

interface IPrintable
{
    void print(string s);
    void printLn(string s);
}

Jak zauważyłeś można to wywołać na różne sposoby. A implementacja tego może wyglądać tak:

class Printable
{
    void print(string s)
    {
        getPrintingDevice().put_string(s);
    }

    void printLn(string s)
    {
        print(s);
        print("\n");
    }
}

W takiej sytuacji nie ma duplikacji kodu, tylko jedna metoda korzysta z drugiej. Jeśli u Ciebie jest to analogicznie, to możesz dodać tę drugą metodę. Jeśli jednak każda z nich robi rzeczy inne lub w inny sposób, to może to prowadzić do duplikacji kodu i późniejszych problemów z utrzymaniem. Nie mówiąc o programiście, który może zacząć się głowić, której powinien użyć i dlaczego. Jeśli jest to tak oczywiste jak print/printLn, to ok. Ale jeśli niekoniecznie, to może jednak zostawić jedną. Albo w jakiś sposób zrobić przeciążenie - ale tylko jeśli ma to sens.

0
Juhas napisał(a):

Myślę, że to zależy od konkretnego przypadku. Dajmy na to przykład z print:

W takiej sytuacji nie ma duplikacji kodu, tylko jedna metoda korzysta z drugiej. Jeśli u Ciebie jest to analogicznie, to możesz dodać tę drugą metodę. Jeśli jednak każda z nich robi rzeczy inne lub w inny sposób, to może to prowadzić do duplikacji kodu i późniejszych problemów z utrzymaniem. Nie mówiąc o programiście, który może zacząć się głowić, której powinien użyć i dlaczego. Jeśli jest to tak oczywiste jak print/printLn, to ok. Ale jeśli niekoniecznie, to może jednak zostawić jedną. Albo w jakiś sposób zrobić przeciążenie - ale tylko jeśli ma to sens.

Nie podoba mi się ten argument, bo ja pytam tylko o interfejs. Nie chciałbym podejmować decyzji nt interfejsu, na podstawie szczegółów implementacyjnych.

Jednak dzięki za wpis.

0

W takim razie jeśli ich rozróżnienie jest naturalne, daj dwie. Jeśli nie, to zacznij od jednej i zobacz co się stanie. Być może po kilku miesiącach okaże się, że potrzebna jest druga. Wtedy będzie update.

0

Co sadzicie o takim rozwiązaniu?
Interface'y:

public interface SmallApi {
    void doStuff();
}
public interface BigApi extends SmallApi {
  void doMoreStuff();
}

Adapter

  void doSomething(BigApi api) {
    System.out.println("big");
    api.doStuff();
    api.doMoreStuff();
  }

Użycie

package sandbox.adapters;

public class Main {
  public static void main(String[] args) {
    SmallApi smallApi = new SmallApi() {
        @Override
        public void doStuff() {
            System.out.println("I'm small api");
        }
    };

    BigApi bigApi =
        new BigApi() {

          @Override
          public void doStuff() {
            System.out.println("I'm big api");
          }

          @Override
          public void doMoreStuff() {
            System.out.println("I'm big API");
          }
        };

      ExampleAdapter adapter = new ExampleAdapter();

      adapter.doSomething(smallApi);
      adapter.doSomething(bigApi);
  }
}

Output

small
I'm small api
big
I'm big api
I'm big API
0

Nie, nie chodzi o coś takiego. Twój przykład przekombinowany na 10tą stronę.

Mój przykład, sytuacja jeden.

interface Api<T> {
  Consumer<T> doStuff(int param);
}

class UseCase<T> {
  public void act(Api<T> api) {
    var a = api.doStuff(2);
    // stuff
    a.accept(something);
  }
}

class UseCaseSimple<T> {
  public void act(Api<T> api) {
    api.doStuff(2).accept(2);
  }
}

I teraz mógłbym dodać redundantną, prostą metodę do tego interfejsu.

interface Api<T> {
  Consumer<T> doStuff(int param);
  void doStuff(int param, T accept); // redundantna metoda, tylko po to żeby uprościć inne calle
}

class UseCase<T> {
  public void act(Api<T> api) {
    var a = api.doStuff(2);
    // stuff
    a.accept(something);
  }
}

class UseCaseSimple<T> {
  public void act(Api<T> api) {
    api.doStuff(2, 2);
  }
}
0

Ok, dopiero wczytałem się w kod, czy coś strasznego się stanie jeżeli ktoś zapomni dać .accept?

Jeżeli tak, to lepiej mieć osobne metody, bo samo initializeOperation(param1, param2, param3) w żaden sposób nie sugeruje użytkownikowi istnienia kontraktu wymagającego wywołania .accept(). Mając 2 różne metody przynajmniej będzie miał szansę się zastanowić.

Z drugiej strony możesz się przed tym zabezpieczyć, jeżeli do metody api przekażesz od razu jakieś ~Runnable zawierające to co ma zostać wykonane pomiędzy initialize() i accept(). W takim wypadku, użytkownik nie będzie mógł naruszyć kontraktu.

0
piotrpo napisał(a):

Ok, dopiero wczytałem się w kod, czy coś strasznego się stanie jeżeli ktoś zapomni dać .accept?

Jeżeli tak, to lepiej mieć osobne metody, bo samo initializeOperation(param1, param2, param3) w żaden sposób nie sugeruje użytkownikowi istnienia kontraktu wymagającego wywołania .accept(). Mając 2 różne metody przynajmniej będzie miał szansę się zastanowić.

Z drugiej strony możesz się przed tym zabezpieczyć, jeżeli do metody api przekażesz od razu jakieś ~Runnable zawierające to co ma zostać wykonane pomiędzy initialize() i accept(). W takim wypadku, użytkownik nie będzie mógł naruszyć kontraktu.

Jeśli nie zawołasz accept() to adapter po prostu nie działa, unit testy failują. Poza tym ignorujesz w ogóle istotę postawionego pytania.

Pytanie brzmiało: "Czy tam gdzie jest jedna, skomplikowana metoda, można dodać drugą, prostszą, ale redundantną?".

0

Oczywiście, że można dodać druga metodę, żeby było łatwiej wywoływać, tak długo jak wiadomo którą kiedy wywołać.

Zasugerowałem jedynie rozwiązanie, w którym nie da się nie wywołać accept()

1 użytkowników online, w tym zalogowanych: 0, gości: 1