Zabezpieczenie kolejności wykonywania metod

0

Jak zabezpieczacie kolejnosc wykonywania metod, zeby ktos w przyszlosci nie zmienil ich kolejnosci powodujac blad w kodzie? Uzywacie do tego np. bool w C#?

Mam np. kod - piersze rozwiazanie:

public void RunEvent() 
{
    var xs = GetXs(); // <-- return type void
    InitAndRemoveXs(); // <-- return type void
    MergeNewXsWithOldXs(xs); // <-- return type void
}

Metoda InitAndRemoveXs() byla duzo wczesniej przez kogos napisana i jest dosc rozbudowana. Chcialem dopisac metode GetXs oraz MergeNewXsWithOldXs.

Drugie rozwiazanie:

public void RunEvent() 
{
    var xs = InitAndRemoveXs(); // <-- return type "jakis typ klasy"
    MergeNewXsWithOldXs(xs); // <-- return type void
}

W drugim rozwiazaniu przenosze metode GetXs do metody InitAndRemoveXs . Dzieki temu zebezpieczam sie przed przeniesieniem przez kogos innego w przyszlosci metody GetXs ponizej metody InitAndRemoveXs Metoda InitAndRemoveXs jest uzywana tez w kilku innych miejscach w aplikacji, wiec nie moge sobie zmieniac po prostu jej nazwy, bo zaraz byly by pytania, po co zmienilem.

3

Drugie rozwiązanie wygląda na lepszy wybór niż pierwsze, natomiast jeszcze lepsze byłoby zabezpieczenie w postaci napisania testów jednostkowych.

1
still.still napisał(a):

Jak zabezpieczacie kolejnosc wykonywania metod, zeby ktos w przyszlosci nie zmienil ich kolejnosci powodujac blad w kodzie?

Najlepiej w ogóle. Unikając efektów ubocznych i trzymając zależności na widoku perfekcyjnie kolejność wykonywania metod jest dowolna lub wymuszona jak w drugim przypadku. Kod powinien być perfekcyjnie tak napisany żeby wprowadzanie takich błędów nie było możliwe. No i testy jak wyżej.

4

Jak masz tych metod więcej i muszą być wykonane w odpowiedniej sekwencji, której zmiana powinna być zasygnalizowana to możesz ratować się czymś w rodzaju wzorca Chain Of Responsibility. Gdzie masz handlery z logiką które wykonywane są jeden po drugim i każdy kolejny sprawdza stan poprzedniego. Jeżeli któryś z handlerów uzna że warunek wykonania jest niespełniony z jakiegoś powodu to przerywa wykonanie.

screenshot-20231030181345.png
W pseudo codzie by to wyglądało mniej więcej tak

public void Execute()
{
  Handler h1 = new ConcreteHandler1();
  Handler h2 = new ConcreteHandler2();
  Handler h3 = new ConcreteHandler3();
  
  h1.SetNext(h2);
  h2.SetNext(h3);
  
  h1.HandleRequest(request);
}

public class ConcreteHandler2 : Handler
{
    public override void HandleRequest(Request request)
    {
        // wykonaj logikę
        
        if (next != null && next.CanHandle)
        {
            next.HandleRequest(request);
        }
        else
        {
           // przerwij wykonanie
        }
    }
}

1

Nie wiem jak w C# miałaby wyglądać implementacja, ale może taka koncepcja: prosty automat stanów (Step1->Step2->Step3) -> eventem jest wywołanie metody, tranzycja możliwa tylko do następnego stanu. Przykłady w javie.

public class ActionSequence {

    public static class FirstStep implements ActionGetXs {
        @Override
        public ActionInitAndRemove doAction() {
            System.out.println("Step1");
            return new SecondStep();
        }
    }

    public static class SecondStep implements ActionInitAndRemove {
        @Override
        public ActionMergeOldWithNew doAction() {
            System.out.println("Step2");
            return new FinalStep();
        }
    }

    public static class FinalStep implements ActionMergeOldWithNew {
        @Override
        public void doAction() {
            System.out.println("Step3");
        }
    }

    public interface ActionGetXs {
        ActionInitAndRemove doAction();
    }

    public interface ActionInitAndRemove {
        ActionMergeOldWithNew doAction();
    }

    public interface ActionMergeOldWithNew {
        void doAction();
    }

    public static void main(String[] args) {
        ActionGetXs firstStep = new FirstStep();
        ActionInitAndRemove secondStep = firstStep.doAction();
        ActionMergeOldWithNew finalStep = secondStep.doAction();
        finalStep.doAction();
    }
}

albo z wykorzystaniem interfejsu funkcyjnego Supplier:

import java.util.function.Supplier;

public class ActionSequenceFunctional {

    @FunctionalInterface
    public interface FirstStep {
        Supplier<SecondStep> doAction();
    }

    @FunctionalInterface
    public interface SecondStep {
        Supplier<FinalStep> doAction();
    }

    @FunctionalInterface
    public interface FinalStep {
        void doAction();
    }

