Skip to content

Commit 1a9d0e6

Browse files
committed
clean architecture post added
1 parent 92ec7fc commit 1a9d0e6

File tree

4 files changed

+224
-0
lines changed

4 files changed

+224
-0
lines changed
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
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`).
385 KB
Loading
17 KB
Loading
43.2 KB
Loading

0 commit comments

Comments
 (0)