Jedna metoda z wieloma parametrami vs. wiele metod, które należy wywoływać w kolejności

16

Mam pewne surowe dane, które muszę zrobić wiele rzeczy (przesunąć, obrócić, przeskalować wzdłuż określonej osi, obrócić do ostatecznego położenia) i nie jestem pewien, jaki najlepszy sposób to zrobić, aby utrzymać czytelność kodu. Z jednej strony mogę wykonać jedną metodę z wieloma parametrami (10+), aby zrobić to, czego potrzebuję, ale jest to koszmar czytania kodu. Z drugiej strony mógłbym wykonać wiele metod z 1-3 parametrami, ale metody te trzeba by wywołać w bardzo konkretnej kolejności, aby uzyskać poprawny wynik. Przeczytałem, że najlepiej jest, aby metody zrobiły jedną rzecz i zrobiły to dobrze, ale wydaje się, że posiadanie wielu metod, które trzeba wywołać, otwiera kod dla trudnych do znalezienia błędów.

Czy jest jakiś paradygmat programowania, którego mógłbym użyć, który zminimalizowałby błędy i ułatwiłby odczytanie kodu?

tomsroboty
źródło
3
Największym problemem nie jest „nie wywoływanie ich w kolejności”, to „niewiedza”, że ty (a ściślej przyszły programista) musisz wywoływać je w kolejności. Upewnij się, że każdy programista serwisowy zna szczegóły (Będzie to w dużej mierze zależeć od tego, w jaki sposób dokumentujesz wymagania, projekt i specyfikacje). Używaj testów jednostkowych, komentarzy i zapewniaj funkcje pomocnicze, które przyjmują wszystkie parametry i wywołują pozostałe
mattnz
Podobnie jak uwaga pomocna , płynny interfejs i wzorzec poleceń mogą być przydatne. Jednak to Ty (jako właściciel) i użytkownicy Twojej biblioteki (klienci) decydujesz, który projekt jest najlepszy. Jak podkreślają inni, konieczne jest poinformowanie użytkowników, że operacje nieprzemienne (że są wrażliwe na kolejność wykonywania), bez których użytkownicy nigdy nie dowiedzą się, jak prawidłowo z nich korzystać.
rwong
Przykłady operacji nieprzemiennych: transformacje obrazu (obracanie, skalowanie i kadrowanie), mnożenie macierzy itp.
rwong
Być może możesz użyć curry: uniemożliwiłoby to zastosowanie metod / funkcji w niewłaściwej kolejności.
Giorgio
Na jakim zestawie metod pracujesz tutaj? To znaczy, myślę, że standardem jest przekazanie obiektu transformacji (takiego jak Java Affine Transform for 2D), który przekazujesz do metody, która go stosuje. Zawartość transformacji jest różna w zależności od kolejności, w której wywołujesz początkowe operacje, zgodnie z projektem (więc jest to „wywołujesz go w takiej kolejności, w jakiej potrzebujesz”, a nie „w kolejności, w jakiej chcę”).
Clockwork-Muse

Odpowiedzi:

24

Uwaga na czasowe połączenie . Jednak nie zawsze jest to problem.

Jeśli musisz wykonać kroki w kolejności, oznacza to, że krok 1 tworzy jakiś obiekt wymagany dla kroku 2 (np. Strumień pliku lub inna struktura danych). Tylko to wymaga, aby druga funkcja musiała być wywołana po pierwszej, nie jest nawet możliwe przypadkowe wywołanie ich w niewłaściwej kolejności.

Dzieląc swoją funkcjonalność na małe kawałki, każda część jest łatwiejsza do zrozumienia i zdecydowanie łatwiejsza do przetestowania w izolacji. Jeśli masz ogromną funkcję 100 linii i coś w środkowych przerwach, jak twój nieudany test powie ci, co jest nie tak? Jeśli jedna z pięciu metod linii ulegnie awarii, nieudany test jednostkowy natychmiast przekieruje cię do fragmentu kodu, który wymaga uwagi.

