Narzędzia AI ułatwiające pisanie kodu
Współcześnie obserwujemy znaczący rozwój sztucznej inteligencji. Ma ona wpływ m.in. na pracę developerów. Czy AI będzie w stanie kompletnie zastąpić programistów? Raczej nie ma na to szans w najbliższej przyszłości. Developerzy powinni jednak cały czas „trzymać rękę na pulsie” i poznawać narzędzia AI. Osoby biegle się nimi posługujące będą miały wkrótce znaczącą przewagę na rynku pracy. Które z nich warto znać? Poniżej przedstawiam te, które mogą realnie ułatwić i przyśpieszyć proces tworzenia oprogramowania.
Nie tylko Chat-GPT
Gdy myślisz o narzędziu AI, które może Ci pomóc z wszelkiego rodzaju problemami, na myśl przychodzi ChatGPT? Podobnie myśli większość osób. W kontekście wytwarzania oprogramowania nie jest to jedyne rozwiązanie. Jako developerzy mamy do wyboru też dedykowane narzędzia, których celem jest ułatwienie pisania kodu. Przykładowe narzędzia to:
- GitHub Copilot – https://github.com/features/copilot
- Tabnine – https://www.tabnine.com/
- Code Snippets – https://codesnippets.ai/
- Cody – https://about.sourcegraph.com/cody
- Amazon CodeWhisper – https://about.sourcegraph.com/cody
Oczywiście, ta lista mogłaby być dużo dłuższa. Na rynku pojawia się coraz więcej tego typu narzędzi. Uważam, że wybór najwygodniejszego i optymalnego jest kwestią mocno subiektywną.
Zanim pomoże Ci AI…
W tym miejscu muszę zwrócić Twoją uwagę na bardzo ważną kwestię, a więc możliwość korzystania z narzędzi AI pod kątem prawnym. Pamiętaj, że jeżeli pracujesz z kodem klienta, nie możesz samodzielnie zdecydować, aby był on przetwarzany na serwerach firm postronnych, które są dostawcą takiego narzędzia.
Pamiętaj o bezpieczeństwie informacji, tajemnicy przedsiębiorstwa oraz danych osobowych. Nie przetwarzaj takich danych w narzędziach AI, jeżeli nie masz pewności, że środowisko, w którym pracujesz jest zamknięte, a dane wprowadzone do narzędzia nie zostaną przetwarzane oraz nie będą przesyłane innym podmiotom. Zweryfikuj czy AI będzie się uczyć na Twoich promptach i outputach. Zanim zdecydujesz się stosować AI w swojej pracy, skonsultuj ten fakt z wewnętrznym działem prawnym, który sprawdzi postanowienia licencyjne narzędzia oraz wyda odpowiednie rekomendacje dot. tego w jaki sposób bezpiecznie wykorzystywać dane rozwiązanie AI.
Jak zacząć korzystać z narzędzi AI?
W tej sekcji przetestuję dwa wybrane narzędzia – Tabnine oraz Cody. Sprawdzę, czy i jakie korzyści w pracy developera daje ich użytkowanie.
Tabnine
Poniżej pokażę użycie Tabnine na przykładzie środowiska Inteliij. Najprostszym sposobem, aby rozpocząć pracę jest po prostu wykorzystanie Markeplace w Inteliij. Wystarczy tam zainstalować wtyczkę Tabnine i po restarcie środowiska możesz już z niej korzystać. Spójrz:
Możesz również przejść na specjalnie przygotowaną stronę: https://www.tabnine.com/install. Tam wystarczy wybrać nasze IDE, a następnie – postępując według dalszej instrukcji – zainstalować wtyczkę.
Sourcegraph Cody
Do instalacji narzędzia Cody również możesz wykorzystać stronę producenta: https://sourcegraph.com/get-cody. Cody może być również zainstalowany nie jako wtyczka, a oddzielna aplikacja. Jednak w dalszej części artykułu będę posługiwał się wtyczką w moim IDE.
Podobnie jak Tabnine wtyczkę Cody możesz zainstalować poprzez Marketplace w IntelliJ. Zobacz, jak to zrobić:
Tabnine w działaniu
Tabnine to narzędzie, które pozwala na autouzupełnianie kodu podczas jego pisania. Wystarczy zacząć pisać funkcję lub metodę, a ono zaproponuje jej szkielet oraz część implementacji. Co więcej, analizując kontekst kodu, w którym pracujemy, potrafi zaproponować rozwiązania bardziej dopasowane do naszego stylu pisania.
Po rozpoczęciu pisania metody Tabnine potrafi z kontekstu (klasa 'OrderService’) zaproponować jej ciało. Zobacz:
Oczywiście to tylko propozycja, którą możesz przyjąć, wciskając klawisz 'TAB’, bądź zobaczyć inną podpowiedź i przejść pomiędzy nimi, używając kombinacji klawiszy 'ALT + [’ bądź klawisza 'option’ w systemie MacOS. Możesz też kontynuować normalne pisanie kodu, a Tabnine będzie starał się podpowiedzieć dalszy fragment – tak jak tutaj:
Mając klasę modelową, możesz otrzymać podpowiedzi dotyczące kolejnych pól pasujących do danego obiektu. Przykład? Zobacz poniżej, co Tabnine sugeruje w klasie Customer po wpisaniu litery 'p’:
Od razu w tym przypadku pojawia się „telefon”. Natomiast w klasie adresu będą to pola takie jak „kraj” czy „ulica”. Co ważne i stanowi dużą zaletę Tabnine, to fakt, że narzędzie to dopasowuje się do naszego stylu pisania kodu. Mając napisany np. builder i dodając kolejne pola, robisz to praktycznie bez wysiłku, zachowując własny wzorzec. Spójrz:
Zauważyłem, że Tabnine może być bardzo pomocny przy pisaniu testów, gdyż może sam uzupełniać dane testowe znanego typu:
Tutaj kolejny przykład, gdy Tabnine zorientował się, co chcę zrobić tylko po nazwie metody:
Moim zdaniem, jest to bardzo pomocne, aczkolwiek podczas korzystania z tego narzędzia czasami pewne rzeczy nie były uwzględnione. Jak w przykładzie poniżej Tabnine zapomniał o dodaniu 'asList()’. Zobacz:
Jednak da się zauważyć, że narzędzie to uczy się całkiem szybko. Przykład? Na początku zawsze używał klasy Arrays do stworzenia listy:
Gdy jednak zamiast podpowiedzi zostanie użyta konstrukcja 'List.of’, Tabnine zaczyna jej używać:
Podsumowanie pracy z Tabnine
Uważam, że jest to bardzo pomocne narzędzie z potencjałem. Da się zaobserwować błędy czy niezrozumienie w niektórych podpowiedziach, aczkolwiek widać również, że Tabnine uczy się naszego stylu pisania. Podpowiedzi nie są nachalne i bez problemu można wcisnąć klawisz TAB, aby skorzystać z podrzuconego kawałka kodu. Szczególnie przydaje się to tam, gdzie musimy stworzyć trochę powtarzalnego kodu. Z Tabnine zadanie to jest ułatwione, a praca przyspiesza.
Praca z Cody
Cody dostarcza mechanizm autouzupełniania podobny do tego w Tabnine. Jest to produkt Sourcegraph, więc jeżeli firma korzysta z tego narzędzia, tym bardziej Cody będzie w stanie nam podpowiedzieć rozwiązania już stosowane wewnątrz organizacji.
Aby nie powtarzać opisu działania autouzupełniania przy przedstawieniu Cody, skupię się na dodatkowych funkcjonalnościach, jakie są tutaj dostępne. Po poprawnym zainstalowaniu w IDE powinna się pojawić nowa ikona. Daje nam ona dostęp do chatu oraz predefiniowanych opcji widocznych poniżej:
Wykrywanie zapachu kodu
Poza tymi predefiniowanymi opcjami możesz też „porozmawiać” w języku naturalnym z Cody o Twoim kodzie. Załóżmy, że masz prostą klasę OrderService, która wyglądającą następująco:
@Service
public class OrderService {
public Order createOrder(Customer c, List<Product> products) {
return OrderBuilder.anOrder()
.withCustomer(c)
.withOrderDate(new Date())
.withProducts(products)
.withTotalPrice(calculateTotalPrice(products))
.build();
}
private double calculateTotalPrice(List<Product> products) {
return products.stream()
.mapToDouble(product -> product.getPrice() * product.getQuantity())
.sum();
}
}
Używając funkcji „Smell code”, możesz sprawdzić kod pod kątem:
- użycia podatnych na błędy części API Javy, takich jak Date i SimpleDateFormat;
- potencjalnych wyjątków NPE;
- zbyt skomplikowanych metod, które można by podzielić;
- zduplikowanej logiki, którą można by wyodrębnić;
- użycia starszych funkcji języka Javy, np. sugestia stream zamiast pętli;
- potencjalnych problemów ze współbieżnością, jeśli kod jest wielowątkowy;
- właściwego obsługiwania wyjątków.
W kontekście powyższego kodu rezultat to:
1. Bezpośrednie użycie java.util.Date może być problematyczne ze względu na mutowalność i podatność na błędy.
2. Brak sprawdzania parametrów wejściowych c i products pod kątem wartości null.
Może to prowadzić do NullPointerException. Należy zweryfikować wymagane parametry.
3. calculateTotalPrice oblicza całkowitą kwotę przy każdym wywołaniu.
Można przechowywać całkowitą cenę w obiekcie Order podczas tworzenia, aby uniknąć ponownego obliczania.
4. Wzorzec OrderBuilder jest poprawny, ale wiąże się z kosztem tworzenia wielu małych obiektów pośrednich.
Optymalizacja mogłaby być przeprowadzona na podstawie użytkowania.
5. Brak logowania lub obsługi błędów. Wyjątki w operacjach strumieniowych mogą nie zostać zauważone.
6. Brak testów. Dodanie testów jednostkowych pomogłoby wyłapać błędy i zapobiegać regresji.
Gdy Cody został poproszony o zmianę nazw zmiennych, wprowadził następujące zmiany:
- w metodzie createOrder c zamieniono na customer
- w metodzie 'calculateTotalPrice' products zamieniono na orderProducts
Wyjaśnianie wybranego kodu
Bardzo przydatną funkcją wydaje się opcja 'Explain Selected Code’. Podejrzewam jednak, że jej przydatność będzie rosła wraz ze wzrostem skomplikowania metody oraz spadkiem jej czytelności. Dodałem odrobinę komplikacji do metody 'calculateTotalPrice’, aby zobaczyć, jak zostanie ona wyjaśniona:
private double calculateTotalPrice(List<Product> products, String code) {
Discount discount = discountRepository.findByCode(code);
Double totalPrice = products.stream()
.mapToDouble(product -> product.getPrice() * product.getQuantity())
.sum();
return discount == null
? totalPrice
: applyDiscount(totalPrice, discount);
}
private double applyDiscount(Double totalPrice, Discount discount) {
return (100 - discount.getPercentOfDiscount()) * totalPrice / 100;
}
Oto wygenerowane wyjaśnienie tej metody:
1. Metoda calculateTotalPrice przyjmuje List<Product> products i String code jako parametry.
2. Najpierw wywołuje discountRepository.findByCode(code), aby na podstawie podanego kodu znaleźć obiekt Discount. Sprawdza to wszelkie możliwe rabaty dla zamówienia na podstawie kodu.
3. Oblicza łączną cenę bez żadnych rabatów przez:
- Strumieniowanie (przechodzenie) przez listę produktów
- Mapowanie każdego Produktu na jego całkowitą cenę przez mnożenie cena * ilość
- Sumowanie zmapowanego strumienia, aby uzyskać łączną cenę
4. Wynik jest przechowywany w zmiennej Double o nazwie totalPrice. Jest to cena przed zastosowaniem rabatu.
5. Sprawdza, czy discount jest null, co oznacza, że nie znaleziono rabatu.
6. Jeśli jest null, po prostu zwraca totalPrice bez stosowania żadnego rabatu.
7. W przeciwnym razie wywołuje metodę applyDiscount, przekazując totalPrice i obiekt discount.
8. applyDiscount oblicza cenę po rabacie w następujący sposób:
- Bierze 100 - discount.getPercentOfDiscount(), aby uzyskać procent do zachowania po rabacie
- Mnoży ten procent przez totalPrice
- Dzieli przez 100, aby uzyskać ostateczną cenę po rabacie
9. Cena po rabacie jest zwracana z applyDiscount.
Wydaje mi się, że taka analiza może być pomocna w lokalizowaniu błędów, na co programiści często tracą wiele cennego czasu. Sprawdzi się to zwłaszcza wtedy, gdy mamy do czynienia z kodem niskiej jakości, którego nie jesteśmy autorami.
Generowanie testu
Następną wartą wspomnienia funkcją jest możliwość wygenerowania testu. Aby zweryfikować, jak Cody poradzi sobie z tym zadaniem, dla funkcji zawierającą warunek zmodyfikowałem metodę 'createOrder’ w następujący sposób:
public Order createOrder(Customer c, List<Product> products) throws CountryNotSupportedYetException {
if (isCustomerFromPoland(c)) {
throw new CountryNotSupportedYetException();
}
return OrderBuilder.anOrder()
.withCustomer(c)
.withOrderDate(new Date())
.withProducts(products)
.withTotalPrice(calculateTotalPrice(products), c)
.build();
}
Wygenerowana klasa prezentuje się tak:
import org.example.model.Customer;
import org.example.model.Product;
import org.example.service.CountryNotSupportedYetException;
import org.example.service.OrderService;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.util.Arrays;
import java.util.List;
public class OrderServiceTest {
private OrderService orderService = new OrderService(null);
@Test
public void testCreateValidOrder() {
// Arrange
Customer customer = new Customer("John", "123 Main St");
Product product = new Product("Apple", 10, 2);
List<Product> products = Arrays.asList(product);
// Act
orderService.createOrder(customer, products);
// Assert
// No exception thrown
}
@Test
public void testCreateOrderUnsupportedCountry() {
// Arrange
Customer customer = new Customer("John", "123 Main St, Poland");
List<Product> products = Arrays.asList();
// Act and Assert
Assertions.assertThrows(CountryNotSupportedYetException.class, () -> {
orderService.createOrder(customer, products);
});
}
@Test
public void testCalculateTotalPrice() {
// Arrange
Product product1 = new Product("Apple", 10, 2);
Product product2 = new Product("Orange", 5, 3);
List<Product> products = Arrays.asList(product1, product2);
// Act
double totalPrice = orderService.calculateTotalPrice(products);
// Assert
Assertions.assertEquals(35, totalPrice);
}
}
Popełnionych zostało tutaj parę błędów i klasa po skopiowaniu wymaga poprawek oraz modyfikacji. Główne problemy to takie jak choćby fakt, że zostały użyte nieistniejące konstruktory, a także test obejmuje metodę prywatną, która nie może być przetestowana w taki sposób. Moim zdaniem, jeżeli jednak masz napisać testy do jakiejś klasy, to taki wygenerowany szkielet może stanowić całkiem dobry punkt startowy, przyśpieszając znacznie pracę.
Możesz oczywiście również starać się poprowadzić sztuczną inteligencję w oczekiwanym kierunku. Przykład? Jako że zostały użyte asercje JUnit, a chciałbym uzyskać w swoich testach asercje z pakietu AssertJ, mogę o to poprosić, używając języka naturalnego (w języku angielskim).
Tak więc na moją prośbę:
Could you use AssertJ assertions with single and joined assertion?
Dostaję test z użytą asercją z pakietu AssertJ:
// Assert
Assertions.assertThat(order)
.hasFieldOrPropertyWithValue("customer", customer)
.hasFieldOrPropertyWithValue("orderDate", new Date())
.hasFieldOrPropertyWithValue("products", products)
.hasFieldOrPropertyWithValue("totalPrice", 10.5);
Procesowanie zapytań
Jeżeli chodzi o Cody i procesowanie zapytań o kod, to możesz tutaj skorzystać z naprawdę bardzo wielu opcji, np. spróbować znaleźć błąd poprzez rozmowę, spytać o możliwie ulepszenia czy potencjalne zagrożenia.
Jako przykład mogę tutaj podać zapytanie:
Which country is not supported and could lead to CountryNotSupportedYetException?
Odpowiedź Cody to:
Based on the code snippet provided, the country "Poland" is not supported yet and could lead to a CountryNotSupportedYetException being thrown:
java
if (c.getAddress().getCountry().equals("Poland")) {
throw new CountryNotSupportedYetException();
}
The code is checking if the customer's address country is "Poland". If so, it will throw the CountryNotSupportedYetException.
So Poland is the country that is not supported yet that could result in that exception.
Oczywiście w takim prostym projekcie możemy równie dobrze wyszukać kod i samodzielnie go przeanalizować. Co innego jednak, gdy mowa o błędzie w znacznie bardziej złożonym projekcie. W takim przypadku czasami sprawniej można by dojść z Cody do przyczyny potencjalnego problemu, tym samym oszczędzając wiele cennego czasu.
Podsumowanie
Myślę, że udało mi się w tym artykule zaprezentować działanie narzędzi AI przydatnych dla programistów. Osobiście dostrzegam znaczący potencjał w rozwoju takich rozwiązań, jednak ważne jest, aby pamiętać, iż ich głównym celem jest ułatwienie i przyspieszenie procesu pisania kodu, a nie jego przejęcie. Podpowiedzi oferowane przez testowane rozwiązania nie były pozbawione błędów, więc kluczowa jest kontrola nad nimi.
W miarę wzrostu popularności narzędzia te z pewnością będą się doskonalić, a tym samym generować bardziej poprawne rezultaty. Na chwilę obecną uważam, że mają zdolność do odciążenia codziennej pracy z kodem, który jest powtarzalny, a także mogą wspomóc rozwój programistów. W jaki sposób? Głównie poprzez analizę błędów oraz dostarczanie wskazówek, jak można poprawić pewne aspekty, o których ktoś mógł nie wiedzieć bądź po prostu zapomnieć w danej chwili. Wiadomo przecież, że czasami człowiek nie przewiduje wszystkich możliwości wystąpienia problemu na etapie pisania kodu.