Sytuacja kobiet w IT w 2024 roku
26.04.20195 min
Tapiwa Muzira

Tapiwa MuziraAndroid & Java Developer

Jak pisać kod, który nie boi się zmian

Sprawdź, jak dzięki enkapsulacji danych i scentralizowanemu zarządzaniu zależnością, tworzyć kod przyjazny zmianom.

Jak pisać kod, który nie boi się zmian

Jedną z najbardziej przydatnych i oszczędzających czas umiejętności, jaką może posiadać programista Androida (czy dowolnej innej dziedziny programowania) jest pisanie kodu, który można łatwo dostosować do przyszłych zmian. Ta umiejętność staje się jeszcze bardziej wartościowa, ponieważ pozwala zwiększyć rozmiar i złożoność projektów, nad którymi pracujesz.

W nowoczesnym developmencie oprogramowania prawie zawsze używasz kodu w postaci gotowych bibliotek, które zostały stworzone i są utrzymywane przez kogoś, kto robi to, czego Ty dokładnie potrzebujesz. Na tym właśnie polega piękno open source’u - oszczędza nam dużo czasu i wysiłku. Powoduje to jednak niepewność co do przyszłości naszego projektu, ponieważ z czasem mogą zostać wydane nowe biblioteki, niekompatybilne z poprzednimi wersjami lub mogą pojawić się lepsze alternatywy, które sprawią, że te, z których korzystałeś, będą przestarzałe. Wskutek tego kluczowe znaczenie ma budowanie struktury swojego kodu w sposób, który ułatwi dostosowanie go do przyszłych zmian.

W tym artykule zobaczymy, jak możemy uczynić nasz kod przyjaznym dla zmian poprzez technikę enkapsulacji danych i scentralizowane zarządzanie zależnością. Zobaczmy teraz, jak można to osiągnąć na przykładzie małej aplikacji na Androida. Jest to dość prosty i niewyszukany przykład, ale wystarczający, by wyjaśnić podstawowe idee technik, o których mowa. W tym artykule założono, że jesteś zaznajomiony z używaniem systemu Android RecyclerView i wstrzykiwaniem zależności za pomocą Dagger2 (linki do samouczków na końcu artykułu).

Załóżmy więc, że budujemy aplikację, która po prostu ładuje obrazy (dajmy na to plakaty filmowe) z zasobu sieciowego i wyświetla je na liście RecyclerView. Teraz powiedzmy, że mamy dwie biblioteki ładowania obrazów, które możemy wybrać - Glide lub Picasso.

Przyjrzyjmy się teraz, jak możemy to wdrożyć bez zwinności kodu. Oto jeden ze sposobów, w jaki możemy wdrożyć nasz adapter RecyclerView:

public class MyAdapter extends RecyclerView.Adapter<MyAdapter.MyViewHolder>{
  
  private Arraylist<String> urls;
  
  public MyAdapter(ArrayList<String> urls){
    this.urls = urls;
  }
  
  // Other code omitted for simplicity
  
  @Override
  public void onBindViewHolder(MyViewHolder holder, int position) {
    
    Glide.with(holder.movieImage.getContext())
    .load(urls.get(position))
    .into(holder.movieImage)
  }
}


W powyższym kodzie po prostu „przyspawaliśmy” implementację Glide do naszego adaptera. Beż żadnego problemu, wysiłku lub bólu. Jeśli później zdecydujemy się przejść na Picasso, musimy wejść do naszego adaptera i usunąć wszystkie elementy Glide, zastąpić je nową implementacją, aby nasza metoda onBindView wyglądała następująco:

public class MyAdapter extends RecyclerView.Adapter<MyAdapter.MyViewHolder>{
  
  private Arraylist<String> urls;
  
  public MyAdapter(ArrayList<String> urls){
    this.urls = urls;
  }
  
  // Other code omitted for brevity
  
  @Override
  public void onBindViewHolder(MyViewHolder holder, int position) {
    
   Picasso.get()
      .load(urls.get(position))
      .into(holder.movieImage);
  }
}


Ponownie łatwy i bezbolesny proces. Więc o co całe zamieszanie? Co, jeśli Twoja aplikacja ma wiele activity/fragmentów z RecylerView, które używają różnych implementacji adaptera? Na przykład możemy mieć MovieDetailActivity, która wyświetli szczegóły dotyczące wybranego filmu, a także „mini” RecyclerView na dole, który pokazuje inne filmy podobne do wyświetlanego. Będziesz również musiał zmienić adapter w tym activity, jak również korzystać z Picasso. Teraz zaczynamy robić więcej pracy, aby dokonać zmiany. Zobaczmy więc, jak możemy sprawić, by nasz kod był bardziej zwinny i otwarty na zmiany.

Zacznijmy od stworzenia własnego niestandardowego typu, który wykorzystamy do hermetyzacji kodu powiązanego z Glide i Picasso.:

public interface ImageLoader{
  
  void loadImage(String url);
}


Następnie tworzymy implementację z Glide naszego niestandardowego typu ImageLoader:


public class GlideImageLoader implements ImageLoader{
  
  public GlideImageLoader(){}
  
  @Override
  public void loadImage(String url, ImageView v){
    Glide.with(v.getContext())
    .load(url)
    .into(v);
  }
}


Teraz możemy przygotować nasz adapter RecylerView do wykorzystania naszego programu ładującego obraz:

public class MyAdapter extends RecyclerView.Adapter<MyAdapter.MyViewHolder>{
  
  private Arraylist<String> urls;
  private ImageLoader imageLoader;
  
