7 wskazówek, jak sprawić, by biblioteka Kotlin zabłysła

Przedmowa

Kiedy zaczynamy programować w nowym języku, na początku często trzymamy się paradygmatów i nawyków wypracowanych podczas programowania w języku, który już znamy. Chociaż na początku może się to wydawać w porządku, dla osób, które programują w języku, który próbujesz opanować przez pewien czas, ta szorstkość jest dość oczywista.

Kiedy dwa lata temu właśnie przestawiłem się z Javy na Kotlin, w zasadzie zajmowałem się „programowaniem Java, ale w Kotlinie”. Chociaż Kotlin był dla mnie jak powiew świeżego powietrza, jako inżynier Androida korzystałem z tych wszystkich drobiazgowych funkcji rozszerzeń i klas danych i nie zawsze byłem konsekwentny. Konwertowałem istniejący kod Java na Kotlin i patrzenie na niego po pewnym czasie pomogło mi pamiętać o kilku pomysłach, kiedy projektowałem API dla różnych komponentów lub bibliotek wielokrotnego użytku w Kotlin.

W BUX pracujemy nad dwiema aplikacjami, które mają kilka wspólnych bibliotek. Chociaż aplikacja Stocks (która jeszcze nie została wydana) została napisana w Kotlinie od samego początku, pierwsza aplikacja CFD została napisana w Javie, a następnie po pewnym czasie przekonwertowana na Kotlin. W tej chwili aplikacja CFD ma 78,7% Kotlin, co oznacza, że ​​wciąż nad nią pracujemy i dlatego ważne jest, aby zwracać uwagę na interfejsy API bibliotek obsługiwane przez dwa zespoły, które używają jednocześnie Java i Kotlin.

Więc jeśli masz istniejącą bibliotekę Kotlin lub Java, którą chcesz Kotlinify lub projektujesz interfejs API za pomocą Kotlin od zera, napisz do mnie kilka pomysłów, co możesz zrobić, aby ułatwić życie użytkownikom biblioteki.

1. Funkcje rozszerzeń

Przez większość czasu, kiedy trzeba rozszerzyć istniejącą klasę o nową funkcjonalność, albo używasz jakiegoś rodzaju kompozycji, albo czerpiesz nową klasę (która, jeśli jest używana w szerokim zakresie, może uczynić twoją hierarchię klas tak delikatną jak szklane kubki z IKEA). Bez względu na twoje preferencje, Kotlin ma na to własną odpowiedź, zwaną funkcjami rozszerzenia. Krótko mówiąc, pozwala ci dodać nową funkcję do istniejącej klasy z całkiem porządną składnią. Na przykład w Androidzie możesz zdefiniować nową metodę dla klasy View w następujący sposób:

Mając to na uwadze, wiele bibliotek, które zapewniają dodatkową funkcjonalność dla istniejących klas (np. Widok, Kontekst itp.), Zamiast używać dekoratorów, statycznych metod fabrycznych lub czegoś innego, może korzystać z zapewniania ich funkcjonalności jako funkcji rozszerzenia płynnie, tak jakby to funkcjonalność istniałaby w oryginalnych klasach od samego początku.

2. Domyślne wartości argumentów

Kotlin został pierwotnie zaprojektowany jako zwięzła, schludna i uporządkowana wersja Javy i okazuje się, że robi to bardzo dobrze z domyślnymi wartościami argumentów funkcji. Dostawca interfejsu API może określić domyślne wartości argumentów, które można pominąć. Byłbym naprawdę zaskoczony, gdybyś nie widział podobnego kodu podczas pracy z SQLite w Androidzie:

Chociaż w tej metodzie jest więcej wad niż tych brzydkich zer na końcu, jestem tutaj, aby nie oceniać, ale raczej powiedzieć, że te czasy minęły i poważnie zakwestionowałem kod napisany w ten sposób w Kotlinie. Istnieje wiele takich przykładów, ale na szczęście możemy teraz zrobić lepiej i jeśli podasz zewnętrzny interfejs API z pewnymi parametrami strojenia, zamiast lub oprócz dostarczenia Konstruktora, rozważ użycie domyślnych wartości argumentów. Będzie to miało co najmniej dwie zalety: po pierwsze, nie zmusisz użytkownika do określenia opcjonalnych parametrów lub czegokolwiek, co możesz przewidzieć wcześniej, a po drugie, użytkownik będzie miał domyślną konfigurację, która po prostu zadziała -pudełko:

Warto również wspomnieć, że jeśli umieścisz te argumenty z wartościami domyślnymi na końcu, użytkownik nie będzie musiał określać nazw dla parametrów obowiązkowych.

