Upgrade to Pro — share decks privately, control downloads, hide ads and more …

코드 품질 1% 올리기

코드 품질 1% 올리기

"[안드로이드 컨퍼런스] 코로나시대의 안드로이드 개발자들"에서 발표한 "코드 품질 1% 올리기" 발표자료입니다

354271902cd8ba2762d05b251dfa0f84?s=128

pluulove (노현석)

May 14, 2022
Tweet

More Decks by pluulove (노현석)

Other Decks in Programming

Transcript

  1. Pluu kakaobank GDE for Android @pluulove must know to write

    better ௏٘ ಿ૕ 1% ৢܻӝ
  2. 2 TARGET ௏٬ җઁ / ഈস

  3. 3

  4. KEY-POINT LOOK BACK

  5. G O O D C O D E ? !

    5 Architecture
  6. DESIGN 6 Good Design No Design दр ӝמ

  7. 7 ୊਺ ௏٘ܳ ࢸݺೡ ٸ যڌѱ द੘ೞաਃ? Language? Structure? DI?

  8. Over 50% Clean Architecture ૕ޙ਷ ҟߧਤ۽ ഛ੢

  9. Clean Architecture ৖ ࢎۈীѱ ࢸݺ ೧ࠁࣁਃ. नੑীѱ ࢸݺب ೧ࠁࣁਃ. ೨ब਷

    ޖ঺ੌөਃ?
  10. Clean Architecture By separating the so ft ware into layers,

    and conforming to The Dependency Rule, you will create a system that is intrinsically testable, with all the bene fi ts that implies. ࣗ೐౟ਝযܳ ҅கਵ۽ ܻ࠙ೞҊ ઙࣘࢿ ӏ஗ਸ ળࣻೣਵ۽ॄ ࠄ૕੸ਵ۽ పझ౟ оמೠ दझమਸ ٜ݅ ࣻ ੓׮ h tt ps://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html ޖ঺੉ ೨बੋо?
  11. Clean Architecture SRP OCP LSP ISP DIP OOP ҙबࢎ ܻ࠙

    Data Structure Architecture … Design Pa tt ern Refactoring … ӝ߈ ૑ध ݽ؛੄ ੉೧ ݶ੽ ޙઁ ߧਤ Domain DDD ୭੸ച
  12. h tt ps://khalilstemmler.com/a rt icles/so ft ware-design-architecture/full-stack-so ft ware-design/ Software

    Design & Architecture Stack
  13. 13 Any? MVC MVVM MVP What’s best?

  14. ੗न݅੄ ӝળ੉ ೙ਃ • ؊ ݆਷ ӝמ? • উ੹? •

    ਬݺ? • ೧ࠁҊ रযࢲ? • ഑द, ࠺ૉפझ সޖࠁ׮ ਋ࢶदೞח ੉ਬח? 14 ޖ঺੉ ૓૞ ੉ਬੋоਃ?
  15. Good Code? • ੍ӝ ए਍ ௏٘ • ઺ࠂ੉ হח ௏٘

    • పझ౟о ए਍ ௏٘ • ӝఋ 15 ޖ঺ਸ ೨ब о஖۽ ل Ѫੋо?
  16. Good Code with Refactoring ೙ࣻ ઑѤ < 16 ௏٘ ௬ܻ౭

    ௿ܽ ௏٘ ೐۽ಕ࣊օ્ܻ ইఃఫ୛ ܻಂష݂ ٣੗ੋ ಁఢ ҃ઁ੸ ࠺ਊ
  17. U S E I D I O M S 17

  18. Are you need mutable? 18 fun test(list: MutableList<Int>): MutableList<String> {

    val transformList = mutableListOf<String>() // TODO return transformList } Parameter/Return੄ ߸ചо হח ҃਋ۄݶ?
  19. Are you need mutable? 19 fun test(list: List<Int>): MutableList<String> {

    val transformList = mutableListOf<String>() // TODO return transformList } Parameter/Return੄ ߸ചо হח ҃਋ۄݶ?
  20. Are you need mutable? 20 fun test(list: List<Int>): List<String> {

    val transformList = mutableListOf<String>() // TODO return transformList } Parameter/Return੄ ߸ചо হח ҃਋ۄݶ?
  21. List transform API 21 fun test(list: List<Int>): List<String> { val

    transformList = mutableListOf<String>() for (item in list) { transformList.add(item.toString()) } return transformList } fun test(list: List<Int>): List<String> { return buildList { for (item in list) { add(item.toString()) } } } 3 fun test(list: List<Int>): List<String> { return list.map { item - > item.toString() } } 2 fun test(list: List<Int>): List<String> { val transformList = list.map { item -> item.toString() } return transformList } 1
  22. • Code smell • Application-level • Class-level • Method-level •

    Link : h tt ps://bit.ly/3vCk6Fy S T A R T L I N E 22
  23. 23 ӝמ ୶о ܻಂష݂ য়ט੄ ؀࢚

  24. 24 SAMPLE ےؒ ࢤࢿӝ • ےؒਵ۽ ࢤࢿػ ఫझ౟/࢚࢝ਸ ಴दೞח ܻझ౟

    • ߓ҃࢝ী ٮܲ ఫझ౟ ஸ۞ • ೦ݾ ࢶఖद, ೧׼ ೦ݾী ⭑ ಴द • CLEAR റীب ӝর • CLEAR ࢶఖद ֎౟ਕ௼ ۽Ӓ ੹࣠
  25. 25 REFACTORING + over engineering খਵ۽ ࣗѐೞח Ѫب ੿׹਷ ইתפ׮.

  26. 26 Step 1. Always run task

  27. [2 time run Task] Executing tasks: [:app:assembleDebug] in project …

    > Task :app:preBuild UP-TO-DATE ... > Task :app:generateDebugBuildConfig ... > Task :app:writeDebugSigningConfigVersions UP-TO-DATE > Task :app:kaptGenerateStubsDebugKotlin > Task :app:kaptDebugKotlin > Task :app:compileDebugKotlin > Task :app:compileDebugJavaWithJavac > Task :app:mergeDebugJavaResource UP-TO-DATE > Task :app:dexBuilderDebug > Task :app:mergeProjectDexDebug > Task :app:packageDebug > Task :app:createDebugApkListingFileRedirect UP-TO-DATE > Task :app:assembleDebug BUILD SUCCESSFUL in 1s 37 actionable tasks: 8 executed, 29 up-to-date 27 android { ... defaultConfig { ... buildConfigField "String", "SAMPLE", '\"' + generateValue() + '"' } } static def generateValue() { return System.currentTimeMillis() } Step 1. Always run task
  28. 28 android { ... defaultConfig { ... // FIXED 1.

    Always run Gradle task buildConfigField "String", "SAMPLE", '\"123"' } } FIXED 1. Always run task
  29. Step 2. Unnecessary, gradle options 1 2 3 4 1.

    min 21ীࢲ ࠛ೙ਃ 2. ೙ਃೠ ২࣌݅ ഝࢿച 3. desugarо ೙ਃೠ ҃਋ী ୶о 4. Optimize ࠽٘ ২࣌ ୶о
  30. Step 3. move network in View 30 class MainActivity :

    AppCompatActivity() { private val logRepository by lazy { provideRepository() } private fun reset() { // Use OkHttp, Retrofit lifecycleScope.launch { logRepository.sendLog() } ... } }
  31. FIXED 3. move network in View 31 class MainActivity :

    AppCompatActivity() { / / FIXED 3. use ViewModel class SearchViewModel : ViewModel() { private val logRepository by lazy { provideRepository() } // FIXED 3. move network fun sendLog() { // Use OkHttp, Retrofit viewModelScope.launch { logRepository.sendLog() } } } ӝࠄ੸ੋ ೧Ѿߨ
  32. Step 4. Generate in View 32 class MainActivity : AppCompatActivity()

    { ... private fun generate() { val item = SampleItem( text = ('a' + (0 until 26).random()).toString(), bgColor = Color.rgb( (0 .. 255).random(), (0 .. 255).random(), (0 .. 255).random() ) ) sampleAdapter.addItem(item) } ... }
  33. FIXED 4. Generate in View 33 class MainActivity : AppCompatActivity()

    { class SearchViewModel : ViewModel() { .. . // FIXED 4. move generate fun generate() { val item = SampleItem( text = / ** generate text */ , bgColor = / ** generate color */ ) ... } } ӝࠄ੸ੋ ೧Ѿߨ
  34. Step 5. Add unit test 34 class SearchViewModelTest { private

    lateinit var viewModel: SearchViewModel @Before fun setup() { viewModel = SearchViewModel() } @Test fun generate() { // when 1 viewModel.generate() // then 1 assertTrue(viewModel.items.getOrAwaitValue().isNotEmpty()) // when 2 viewModel.reset() // then 2 assertTrue(viewModel.items.getOrAwaitValue().isEmpty()) } } • Unit Test۽ ViewModel పझ౟ • ࡅܲ ۽૒ ୓௼
  35. Step 6. Import android framework 35 import android.graphics.Color class SearchViewModel

    : ViewModel() { ... }
  36. interface SampleItemGenerator { fun generateAlphabet(): String fun generateColor(): Int //

    Optional fun generate(): SampleItem } import android.graphics.Color class SearchViewModel( // FIXED 6. provide generator private val generator: SampleItemGenerator ) : ViewModel() { .. . } FIXED 6. Import android framework 36 import android.graphics.Color
  37. Step 7. Favorite in View 37 class MainActivity : AppCompatActivity()

    { private val preferences: SharedPreferences by lazy { getSharedPreferences("sample", Context.MODE_PRIVATE) } private fun setUpViews() { sampleAdapter = SampleAdapter( onFavorite = { preferences.edit { putString("KEY", it) } sampleAdapter.updateFavorite(it) sampleAdapter.notifyDataSetChanged() } ) sampleAdapter.updateFavorite( preferences.getString("KEY", null).orEmpty() ) } } View੄ ҙबࢎ৬ ׮ࣗ Ѣܻо ݢ Preference
  38. FIXED 7. Favorite in View 38 data class SampleItem( val

    text: String, val bgColor: Int, // FIXED 7. add field favorite val isFavorite: Boolean = false ) class SearchViewModel( ... // FIXED 7. provide saver private val savingRepository: SavingRepository ) : ViewModel() { ... fun generate() { val item = generator.generate() // FIXED 7. use favorite val favoriteText = savingRepository.getFavorite() cachedList.add( item.copy(isFavorite = item.text == favoriteText) ) } // FIXED 7. update favorite fun updateFavorite(text: String) { savingRepository.saveFavorite(text) ... interface SavingRepository { fun saveFavorite(text: String) fun getFavorite(): String } class SavingRepositoryImpl( private val preferences: SharedPreferences ) : SavingRepository { override fun saveFavorite(text: String) { /** save text */ } override fun getFavorite(): String { return /** get text */ } }
  39. class SampleAdapter( private val onFavorite: (String) -> Unit ) :

    RecyclerView.Adapter<SampleViewHolder>() { override fun onCreateViewHolder( parent: ViewGroup, viewType: Int ): SampleViewHolder { return SampleViewHolder.create(parent, onFavorite) } ... } Step 8. User events in RecyclerViews 39 class SampleViewHolder( private val binding: ItemSampleBinding, private val onFavorite: (String) -> Unit ) : RecyclerView.ViewHolder(binding.root) { init { binding.btnButton.setOnClickListener { // TODO onFavorite( /** Text */ ) } } ... } Optional Adapter੄ ࠂ੟بী ݏ୾ࢲ ૐо
  40. FIXED 8. User events in RecyclerViews 40 Optional data class

    SampleItem( val text: String, val bgColor: Int, val isFavorite: Boolean = false, // FIXED 8. add field action val onFavorite: (String) -> Unit ) class SampleViewHolder( private val binding: ItemSampleBinding ) : RecyclerView.ViewHolder(binding.root) { private var item: SampleItem? = null init { binding.btnButton.setOnClickListener { item ?. let { safeItem -> // FIXED 8. use action safeItem.onFavorite(safeItem.text) } } class SearchViewModel( . .. ) { fun generate() { val item = SampleItem( text = . .. , bgColor = .. . , isFavorite = …, // FIXED 8. pending favorite function onFavorite = :: updateFavorite ) ... } / / FIXED 8. change visibility modifier private fun updateFavorite(text: String) { ... } } UI events ~ Handle user events h tt ps://developer.android.com/topic/architecture/ui-layer/ events#recyclerview-events
  41. Step 9. Item manage for repository 41 class SearchViewModel( .

    .. ) : ViewModel() { private val cachedList = mutableListOf<SampleItem>() fun generate() { val item = /** Generate item * / if (cachedList.none { it.text == item.text }) { val favoriteText = savingRepository.getFavorite() cachedList.add( /* * Mapping item * / ) } else { ... } } fun reset() { cachedList.clear() ... } } Optional ؘ੉ఠ ࠁҙਸ ViewModel ؀न ׮ܲҔী ݐӝҊ र׮ݶ? ୶റ, ؘ੉ఠ ੷੢ਸ ࢲߡ۽ ੉ҙೠ׮ݶ?
  42. class SearchViewModel( private val itemRepository: ItemRepository, ... ) : ViewModel()

    { private val cachedList = mutableListOf<SampleItem>() fun generate() { // FIXED 9. use item repository itemRepository.generate() .onSuccess { refresh() } } fun reset() { itemRepository.reset() ... } } FIXED 9. Item manage for repository 42 Optional - interface ItemRepository { val data: List<GenerateItem> fun generate(): Result<GenerateItem> fun reset() } // FIXED 9. use item repository class ItemRepositoryImpl : ItemRepository { private val cachedList = mutableListOf() override val data: List<GenerateItem> get() = cachedList override fun generate(): Result<GenerateItem> { val item: GenerateItem = /** Generate */ } override fun reset() { ... } }
  43. Step 10. Add ViewModelProvider Factory 43 private val viewModel by

    lazy { SearchViewModel( itemRepository = /** Generate */ , savingRepository = /** Generate */ ) }
  44. FIXED 10. Add ViewModelProvider Factory 44 private val viewModel by

    viewModels { / / FIXED 10. user ViewModelProvider Factory SearchViewModelFactory(this) } recommend : DI (Dagger, Hilt, Koin etc) private val viewModel by lazy { private val context: Context ) : ViewModelProvider.Factory { override fun <T : ViewModel> create(modelClass: Class<T>): T { return SearchViewModel( ... ) as T } }
  45. Step 11. Use Inline classes 45 Optional पઁ ֍ਸ ч

    : ARGB or RGB Int੄ Range : Int.MIN_VALUE ~ Int.MAX_VALUE h tt ps://developer.android.com/reference/android/graphics/Color data class SampleItem( val text: String, val bgColor: Int, val isFavorite: Boolean = false, val onFavorite: (String) -> Unit ) { fun isDarkBg(): Boolean { return /** true or false */ } }
  46. data class SampleItem( val text: String, val bgColor: ColorValue, val

    isFavorite: Boolean = false, val onFavorite: (String) -> Unit ) FIXED 11. Use Inline classes 46 Optional data class SampleItem( tt ps://kotlinlang.org/docs/inline-classes.html @JvmInline value class ColorValue(@ColorInt val value: Int) { fun isDark(): Boolean = /* * true or false */ }
  47. class ItemRepositoryImpl { ... ) : ItemRepository { private val

    cachedList = mutableListOf<GenerateItem>() override val data: List<GenerateItem> get() = cachedList override fun generate(): Result<GenerateItem> { val item: GenerateItem = /** Generate * / return if (cachedList.none { it.text == item.text }) { cachedList.add(item) Result.success(item) } else { Result.failure( / ** Error * / ) } } ... } Step 12. Migration wrapper data 47 Optional LOOPܳ جݶࢲ ୓௼ ୭ঈ੄ दաܻয়ח ݃૑݄ Elementө૑ ୓௼
  48. FIXED 12. Migration wrapper data 48 Optional class ItemRepositoryImpl {

    . .. ) : ItemRepository { / / FIXED 13. modify container type private val cachedSet = TreeSet<GenerateItem>( TreeSet { p0, p1 -> p0.text.compareTo(p1.text) } ) override val data: List<GenerateItem> get() = cachedSet.toList() override fun generate(): Result<GenerateItem> { val item: GenerateItem = /** Generate */ return if (cachedSet.add(item)) { ... } else { ... } } . .. }
  49. Step 13. Use Coroutine Flow 49 class SearchViewModel( ... )

    : ViewModel() { ... fun generate() { itemRepository.generate() .onSuccess { refresh() } } fun reset() { itemRepository.reset() refresh() } private fun updateFavorite(text: String) { savingRepository.saveFavorite(text) refresh() } private fun refresh() { // refresh live event } } Optional ؘ੉ఠ ୶о/јनਸ Steram ؀਽
  50. FIXED 13. Use stream 50 class SearchViewModel( . .. )

    : ViewModel() { val items: LiveData<List<SampleItem >> = savingRepository.favoriteTextFlow.combine( itemRepository.dataFlow ) { savingText, list -> // merge items } . .. } Optional ৘ઁ۽ח Coroutine Flow ࢎਊ ֢୹ೡ ఫझ౟ ߂ ૌѹ଺ӝ ױযо ߸҃غח ҃਋, Streamਸ ా೧ࢲ ୊ܻ - LiveData, Rx, Coroutine Flow
  51. 51 app/src/main/java/com/pluu/sample/codeforreadability ├── SampleApp.kt ├── data │ ├── ItemRepository.kt │

    ├── LogResult.kt │ ├── SampleApi.kt │ ├── SampleRepository.kt │ └── SavingRepository.kt ├── model │ ├── ColorValue.kt │ ├── GenerateItem.kt │ ├── SampleItem.kt │ └── TriggerTreeSet.kt ├── presentation │ ├── MainActivity.kt │ ├── SampleAdapter.kt │ ├── SampleItemDecoration.kt │ ├── SampleViewHolder.kt │ ├── SearchViewModel.kt │ └── SearchViewModelFactory.kt ├── provider │ ├── RandomGenerator.kt │ └── RetrofitProvider.kt └── utils ├── DimensExtension.kt └── keyFlow.kt app/src/main/java/com/pluu/sample/codeforreadability ├── SampleApp.kt ├── data │ ├── LogResult.kt │ ├── SampleApi.kt │ └── SampleRepository.kt ├── model │ └── SampleItem.kt ├── presentation │ ├── MainActivity.kt │ ├── SampleAdapter.kt │ ├── SampleItemDecoration.kt │ └── SampleViewHolder.kt ├── provider │ └── RetrofitProvider.kt └── utils └── DimensExtension.kt Diff 655 additions and 116 deletions Before A ft er
  52. ࣻҊೞ࣑णפ׮ ৈӝө૑о ӝࠄ ੼ࣻ җ੿ 52

  53. Exclude git history Code Forma tt ing Lint No Internet

    Con fi guration changes Contrived complexity Side e ff ects Large class Too many parameters Long method Excessively long line of code F I N A L C H E C K 53 җઁ ઁ୹ ੹
  54. Try 54 ܻಂష݂ ѼҊೣ оةࢿ SOLID ࠽٘ ѐࢶ 1%

  55. Thank you. pluulove@gmail.com http:/ /pluu.github.io pluulove pluulove