Tak powinien wyglądać złożony kod :

public List<Widget> process(File file) throws IOException {
  try (BufferedReader in = new BufferedReader(new FileReader(file))) {
    List<Widget> widgets = new LinkedList<>();
    String line;
    while ((line = in.readLine()) != null) {
      if (isApplicable(line)) { // Filter blank lines, comments, etc.
        Ore o = preprocess(line);
        Ingot i = smelt(o);
        Alloy a = combine(i, new Nonmetal('C'));
        Widget w = smith(a);
        widgets.add(w);
      }
    }
    return widgets;
  }
}

W dowolnym momencie podczas konwersji nieprzetworzonych danych do gotowego widgetu każda funkcja zwraca coś wymaganego na następnym etapie procesu. Nie można utworzyć stopu z żużla, najpierw należy go wytopić (oczyścić). Nie można utworzyć widgetu bez odpowiedniego zezwolenia (np. Stalowego) jako danych wejściowych.

Konkretne szczegóły każdego kroku zawarte są w poszczególnych funkcjach, które można przetestować: zamiast testowania jednostkowego całego procesu wydobywania skał i tworzenia widżetów, testuj każdy konkretny krok. Teraz masz łatwy sposób na upewnienie się, że jeśli proces tworzenia widgetu się nie powiedzie, możesz zawęzić konkretny powód.

Oprócz korzyści z testowania i sprawdzania poprawności, pisanie kodu w ten sposób jest znacznie łatwiejsze do odczytania. Nikt nie może zrozumieć ogromnej listy parametrów . Podziel go na małe kawałki i pokaż, co oznacza każdy mały kawałek: to można zaśmiecać .

Społeczność
źródło
2
Dzięki, myślę, że to dobry sposób na rozwiązanie problemu. Mimo że zwiększa liczbę obiektów (i może wydawać się to niepotrzebne), wymusza porządek, zachowując czytelność.
tomsrobots
10

Argument „należy wykonać w kolejności” jest dyskusyjny, ponieważ prawie cały kod musi być wykonywany we właściwej kolejności. W końcu nie możesz zapisać pliku, a następnie go otworzyć, a następnie zamknąć, prawda?

Powinieneś skoncentrować się na tym, co czyni twój kod najbardziej łatwym w utrzymaniu. Zazwyczaj oznacza to pisanie funkcji, które są małe i łatwe do zrozumienia. Każda funkcja powinna mieć jeden cel i nie mieć nieprzewidzianych skutków ubocznych.

Dave Nay
źródło
5

Stworzyłbym » ImageProcesssor « (lub dowolną nazwę odpowiednią dla twojego projektu) oraz obiekt konfiguracyjny ProcessConfiguration , który przechowuje wszystkie niezbędne parametry.

 ImageProcessor p = new ImageProcessor();

 ProcessConfiguration config = new processConfiguration().setTranslateX(100)
                                                         .setTranslateY(100)
                                                         .setRotationAngle(45);
 p.process(image, config);

Wewnątrz procesora obrazu zawarto cały proces za jednym mehtodem process()

public class ImageProcessor {

    public Image process(Image i, ProcessConfiguration c){
        Image processedImage=i.getCopy();
        shift(processedImage, c);
        rotate(processedImage, c);
        return processedImage;
    }

    private void rotate(Image i, ProcessConfiguration c) {
        //rotate
    }

    private void shift(Image i, ProcessConfiguration c) {
        //shift
    }
}

Metoda ta nazywa metod transformacyjnych w odpowiedniej kolejności shift(), rotate(). Każda metoda pobiera odpowiednie parametry z przekazanej konfiguracji procesu .

public class ProcessConfiguration {

    private int translateX;

    private int rotationAngle;

    public int getRotationAngle() {
        return rotationAngle;
    }

    public ProcessConfiguration setRotationAngle(int rotationAngle){
        this.rotationAngle=rotationAngle;
        return this;
    }

    public int getTranslateY() {
        return translateY;
    }