3. Przedmioty

Czy kiedykolwiek sam musiałeś wdrożyć wzór Singleton w Javie? Prawdopodobnie miałeś, a jeśli tak, powinieneś wiedzieć, jakie to może być kłopotliwe:

Istnieją różne implementacje, z których każda ma swoje zalety i wady. Kotlin rozwiązuje wszystkie te 50 odcieni implementacji wzorca Singleton za pomocą jednej struktury zwanej deklaracją obiektu. Spójrz, nie ma „podwójnie sprawdzonej blokady”:

Co ciekawe, oprócz składni, inicjalizacja deklaracji obiektu jest bezpieczna dla wątków i leniwie inicjowana przy pierwszym dostępie:

W przypadku takich bibliotek, jak Fabric, Glide, Picasso lub dowolna inna, która używa pojedynczej instancji obiektu jako głównego punktu wejścia do ogólnego interfejsu API biblioteki, jest to naturalny sposób na przejście teraz i nie ma powodu, aby używać starej metody Java rób te same rzeczy.

4. Pliki źródłowe

W ten sam sposób, w jaki Kotlin przemyśla wiele problemów składniowych, z którymi stykamy się na co dzień, zmienia się także sposób, w jaki organizujemy tworzony przez nas kod. Pliki źródłowe Kotlin służą jako baza wielu deklaracji, które są semantycznie blisko siebie. Okazuje się, że są idealnym miejscem do zdefiniowania niektórych funkcji rozszerzenia związanych z tą samą klasą. Sprawdź ten uproszczony fragment kodu źródłowego Kotlin, w którym wszystkie funkcje rozszerzenia używane do manipulowania tekstem znajdują się w tym samym pliku „Strings.kt”:

Innym odpowiednim przykładem jego zastosowania jest zdefiniowanie protokołu komunikacji z interfejsem API wraz z klasami danych i interfejsami w jednym pliku źródłowym. Pozwoli to użytkownikowi nie stracić uwagi, przełączając się między różnymi plikami podczas śledzenia przepływu:

Ale nie posuwaj się za daleko, ponieważ ryzykujesz przytłoczeniem użytkownika rozmiarem pliku i przekształceniem go w klasę RecyclerView z ~ 13 000 linii .

5. Coroutines

Jeśli biblioteka korzysta z wielu wątków w celu uzyskania dostępu do sieci lub wykonania innej długotrwałej pracy, rozważ udostępnienie interfejsu API z funkcjami zawieszenia. Począwszy od Kotlin 1.3, już nie eksperymentujemy, co jest świetną okazją, aby zacząć je wykorzystywać w produkcji, jeśli wcześniej mieliście wątpliwości. Coroutines w niektórych przypadkach mogą być dobrą alternatywą dlaObservable z RxJava i innych sposobów radzenia sobie z wywołaniami asynchronicznymi. To, co mogłeś już wcześniej zobaczyć, to API pełne metod z wywołaniami zwrotnymi wyników lub w czasie szumu Rx opakowanego w Single lub Completable:

Ale teraz jesteśmy w erze Kotlina, więc można go zaprojektować również na korzyść użycia koroutyn:

Pomimo tego, że coroutines działają lepiej niż stare dobre wątki Java pod względem lekkości, znacznie przyczyniają się do czytelności kodu:

6. Umowy

Oprócz stabilnych coroutines, Kotlin 1.3 wprowadza także programistów sposób interakcji z kompilatorem. Kontrakt to nowa funkcja, która pozwala nam, jako twórcom bibliotek, dzielić się z kompilatorem wiedzą, którą posiadamy, określając tak zwane efekty. Aby ułatwić zrozumienie efektu, przyjmijmy jego najbliższą analogię z Javy. Większość z was prawdopodobnie widziała lub nawet korzystała z klasy Warunków z Guawy z wieloma twierdzeniami i jej adaptacją w bibliotekach takich jak Dagger, które nie chcą pobierać całej biblioteki:

Jeśli przekazany obiekt referencyjny ma wartość NULL, wówczas metoda zgłosi wyjątek, a pozostały kod umieszczony po tej metodzie nie zostanie osiągnięty, dlatego możemy bezpiecznie założyć, że referencja nie będzie tam pusta. Nadchodzi problem - chociaż mamy tę wiedzę, nie pomaga to kompilatorowi w przyjęciu tego samego założenia. To tutaj pojawiają się adnotacje @ Nullable i @ NotNull, ponieważ istnieją one wyłącznie po to, aby pomóc kompilatorowi zrozumieć warunki. Taki jest naprawdę efekt - jest to wskazówka dla kompilatora, która pomaga mu przeprowadzić bardziej wyrafinowaną analizę kodu. Istniejącym efektem, który już widziałeś w Kotlinie, jest rzutowanie inteligentne określonego typu w bloku po sprawdzeniu jego typu:

