Używanie sprawdzania typu statycznego w celu ochrony przed błędami biznesowymi


13

Jestem wielkim fanem sprawdzania typu statycznego. Zapobiega popełnianiu takich głupich błędów:

// java code
Adult a = new Adult();
a.setAge("Roger"); //static type checker would complain
a.setName(42); //and here too

Ale to nie przeszkadza ci popełniać tak głupich błędów:

Adult a = new Adult();
// obviously you've mixed up these fields, but type checker won't complain
a.setAge(150); // nobody's ever lived this old
a.setWeight(42); // a 42lb adult would have serious health issues

Problem pojawia się, gdy używasz tego samego typu do reprezentowania oczywiście różnego rodzaju informacji. Myślałem, że dobrym rozwiązaniem byłoby rozszerzenie Integerklasy, aby zapobiec błędom logiki biznesowej, ale nie dodawać funkcjonalności. Na przykład:

class Age extends Integer{};
class Pounds extends Integer{};

class Adult{
    ...
    public void setAge(Age age){..}
    public void setWeight(Pounds pounds){...}
}

Adult a = new Adult();
a.setAge(new Age(42));
a.setWeight(new Pounds(150));

Czy jest to uważane za dobrą praktykę? A może występują nieprzewidziane problemy techniczne związane z tak restrykcyjnym projektem?


5
a.SetAge( new Age(150) )Nadal nie będziesz się kompilować?
John Wu

1
Typ int Java ma ustalony zakres. Oczywiście chcesz mieć liczby całkowite z niestandardowymi zakresami, np. Liczba całkowita <18, 110>. Szukasz typów uściślenia lub typów zależnych, które oferują niektóre (inne niż główne) języki.
Theodoros Chatzigiannakis

3
To, o czym mówisz, to świetny sposób na projektowanie! Niestety, Java ma tak prymitywny system typów, że trudno jest zrobić dobrze. I nawet jeśli spróbujesz to zrobić, może to spowodować skutki uboczne, takie jak niższa wydajność lub niższa wydajność programistów.
Euforia

@JohnWu tak, twój przykład nadal będzie się kompilował, ale jest to pojedynczy punkt niepowodzenia dla tej logiki. Po zadeklarowaniu new Age(...)obiektu nie można niepoprawnie przypisać go do zmiennej typu Weightw żadnym innym miejscu. Zmniejsza liczbę miejsc, w których mogą wystąpić błędy.
J-bob

Odpowiedzi:


12

Zasadniczo pytasz o system jednostek (nie, nie testy jednostkowe, „jednostka” jak w „jednostce fizycznej”, takie jak mierniki, wolty itp.).

W twoim kodzie Agereprezentuje czas i Poundsreprezentuje masę. Prowadzi to do takich rzeczy, jak konwersja jednostek, jednostki podstawowe, precyzja itp.


Były / są próby wprowadzenia czegoś takiego do Javy, na przykład:

Wydaje się, że dwie ostatnie żyją w tym githubie: https://github.com/unitsofmeasurement


C ++ ma jednostki poprzez Boost


LabView jest dostarczany z wieloma jednostkami .


Istnieją inne przykłady w innych językach. (zmiany są mile widziane)


Czy jest to uważane za dobrą praktykę?

Jak widać powyżej, im bardziej prawdopodobne jest, że język obsługuje wartości z jednostkami, tym bardziej natywnie obsługuje jednostki. LabView jest często używany do interakcji z urządzeniami pomiarowymi. W związku z tym sensowne jest posiadanie takiej funkcji w języku i używanie jej z pewnością byłoby uważane za dobrą praktykę.

Ale w każdym języku wysokiego poziomu ogólnego przeznaczenia, gdzie popyt na tak rygor jest niski, prawdopodobnie jest to nieoczekiwane.

A może występują nieprzewidziane problemy techniczne związane z tak restrykcyjnym projektem?

Moje przypuszczenie byłoby: wydajność / pamięci. Jeśli masz do czynienia z wieloma wartościami, narzut obiektu na wartość może stać się problemem. Ale jak zawsze: przedwczesna optymalizacja jest źródłem wszelkiego zła .

Myślę, że większym „problemem” jest przyzwyczajanie się do ludzi, ponieważ jednostka jest zwykle domyślnie zdefiniowana w następujący sposób:

