|
| 1 | +--- |
| 2 | +layout: post |
| 3 | +title: "Clean Architecture" |
| 4 | +date: 2019-09-02 |
| 5 | +categories: ["Wzorce architektoniczne"] |
| 6 | +permalink: /blog/wzorce/:title/ |
| 7 | +image: architecture/clean_architecture |
| 8 | +github: architectural-patterns/tree/master/clean_architecture |
| 9 | +description: "Wzorce projektowe / architektoniczny" |
| 10 | +keywords: "clean, architecture, wzorzec, pattern, architektura, warstwy, zaleznosci, layers, dependencies, presentation, domain, data, framework, use cases, repositories, sources, android, kotlin, programowanie, programming" |
| 11 | +--- |
| 12 | + |
| 13 | +## Zasada zależności |
| 14 | +`Clean architecture` nie jest sam w sobie wzorcem architektonicznym lecz propozycją na organizację architektury aplikacji w taki sposób, aby różne obszary oprogramowania były łatwo wymienne, a projekt mógł zostać w całości zaadaptowany w innym środowisku uruchomieniowym bez względu na wykorzystywany framework (np. spójny kod dla aplikacji mobilnej i internetowej). Kod dzielony jest na `warstwy` przypominające koncentryczne `okręgi` zgodnie z zasadą, że `warstwy wewnętrzne` nie wiedzą nic o `warstwach zewnętrznych`, a co za tym idzie nie posiadają do nich `zależności`. Innymi słowy zależności kodu źródłowego mogą wskazywać tylko do wewnątrz. Dotyczy to każdej jednostki kodu tzn. klasy, funkcji, zmiennej itd. Okręgi reprezentują różne obszary oprogramowania, gdzie zewnętrzne koła są mechanizmami niskiego poziomu, a wewnętrzne zasadami wysokiego poziomu. Im głębsza warstwa tym większy poziom abstrakcji. Przekraczanie granic bez naruszania zasady zależności wartwy możliwe jest za pomocą `inwersji zależności` (`dependency inversion`) realizowanej przy użyciu różnych technik programistycznych (np. `polimorfizm`). |
| 15 | + |
| 16 | +## Warstwy |
| 17 | +W klasycznym podejściu można wyróżnić cztery warstwy: `Entities`, `Use Cases`, `Interface Adapters`, `Framework and Drivers`. Jednakże nie jest to ogólna i jedyna słuszna propozycja. Podział oprogramowania na warstwy zależy od środowiska, wielkości i złożoności aplikacji, postawionych wymagań oraz kalkulacji kosztów i zysków. Równie dobrze może się okazać, że optymalna implementacja realizowana jest w oparciu inną liczbę okręgów. `Warstwa Entities` to reguły biznesowe wspólne dla wszystkich aplikacji w projekcie. Mogą to być obiekty z metodami czy też zbiory struktur danych i funkcji. `Warstwa Use Cases` zawiera reguły biznesowe specyficzne dla danej aplikacji. Implementuje przypadki użycia systemu, które sterują przepływem danych między podmiotami. `Warstwa Interface Adapters` jest zestawem adapterów odpowiedzialnych za konwersje danych z warstw `Use Cases` i `Entities` do zewnętrznych agentów. To tutaj przeważnie znajdują się klasy implementujące architekturę aplikacji (np. `View`, `Presenter`, `Controller`). `Warstwa Framework and Drivers` składa się z różnych zewnętrznych bibliotek, sterowników i narzędzi. W tym miejscu pojawiają się wszystkie szczegóły zewnętrznych wywołań, które są przekazywane do kolejnych wewnętrznych okręgów. |
| 18 | + |
| 19 | +//TODO diagram |
| 20 | + |
| 21 | +## Zastosowanie |
| 22 | +Zastosowanie dowolnej architektury systemu pomimo różnic posiada jeden wspólny cel, podział odpowiedzialności i zagrożeń poprzez rozdzielenie oprogramowania na warstwy. `Clean Architecture` wpisuje się w ten trend i podobnie jak inne architektury jego użycie dostarcza wielu korzyści. Pozwala na tworzenie systemów niezależnych od framework dzięki czemu biblioteki moga być wymienne i traktowane jako narzędzia. Ułatwia testowanie reguł biznesowych ze względu na ich separacje od interfejsu użytkownika i źródeł danych. Ponadto umożliwia zmianę interfejsu użytkownika oraz źródeł danych bez dokonywania modyfikacji w pozostałych obszarach kodu. `Clean Architecture` nie jest związany z żadną konkretną architekturą w związku z czym może zostać zaadaptowany do większości wzorców architektonicznych takich jak `MVC`, `MVP`, `MVMM`, `MVI`. |
| 23 | + |
| 24 | +## Android |
| 25 | +Jedną z najpopularniejszych praktyk realizacji `Clean Architecture` dla `Android` jest wyróżnienie trzech warstw: `Presentation`, `Domain`, `Data`. Warstwa `Presentation` zawiera interfejs użytkownika oraz jego obsługę, `Domain` posiada klasy danych i definicję przypadków użycia natomiast `Data` składa się z repozytoriów i zewnętrznych źródeł danych. Odwołując się do klasycznej definicji `Clean Architecture` możliwe jest zastosowanie jeszcze szerszego podziału poprzez wyłączenie m.in. przypadków użycia z `Domain` do osobnej warstwy `Use Cases` oraz właściwych źródeł danych do warstwy `Framework`. |
| 26 | + |
| 27 | +//TODO diagram |
| 28 | + |
| 29 | +## Przykład |
| 30 | +Aplikacja Filmhead umożliwia zarządzanie prywatną listą filmów użytkownika. Filmy mogą zostać dodane do listy obserwowanych, natomiast te obejrzane mogą być ocenione. Projekt posiada dedykowane aplikacje w wersji `przeglądarkowej` oraz na urządzenia z systemem `Android`. Z uwagi na zachowanie spójności działania aplikacji na wszystkich platformach oraz współdzielenie części kodu zdecydowano się na zastosowanie `Clean Architecture` w wariancie pięciu warstw: `Presentation`, `Use Cases`, `Domain`, `Data`, `Framework`. W przypadku aplikacji mobilnej należy podjąć także decyzję o wyborze wzorca architektonicznego (np. `MVP`). |
| 31 | + |
| 32 | +{% highlight kotlin %} |
| 33 | +//DOMAIN |
| 34 | +data class Film (val title: String, val vote: Int?) |
| 35 | +{% endhighlight %} |
| 36 | + |
| 37 | +W warstwie `Domain` definiowane są modele danych wykorzystywane przez kolejne warstwy. Klasyczny `Clean Architecture` proponuje posiadanie jednej reprezentacji modelu danego bytu dla każdej warstwy w celu całkowitej niezależności warstw od modeli wewnętrznych. Jednakże w wielu przypadkach może się to wiązać z przerostem formy nad treścią. |
| 38 | + |
| 39 | +{% highlight kotlin %} |
| 40 | +//DATA |
| 41 | +class FilmsRepository (private val localSource: FilmLocalSource, private val remoteSource: FilmRemoteSource) { |
| 42 | + |
| 43 | + fun getFilms() : List<Film> { |
| 44 | + val remotes = remoteSource.downloadFilms() |
| 45 | + if(remotes.isNotEmpty()) { |
| 46 | + localSource.merge(remotes) |
| 47 | + return remotes |
| 48 | + } |
| 49 | + else { |
| 50 | + return localSource.getLocalFilms() |
| 51 | + } |
| 52 | + } |
| 53 | + |
| 54 | + fun putFilm(film: Film) { |
| 55 | + localSource.saveFilm(film) |
| 56 | + remoteSource.uploadFilm(film) |
| 57 | + } |
| 58 | +} |
| 59 | + |
| 60 | +interface FilmLocalSource { |
| 61 | + |
| 62 | + fun getLocalFilms() : List<Film> |
| 63 | + fun saveFilm(film: Film) |
| 64 | + fun merge(films: List<Film>) |
| 65 | +} |
| 66 | + |
| 67 | +interface FilmRemoteSource { |
| 68 | + |
| 69 | + fun downloadFilms() : List<Film> |
| 70 | + fun uploadFilm(film: Film) |
| 71 | +} |
| 72 | +{% endhighlight %} |
| 73 | + |
| 74 | +Warstwa `Data` odpowiedzialna jest za definicję repozytoriów (`Repositories`) realizujących logikę biznesową żądań poprzez wywołanie odpowiednich operacji na deklarowanych źródłach danych (`Sources`). |
| 75 | + |
| 76 | +{% highlight kotlin %} |
| 77 | +//USE CASES |
| 78 | +class GetFilms (private val filmsRepository: FilmsRepository) { |
| 79 | + |
| 80 | + operator fun invoke(): List<Film> = filmsRepository.getFilms() |
| 81 | +} |
| 82 | + |
| 83 | +class AddFilm (private val filmsRepository: FilmsRepository) { |
| 84 | + |
| 85 | + operator fun invoke(film: Film) = filmsRepository.putFilm(film) |
| 86 | +} |
| 87 | +{% endhighlight %} |
| 88 | + |
| 89 | +Warstwa `Use Cases` konwertuje akcje i zdarzenia użytkownika oraz systemu do żądań delegowanych do kolejnych wewnętrznych warstw. |
| 90 | + |
| 91 | +{% highlight kotlin %} |
| 92 | +//FRAMEWORK |
| 93 | +class RoomFilmsSource : FilmLocalSource { |
| 94 | + |
| 95 | + //mock implementation of Room database |
| 96 | + private var items = mutableListOf<Film>() |
| 97 | + |
| 98 | + override fun getLocalFilms(): List<Film> { |
| 99 | + return items |
| 100 | + } |
| 101 | + |
| 102 | + override fun saveFilm(film: Film) { |
| 103 | + items.add(film) |
| 104 | + } |
| 105 | + |
| 106 | + override fun merge(films: List<Film>) { |
| 107 | + for(film in films) { |
| 108 | + if(!items.contains(film)) |
| 109 | + items.add(film) |
| 110 | + } |
| 111 | + } |
| 112 | +} |
| 113 | + |
| 114 | +class RetrofitFilmsSource : FilmRemoteSource { |
| 115 | + |
| 116 | + //mock implementations of Retrofit framework |
| 117 | + private val items = mutableListOf<Film>() |
| 118 | + |
| 119 | + override fun downloadFilms(): List<Film> { |
| 120 | + return items |
| 121 | + } |
| 122 | + |
| 123 | + override fun uploadFilm(film: Film) { |
| 124 | + items.add(film) |
| 125 | + } |
| 126 | +} |
| 127 | +{% endhighlight %} |
| 128 | + |
| 129 | +Warstwa `Framework` wykorzystuje specyficzne zewnętrzne zależności (np. biblioteka systemowa, wybrany framework) implementując szczegóły realizacji przypadków użycia. |
| 130 | + |
| 131 | +{% highlight kotlin %} |
| 132 | +//PRESENTATION |
| 133 | +data class Film (val title: String, val status: String) |
| 134 | + |
| 135 | +//for this layer use own Film model, convert Film model from domain |
| 136 | +//use Kotlin import as feature |
| 137 | +fun DomainFilm.toPresentation(): Film { |
| 138 | + if(vote == null || vote == 0) return Film(title, "To watch!") |
| 139 | + else return Film(title, "$vote/10") |
| 140 | +} |
| 141 | + |
| 142 | +class FilmsPresenter (private val view: FilmsView, private val getFilms: GetFilms, private val addFilm: AddFilm) { |
| 143 | + |
| 144 | + fun init() { |
| 145 | + view.showProgress(true) |
| 146 | + val films = getFilms.invoke() |
| 147 | + view.renderFilms(films.map (DomainFilm::toPresentation) ) |
| 148 | + view.showProgress(false) |
| 149 | + } |
| 150 | + |
| 151 | + fun uninit() { |
| 152 | + |
| 153 | + } |
| 154 | + |
| 155 | + fun addFilmClicked(title: String, vote: Int?) { |
| 156 | + view.showProgress(true) |
| 157 | + val film = Film(title, vote) |
| 158 | + addFilm.invoke(film) |
| 159 | + view.renderNewFilm(film.toPresentation()) |
| 160 | + view.showProgress(false) |
| 161 | + } |
| 162 | +} |
| 163 | + |
| 164 | +interface FilmsView { |
| 165 | + |
| 166 | + fun showProgress(enable: Boolean) |
| 167 | + fun renderFilms(films: List<Film>) |
| 168 | + fun renderNewFilm(film: Film) |
| 169 | +} |
| 170 | + |
| 171 | +class MainActivity : AppCompatActivity(), FilmsView { |
| 172 | + |
| 173 | + private val filmsAdapter = FilmsAdapter() |
| 174 | + private val presenter: FilmsPresenter |
| 175 | + |
| 176 | + init { |
| 177 | + //mostly use dependency injection instead of manual creating |
| 178 | + val room = RoomFilmsSource() |
| 179 | + val retrofit = RetrofitFilmsSource() |
| 180 | + val repository = FilmsRepository(room, retrofit) |
| 181 | + |
| 182 | + presenter = FilmsPresenter(this, GetFilms(repository), AddFilm(repository)) |
| 183 | + } |
| 184 | + |
| 185 | + override fun onCreate(savedInstanceState: Bundle?) { |
| 186 | + super.onCreate(savedInstanceState) |
| 187 | + setContentView(R.layout.activity_main) |
| 188 | + |
| 189 | + recyclerView.apply { |
| 190 | + adapter = filmsAdapter |
| 191 | + layoutManager = LinearLayoutManager(this@MainActivity) |
| 192 | + } |
| 193 | + button.setOnClickListener { |
| 194 | + //this is mock implementation |
| 195 | + //show some add dialog or activity/fragment instead of |
| 196 | + presenter.addFilmClicked("Film", (0..10).random()) |
| 197 | + } |
| 198 | + |
| 199 | + presenter.init() |
| 200 | + } |
| 201 | + |
| 202 | + override fun onDestroy() { |
| 203 | + presenter.uninit() |
| 204 | + super.onDestroy() |
| 205 | + } |
| 206 | + |
| 207 | + override fun renderFilms(films: List<Film>) { |
| 208 | + filmsAdapter.items.clear() |
| 209 | + filmsAdapter.items.addAll(films) |
| 210 | + filmsAdapter.notifyDataSetChanged() |
| 211 | + } |
| 212 | + |
| 213 | + override fun renderNewFilm(film: Film) { |
| 214 | + filmsAdapter.items.add(film) |
| 215 | + filmsAdapter.notifyDataSetChanged() |
| 216 | + } |
| 217 | + |
| 218 | + override fun showProgress(enable: Boolean) { |
| 219 | + //show or hide some progress |
| 220 | + } |
| 221 | +} |
| 222 | +{% endhighlight %} |
| 223 | + |
| 224 | +Warstwa `Presentation` odpowiedzialna jest za prezentację i obsługę interfejsu graficznego użytkownika. W tym miejscu poza widokiem definiowane są klasy architektury (np. `MVP`). |
0 commit comments