    public static FinalStep createFinalStep() {
        return () -> System.out.println("Step3");
    }

    public static SecondStep createSecondStep() {
        return () -> {
            System.out.println("Step2");
            return () -> createFinalStep();
        };
    }

    public static FirstStep createFirstStep() {
        return () -> {
            System.out.println("Step1");
            return () -> createSecondStep();
        };
    }

    public static void main(String[] args) {
        FirstStep firstStep = createFirstStep();
        SecondStep secondStep = firstStep.doAction().get();
        FinalStep finalStep = secondStep.doAction().get();
        finalStep.doAction();
    }
}

2

Zrefaktoruj tak, żeby kolejność była wymuszona typami. Z drugiej strony wydaje mi się, że w tej sytuacji masz burdel w kodzie i będzie ciężko. Osobiście celowałbym w refaktor, ostatecznie nie robiłbym nic, bo jak wprowadzisz coś innego (np. wymuszona kolejność poprzez przepychaną wartość danego typu, która nie zawiera żadnych danych) to będzie jeszcze gorzej

0
slsy napisał(a):

Zrefaktoruj tak, żeby kolejność była wymuszona typami. Z drugiej strony wydaje mi się, że w tej sytuacji masz burdel w kodzie i będzie ciężko. Osobiście celowałbym w refaktor, ostatecznie nie robiłbym nic, bo jak wprowadzisz coś innego (np. wymuszona kolejność poprzez przepychaną wartość danego typu, która nie zawiera żadnych danych) to będzie jeszcze gorzej

Chyba to samo chcę powiedzieć w innych słowach.
Ja mam przed oczami filozofię buildera (choć niekoniecznie builder w sensie ścisłym), kolejne metody zwracają inny typ (mam ochotę go nazwać: etap buildera), Z WŁAŚCIWYMI MOZLIWOŚCIAMI / lub bez nich

still.still napisał(a):

Jak zabezpieczacie kolejnosc wykonywania metod, zeby ktos w przyszlosci nie zmienil ich kolejnosci powodujac blad w kodzie? Uzywacie do tego np. bool w C#?

Pachnie jakbyś autor tego kodu wieki pracował w jakimś proceduralnym kodzie bez enkapsulacji ...
EDIT: żeby było jeszcze śmieszniej, bardziej "trendi" , to udający obiektowy, bo przecież metody są

0

To wyglada jak proceduralny kod, napisany przez kolejne pokolenia studentow, ktorego nie mozna dotykac, bo nikt nie chce, zeby sie wywalilo po refaktoryzacji u klienta, a trudno wszystko przykryc testami jednostkowymi. Nie ma tez pieniedzy na refaktoring, bo za wszystkie taski klient placi. A klient nie za placi za refactoring, bo przeciez dziala, to po co zmieniac.

Burdel jest spowodowany przez kolejne pokolenia studentow. A jak chcialem zrobic drobny refactoring, to teamlead powiedzial, ze nie, bo trzeba bedzie zrobic wiekszy refactoring. Ja: A kiedy bedzie ten wiekszy refactoring? Teamlead: Na razie nie bedziemy robic.

1
still.still napisał(a):

Jak zabezpieczacie kolejnosc wykonywania metod, zeby ktos w przyszlosci nie zmienil ich kolejnosci powodujac blad w kodzie? Uzywacie do tego np. bool w C#?

To moim zdaniem jest trochę słabe podejście z kilku powodów:

  • po pierwsze, takie gdybanie na przyszłośc nie jest dobre. YAGNI.
  • po drugie, to jest raczej średni pomysł żeby komuś "zabraniać" czegoś
  • po trzecie, najprawdopodobnie, jesli ktoś zmieni kolejność tych metod to będzie miał ku temu powód
still.still napisał(a):

W drugim rozwiazaniu przenosze metode GetXs do metody InitAndRemoveXs . Dzieki temu zebezpieczam sie przed przeniesieniem przez kogos innego w przyszlosci metody GetXs ponizej metody InitAndRemoveXs Metoda InitAndRemoveXs jest uzywana tez w kilku innych miejscach w aplikacji, wiec nie moge sobie zmieniac po prostu jej nazwy, bo zaraz byly by pytania, po co zmienilem.

Jeśli te metody faktycznie są tak ze sobą ściśle związane to może mógłbys je wynieść do klasy osobnej?

Poza tym, jeśli to jest na prawdę tak ważne, że jedna metoda musi być wywołana po drugiej, bo jak nie to bug, to po prostu napisz pod to test automatyczny.

Bo jak piszesz o tym jakie wartości zwracać; to trochę tak jakbyś wymagał od systemu typów więcej niż on jest w stanie zrobić - tzn. to wygląda tak jakbyś chciał żeby system typów w C# Ci przewidział kiedy jakie metody można wywołać. A tak się nie da.

2

W językach z typami najprościej uzyskiwać takie rzeczy nie bojąc się używać typów.

Implementacja:

class FirstStepContext
{
     public SecondStepContext DoFirstThing();
}