Kompilator Kotlin jest już sprytny i bez wątpienia będzie się poprawiał w przyszłości, ale wraz z wprowadzeniem kontraktów, programiści Kotlin dali nam, jako twórcom bibliotek, możliwość ulepszenia go, pisząc własne inteligentne obsady i tworząc efekty, które pomogą naszym użytkownikom napisz czystszy kod. Teraz przepiszmy ponownie metodę checkNotNul w Kotlinie, używając kontraktów, aby zademonstrować ich możliwości:

Istnieją również różne efekty, ale nie jest to pełnowymiarowe wprowadzenie do kontraktów i mam nadzieję, że po prostu dało ci wyobrażenie o tym, jak możesz z niego skorzystać. Ponadto repozytorium stdlib w Github zawiera wiele przydatnych przykładów kontraktów, które warto sprawdzić.

Z wielką mocą wiąże się wielka odpowiedzialność, a umowy nie są wyjątkiem. Cokolwiek podasz w swoim kontrakcie, kompilator traktuje go jak Biblię Świętą. Z technicznego punktu widzenia kompilator nie kwestionuje ani nie weryfikuje wszystkiego, co tam piszesz, więc musisz dokładnie sprawdzić swój kod i upewnić się, że nie wprowadzasz żadnych niezgodności.

Jak można zauważyć z adnotacji @ExperimentalContracts, kontrakty są nadal w fazie eksperymentalnej, więc nie tylko interfejs API może się zmieniać z czasem, ale także niektóre nowe funkcje mogą z niego wynikać, gdy staje się coraz bardziej dojrzały.

7. Interoperacyjność Java

Podczas pisania biblioteki w Kotlin ważne jest także poszerzenie grona odbiorców, zapewniając płynną integrację dla innych programistów używających Javy. Jest to bardzo ważne, ponieważ wciąż istnieje wiele projektów, które trzymają się Javy i nie chcą przepisywać istniejącego kodu z różnych powodów. Ponieważ chcemy, aby społeczność Kotlin rosła szybciej, lepiej pozwolić tym projektom robić to stopniowo, krok po kroku. Oczywiście po tej stronie Muru nie jest tak słonecznie, ale są pewne rzeczy, które możesz jako opiekun biblioteki zrobić.

Pierwszą rzeczą jest to, że funkcje rozszerzenia Kotlin w Javie zostaną skompilowane w brzydkie metody statyczne, gdzie pierwszym argumentem metody jest typ odbiornika:

Chociaż tak naprawdę nie masz tutaj dużej kontroli, możesz przynajmniej zmienić nazwę generowanej klasy, dodając następujący wiersz na początku pliku źródłowego zawierającego funkcje poziomu pakietu powyżej:

Kolejny przykład tego, jak można nieznacznie wpłynąć na wygenerowany kod, jest związany z użyciem metod obiektu towarzyszącego:

Możesz zmusić Kotlin do generowania klas statycznych zamiast funkcji zdefiniowanych w obiekcie towarzyszącym za pomocą adnotacji @JvmStatic:

W porównaniu z Kotlinem, w Javie dwie funkcje o podobnych nazwach, ale różne typy ogólne nie mogą być zdefiniowane razem z powodu wymazywania typu, więc może to stanowić przeszkodę dla użytkowników Java. Mamy nadzieję, że możesz tego uniknąć, zmieniając podpis metody za pomocą innej adnotacji:

I wreszcie możliwość definiowania sprawdzonych wyjątków w funkcjach dla użytkowników Java, chociaż nie są one bezpośrednio dostępne w Kotlin. Może to być przydatne, ponieważ paradygmat deklaracji wyjątku w Javie i Kotlinie jest inny:

Większość rzeczy tutaj jest opcjonalnych, ale z pewnością mogą być źródłem uznania dla twojej biblioteki od użytkowników Java.

Dolna linia

Kotlin to świetny język, który nieustannie się rozwija. Istnieje wiele rzeczy, które możesz zrobić w bibliotece, aby korzystanie z niej było płynne. Spośród wszystkiego, co stosujesz na podstawie powyższych pomysłów, staraj się przede wszystkim być użytkownikiem i zastanów się, co by to było i jak byś z niego korzystał. Diabeł tkwi w szczegółach i mam nadzieję, że te drobiazgi, które zastosujesz, przyniosą korzyści użytkownikom i poprawią ich wrażenia. Twoje zdrowie!