  public MyAdapter(ArrayList<String> urls, ImageLoader imageLoader){
    this.urls = urls;
    this.imageLoader = imageLoader;
  }
  
  // Other code omitted for simplicity
  
  @Override
  public void onBindViewHolder(MyViewHolder holder, int position) {
    imageLoader.loadImage(urls.get(position), holder.movieImage);
  }
}


To, co skutecznie zrobiliśmy powyżej, to ukrycie (lub enkapsulacja, jeśli chcesz) z naszego adaptera szczegółów ładowania obrazów. Wszystko, co nasz adapter wie teraz, to to, że akceptuje jako jeden z wejść obiekt typu ImageLoader i może wywołać metodę loadImage(), przekazując wymagane argumenty. Ukrywanie informacji przez hermetyzację to potężna koncepcja. Pokażę dlaczego.

Następnie inicjalizujemy nasz adapter RecyclerView w naszym głównym działaniu na Create w następujący sposób:

public class MainActivity extends AppCompatActivity {
  
  @Override
    protected void onCreate(Bundle savedInstanceState) {
      
      // other code omitted for simplicity
      
      ImageLoader mImageLoader = new GlideImageLoader();
      
      MyAdapter mAdapter = new MyAdapter(imageUrlsList, mImageLoader);
      moviesRecyclerView.setAdapter(mAdapter);
      
      //other code omitted for simplicity
    }
}


Teraz, aby zmienić z Glide na Picasso, najpierw tworzymy implementację Picasso ImageLoader w następujący sposób:

public class PicassoImageLoader implements ImageLoader{
  
  public PicassoImageLoader(){}
  
  @Override
  public void loadImage(String url, ImageView v){
    Picasso.get()
      .load(url)
      .into(v);
  }
}


… i ponownie w naszym głównym działaniu onCreate inicjalizujemy nasz adapter:

public class MainActivity extends AppCompatActivity {
  
  @Override
    protected void onCreate(Bundle savedInstanceState) {
      
      // other code omitted for simplicity
      
      ImageLoader mImageLoader = new PicassoImageLoader();
      
      MyAdapter mAdapter = new MyAdapter(imageUrlsList, mImageLoader);
      moviesRecyclerView.setAdapter(mAdapter);
      
      //other code omitted for simplicity
    }
}


Zauważ, że tym razem nie dotknęliśmy naszego adaptera RecyclerView, aby dokonać niezbędnych zmian. Fajnie, co? Nie do końca, ponieważ teraz zamiast tego naruszamy nasz MainActivity i będziemy musieli także naruszyć hipotetyczną aktywność MovieDetailActivity wspomnianej wcześniej, ponieważ ona również musi zostać zmieniona, aby wykorzystać nasz nowy PicassoImageLoader.

Scentralizowane zarządzanie zależnościami

Zobaczmy teraz, jak scentralizowane jest zarządzanie zależnościami za pomocą wtryskiwacza zależności, co pomoże nam uczynić nasz kod jeszcze bardziej przyjaznym zmianom. Będziemy używać frameworka do wstrzykiwania zależności Dagger2. Teraz pominiemy wszystkie inne kroki implementacji Dagger2 i przejdziemy do punktu tego artykułu, który odnosi się do Dagger2.

Tak więc nasz moduł aplikacji Dagger2 i komponent używający Glide, który dostarczy nasz GlideImageLoader, gdy będzie to potrzebne, wygląda tak:

@Module
public class MyAppModule{
  
  @Provides
  ImageLoader provideImageLoader(){
    return new GlideImageLoader();
  }
}


Następnie implementujemy Dagger w naszej MainActivity:

public class MainActivity extends AppCompatActivity {
  
  //field to be injected by Dagger
  @Inject
  ImageLoader mImageLoader;
  
  @Override
    protected void onCreate(Bundle savedInstanceState) {
      
      // other code omitted for simplicity
      
      //Get App component and inject activity
      ((BaseApplication) getApplication()).getComponent().inject(this);
      
      //set up recycler view
      MyAdapter mAdapter = new MyAdapter(imageUrlsList, mImageLoader);
      moviesRecyclerView.setAdapter(mAdapter);
      
      //other code omitted for simplicity
    }
}


I przypuśćmy, że nasza inna aktywność (MovieDetailActiviy) wygląda tak:

public class MovieDetailActivity extends AppCompatActivity {
  
  //field to be injected by Dagger
  @Inject
  ImageLoader mImageLoader;
  
  @Override
    protected void onCreate(Bundle savedInstanceState) {
      
      // other code omitted for simplicity
      
      //Get App component and inject activity
      ((BaseApplication) getApplication()).getComponent().inject(this);
      
      //set up recycler view
      MyAdapter mAdapter = new MyAdapter(imageUrlsList, mImageLoader);
      similarMoviesRecyclerView.setAdapter(mAdapter);
      
      //other code omitted for simplicity
    }
}


Teraz, kiedy zdecydujemy się zmienić nasz projekt na Picasso, musimy zmienić tylko jeden element... tak, zgadliście, musimy zmienić tylko metodę dostawcy ImageLoader w naszym module zależności (MyAppModule), aby przywrócić implementację Picasso :)

@Module
public class MyAppModule{
  
  @Provides
  ImageLoader provideImageLoader(){
    return new PicassoImageLoader();
  }
}


Dagger sam zlokalizuje wszystkie zależności ImageLoadera w naszym projekcie (W tym przypadku adnotowane przez @Inject pola ImageLoadera w MainActivity i MovieDetailActivity) i wstrzyknie dostarczony ImageLoader. Żadne Activity ani adapter nie zostały nawet tknięte. Sukces gwarantowany!

<p>Loading...</p>