class SecondStepContext
{
     public ThirdStepContext DoNextThing();
}

class ThirdStepContext
{
     public int GetFinalResult();
}

Użycie:

var x = firstClassContext
    .DoFirstThing()
    .DoNextThing()
    .GetFinalResult();
0
somekind napisał(a):

W językach z typami najprościej uzyskiwać takie rzeczy nie bojąc się używać typów.

No tak, to jest dobry pół środek, ale to nadal nie "zabezpiecza" przed wywołaniem. Nadal możesz zrobić new ThirdStepContext().GetFinalResult().

1
Riddle napisał(a):

No tak, to jest dobry pół środek, ale to nadal nie "zabezpiecza" przed wywołaniem. Nadal możesz zrobić new ThirdStepContext().GetFinalResult().

No przykład jest uproszczony, ale zakładam, że jakieś dane są w tym kontekście przekazywane, konstruktory mają parametry, a więc takie coś nie przejdzie.
Tzn. przejdzie, ale nie będzie w tym nic złego.
Można też kombinować z zagnieżdżeniem klas kontekstów i prywatnymi konstruktorami, aby kod jeszcze bardziej uszczelnić.

1

Nie trzeba kombinować z konstruktorami, można zwrócić interfejs a wszystkie klasy oprócz podstawowej zrobić jako internal w osobnym assembly:

var builder = new FirstStepContext();

builder.DoFirstThing()
  .DoNextThing()
  .GetFinalResult();

public interface IFirstStepContext
{
    ISecondStepContext DoFirstThing();
}

public interface ISecondStepContext
{
    public IThirdStepContext DoNextThing();
}

public interface IThirdStepContext
{
    public int GetFinalResult();
}


public class FirstStepContext : IFirstStepContext
{
    public ISecondStepContext DoFirstThing()
    {
        return new SecondStepContext(); 
    }
}

internal class SecondStepContext : ISecondStepContext
{
    public IThirdStepContext DoNextThing()
    {
        return new ThirdStepContext();  
    }
}

internal class ThirdStepContext : IThirdStepContext
{
    public int GetFinalResult()
    {
        return 0;
    }
}

Wtedy nie ma możliwości użycia:

new ThirdStepContext().GetFinalResult().

BTW. tak w ogóle znam ten wzorzec jako FluentBuilder.

0
somekind napisał(a):
Riddle napisał(a):

No tak, to jest dobry pół środek, ale to nadal nie "zabezpiecza" przed wywołaniem. Nadal możesz zrobić new ThirdStepContext().GetFinalResult().

No przykład jest uproszczony, ale zakładam, że jakieś dane są w tym kontekście przekazywane, konstruktory mają parametry, a więc takie coś nie przejdzie.
Tzn. przejdzie, ale nie będzie w tym nic złego.
Można też kombinować z zagnieżdżeniem klas kontekstów i prywatnymi konstruktorami, aby kod jeszcze bardziej uszczelnić.

Oczywiście, możesz to bardziej próbować zawężać typami.

Ale chodzi mi o to, że to nadal nie jest "zabezpieczenie" przed wywołaniem. Nie możesz nigdy typami wymusić że jakaś metoda była wywołana.

Ktoś by powiedział - jaki jest sens takiego zabiegu, skoro można napisać test automatyczny pod to wywołanie, które jest dużo prostsze.

0
Wilktar napisał(a):

Nie trzeba kombinować z konstruktorami, można zwrócić interfejs a wszystkie klasy oprócz podstawowej zrobić jako internal w osobnym assembly:

Tak, tylko posiadanie jednej implementacji interfejsu to code smell, więc ja bym nie szedł w takim kierunku. Klasy w zupełności wystarczają.

0

Zabranianie nic nie da bo i tak jak ktos bedzie chcial to zmieni ;)

Moje rozwiazanie. Zrob takie spagetti zeby jak ktos tam zajrzy to sobie pomyslal "o kurczę" i sam opakuje to wszystko w kolejna metode tak jak ty zrobiles z wczesniejszym rozwiazaniem (tj metoda InitAndRemoveXs()) ;)

1
still.still napisał(a):

W drugim rozwiazaniu przenosze metode GetXs do metody InitAndRemoveXs
Metoda InitAndRemoveXs jest uzywana tez w kilku innych miejscach w aplikacji,

zaraz, metoda jest używana jeszcze w innych miejscach a Ty dopisujesz coś na jej początku?

0
Miang napisał(a):
still.still napisał(a):

W drugim rozwiazaniu przenosze metode GetXs do metody InitAndRemoveXs
Metoda InitAndRemoveXs jest uzywana tez w kilku innych miejscach w aplikacji,

zaraz, metoda jest używana jeszcze w innych miejscach a Ty dopisujesz coś na jej początku?

tak, ale dalem to jako drugie (wiec gorsze) rozwiazanie, a dopisalbym tylko odczyt danych. (To tez gorsze rozwiazanie ze wzgledu na to, ze i tak metoda InitAndRemoveXs powinna byc mocno zrefaktorowana.)

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