class Adult
{
    ...
    public void setAge(int ageInYears){..}

Ludzie będą zdezorientowani, gdy będą musieli przekazać obiekt jako wartość czegoś, co z pozoru można opisać prostym int, gdy nie będą zaznajomieni z systemami jednostek.


„Ludzie będą zdezorientowani, gdy będą musieli przekazać przedmiot jako wartość czegoś, co z pozoru można by opisać prostym int...” - dla którego mamy tutaj przewodnika najmniejszej niespodzianki. Dobry chwyt
Greg Burghardt

1
Myślę, że niestandardowe zakresy przenoszą to z dziedziny biblioteki jednostek do typów zależnych
jk.

6

W przeciwieństwie do odpowiedzi zerowej, zdefiniowanie typu dla „jednostki” może być korzystne, jeśli liczba całkowita nie wystarcza do opisania pomiaru. Na przykład masa jest często mierzona w wielu jednostkach w tym samym systemie pomiarowym. Pomyśl „funty” i „uncje” lub „kilogramy” i „gramy”.

Jeśli potrzebujesz bardziej szczegółowego poziomu pomiaru, zdefiniowanie rodzaju jednostki jest korzystne:

public struct Weight {
    private int pounds;
    private int ounces;

    public Weight(int pounds, int ounces) {
        // Value range checks go here
        // Throw exception if ounces is greater than 16?
    }

    // Getters go here
}

W przypadku „wieku” zalecam obliczanie tego w czasie wykonywania na podstawie daty urodzenia osoby:

public class Adult {
    private Date birthDate;

    public Interval getCurrentAge() {
        return calculateAge(Date.now());
    }

    public Interval calculateAge(Date date) {
        // Return interval between birthDate and date
    }
}

2

To, czego szukasz, jest znane jako typy oznaczone . Są sposobem na powiedzenie „jest to liczba całkowita reprezentująca wiek”, podczas gdy „jest to także liczba całkowita, ale reprezentuje wagę” i „nie można przypisać jednej do drugiej”. Zauważ, że wykracza to poza jednostki fizyczne, takie jak metry lub kilogramy: w moim programie mogę mieć „wysokości ludzi” i „odległości między punktami na mapie”, obie mierzone w metrach, ale nie ze sobą kompatybilne, ponieważ przypisałem jedną do drugi nie ma sensu z punktu widzenia logiki biznesowej.

Niektóre języki, takie jak Scala, dość łatwo obsługują oznaczone typy (patrz link powyżej). W innych możesz tworzyć własne klasy opakowań, ale jest to mniej wygodne.

Walidacja, np. Sprawdzenie, czy wzrost osoby jest „rozsądny”, to inna kwestia. Możesz wstawić taki kod do swojej Adultklasy (konstruktora lub seterów) lub wewnątrz otagowanych klas typu / otoki. W pewien sposób wbudowane klasy, takie jak URLlub UUIDspełniające taką rolę (między innymi, np. Zapewniając metody użytkowe).

To, czy użycie oznaczonych typów lub klas opakowań rzeczywiście pomoże ulepszyć kod, będzie zależeć od kilku czynników. Jeśli twoje obiekty są proste i mają niewiele pól, ryzyko niewłaściwego przypisania ich jest niskie, a dodatkowy kod potrzebny do użycia oznaczonych typów może nie być wart wysiłku. W złożonych systemach ze złożonymi strukturami i dużą ilością pól (zwłaszcza jeśli wiele z nich ma ten sam prymitywny typ), może to być naprawdę pomocne.

W kodzie, który piszę, często tworzę klasy otoki, jeśli przekazuję mapy. Typy Map<String, String>same w sobie są bardzo nieprzejrzyste, więc zawijanie ich w klasy znaczącymi nazwami, na przykład, bardzo NameToAddresspomaga. Oczywiście przy typach oznaczonych możesz pisać Map<Name, Address>i nie potrzebujesz opakowania dla całej mapy.

Jednak w przypadku prostych typów, takich jak Strings lub Integers, stwierdziłem, że klasy opakowań (w Javie) są zbyt uciążliwe. Zwykła logika biznesowa nie była taka zła, ale pojawiło się wiele problemów z serializacją tych typów do JSON, mapowaniem ich do obiektów DB itp. Możesz pisać mapery i hooki dla wszystkich dużych frameworków (np. Jackson i Spring Data), ale dodatkowa praca i konserwacja związana z tym kodem zrekompensują wszelkie korzyści wynikające z używania tych opakowań. Oczywiście w YMMV iw innym systemie bilans może być inny.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.