    public ProcessConfiguration setTranslateY(int translateY) {
        this.translateY = translateY;
        return this;
    }

    public int getTranslateX() {
        return translateX;
    }

    public ProcessConfiguration setTranslateX(int translateX) {
        this.translateX = translateX;
        return this;
    }

    private int translateY;

}

Użyłem płynnych interfejsów

public ProcessConfiguration setRotationAngle(int rotationAngle){
    this.rotationAngle=rotationAngle;
    return this;
}

co pozwala na dobrą inicjalizację (jak pokazano powyżej).

Oczywista zaleta polegająca na enkapsulacji niezbędnych parametrów w jednym obiekcie. Podpisy twojej metody stają się czytelne:

private void shift(Image i, ProcessConfiguration c)

Chodzi o przesuwanie się obrazu i szczegółowe parametry są w jakiś sposób skonfigurowane .

Alternatywnie możesz utworzyć ProcessingPipeline :

public class ProcessingPipeLine {

    Image i;

    public ProcessingPipeLine(Image i){
        this.i=i;
    };

    public ProcessingPipeLine shift(Coordinates c){
        shiftImage(c);
        return this;
    }

    public ProcessingPipeLine rotate(int a){
        rotateImage(a);
        return this;
    }

    public Image getResultingImage(){
        return i;
    }

    private void rotateImage(int angle) {
        //shift
    }

    private void shiftImage(Coordinates c) {
        //shift
    }

}

Wywołanie metody do metody processImageutworzyłoby instancję takiego potoku i uczyniło przezroczystym co i w jakiej kolejności jest wykonywana: przesunięcie , obrót

public Image processImage(Image i, ProcessConfiguration c){
    Image processedImage=i.getCopy();
    processedImage=new ProcessingPipeLine(processedImage)
            .shift(c.getCoordinates())
            .rotate(c.getRotationAngle())
            .getResultingImage();
    return processedImage;
}
Thomas Junk
źródło
3

Czy zastanawiałeś się nad jakimś curry ? Wyobraź sobie, że masz klasę Processeei klasę Processor:

class Processor
{
    private final Processee _processee;

    public Processor(Processee p)
    {
        _processee = p;
    }

    public void process(T1 a1, T2 a2)
    {
        // Process using a1
        // then process using a2
    }
}

Teraz możesz zastąpić klasę Processordwiema klasami Processor1i Processor2:

class Processor1
{
    private final Processee _processee;

    public Processor1(Processee p)
    {
        _processee = p;
    }

    public Processor2 process(T1 a1)
    {
        // Process using argument a1

        return new Processor2(_processee);
    }
}

class Processor2
{
    private final Processee _processee;

    public Processor(Processee p)
    {
        _processee = p;
    }

    public void process(T2 a2)
    {
        // Process using argument a2
    }
}

Następnie możesz wywoływać operacje we właściwej kolejności, używając:

new Processor1(processee).process(a1).process(a2);

Możesz zastosować ten wzór wiele razy, jeśli masz więcej niż dwa parametry. Możesz także grupować argumenty według własnego uznania, tzn. Nie trzeba, aby każda processmetoda pobierała dokładnie jeden argument.

Giorgio
źródło
Mieliśmy prawie ten sam pomysł;) Jedyna różnica polega na tym, że Twój rurociąg wymusza ścisłe polecenie przetwarzania.
Thomas Junk
@ThomasJunk: O ile rozumiem, jest to wymóg: „aby uzyskać poprawny wynik, należy wywołać te metody w ściśle określonej kolejności”. Posiadanie ścisłego polecenia wykonania brzmi bardzo podobnie do kompozycji funkcji.
Giorgio
I ja też. Ale, jeśli przetwarzanie zamówień zmienia się, trzeba dużo refaktoryzować;)
Thomas Junk
@ThomasJunk: True. To zależy od aplikacji. Jeśli etapy przetwarzania mogą być zamieniane bardzo często, prawdopodobnie twoje podejście jest lepsze.
Giorgio