Irbisa · Cheatsheet

OOP, SOLID & Design Patterns

OOP and SOLID in Kotlin, DRY, KISS and YAGNI, coupling and cohesion, dependency injection, GoF patterns and where Android and its libraries use them, anti-patterns

Middle Developer · 19 questions

  1. 01

    What do DRY, KISS and YAGNI mean, and when does applying DRY make code worse?

    DRY says each piece of knowledge has one home, KISS says pick the simplest design that works, and YAGNI says do not build for requirements you do not have yet.

    // DRY: one rule, one home; the UI hint is derived from it, not copiedobject PasswordRule {    const val MIN_LENGTH = 8    fun isValid(password: String) = password.length >= MIN_LENGTH    val hint = "At least $MIN_LENGTH characters"}…
  2. 02

    What are coupling and cohesion, and why do you aim for low coupling and high cohesion?

    Coupling is how much one class depends on how another works; cohesion is how closely the parts of one class belong together.

  3. 03

    What is the Observer pattern, and which forms of it do you use on Android?

    Observer lets an object (the subject) notify any number of dependents when it changes, without knowing who they are.

    fun interface CartListener {    fun onCartChanged(count: Int)} class Cart {    private val listeners = mutableListOf<CartListener>()…
  4. 04

    What is the Strategy pattern, and how do you write one in Kotlin?

    Strategy puts each variant of an algorithm behind one interface, so the caller picks or swaps it at runtime instead of branching.

    // a stateless one-method strategy: a function type is enoughtypealias Discount = (subtotalCents: Long) -> Long val noDiscount: Discount = { it }val blackFriday: Discount = { it * 70 / 100 }fun coupon(offCents: Long): Discount = { it - minOf(offCents, it / 5) }   // at most 20% off…
  5. 05

    What does this code print, and which SOLID principle does Square break?

    open class Rectangle {    open var width = 0    open var height = 0    val area get() = width * height} class Square : Rectangle() {    override var width: Int        get() = super.width        set(value) { super.width = value; super.height = value }    override var height: Int        get() = super.height        set(value) { super.width = value; super.height = value }} fun resize(r: Rectangle) { r.width = 5; r.height = 4; println(r.area) }fun main() { resize(Rectangle()); resize(Square()) }

    It prints 20 and then 16.

  6. 06

    How does Interface Segregation apply to Android listeners and repositories, and do default methods fix a fat interface?

    No client should depend on methods it does not use: split a fat interface into small role interfaces, which one class can still implement together.

    // fat: a screen that only shows the profile also depends on uploads and account deletioninterface UserRepository {    suspend fun profile(id: Long): Profile    suspend fun updateName(id: Long, name: String)    suspend fun uploadAvatar(id: Long, jpeg: ByteArray)    suspend fun deleteAccount(id: Long)…
  7. 07

    Should every class have an interface? When is an interface with a single implementation worth it?

    No. An interface earns its place where the implementation must be swappable: a fake in tests, another platform, a module boundary that must not see the real class.

  8. 08

    What is the difference between a factory function, Factory Method and Abstract Factory, and where does Android use each?

    A factory function hides how one object is built behind a name; Factory Method lets a subclass decide what to create by overriding a method the framework calls; Abstract Factory is one object that creates a whole family of related objects that must match each other.

    // factory function: named like the type, validates and hides the implementation classinterface Session { val userId: Long }private class ActiveSession(override val userId: Long) : Session fun Session(userId: Long): Session {    require(userId > 0) { "invalid user id: $userId" }…
  9. 09

    Named and default arguments replace most Builders in Kotlin. When is a Builder still the right choice, and why do OkHttp and Retrofit keep one?

    A Builder still pays off when an object is configured step by step in different places, when build() must validate options together, when you derive variants from an existing object, and when a library API must stay binary compatible; OkHttp and Retrofit hit all four.

    class HttpConfig private constructor(    val baseUrl: String,    val timeoutMs: Long,    val headers: Map<String, String>,) {    fun newBuilder() = Builder(this)                        // derive a variant…
  10. 10

    Find the bug: image requests to a third-party CDN carry the user's access token.

    val base = OkHttpClient.Builder()    .connectTimeout(10, TimeUnit.SECONDS)    .readTimeout(20, TimeUnit.SECONDS) val apiClient = base    .addInterceptor(AuthInterceptor(tokens))    .build() // meant to be: the same timeouts, without authval cdnClient = base.build()

    OkHttpClient.Builder is mutable: addInterceptor adds to base itself and returns the same builder, so base.build() later creates a second client that also carries AuthInterceptor, and every image request sends Authorization to the CDN.

  11. 11

    Adapter, Decorator, Proxy and Facade all wrap another object. How do you tell them apart, and is RecyclerView.Adapter a real Adapter?

    By intent: an Adapter changes the interface so incompatible code fits together, a Decorator keeps the interface and adds behaviour, a Proxy keeps the interface and controls access to the real object, and a Facade puts one simpler interface in front of a whole subsystem.

    interface ImageSource {    suspend fun load(url: String): ByteArray} // Adapter: a callback SDK made to fit the interface the app expectsclass SdkImageSource(private val sdk: LegacySdk) : ImageSource {…
  12. 12

    What are Flyweight and Object Pool, and why do Drawable.mutate() and Message.obtain() exist?

    Flyweight shares one heavy state between many light objects; Object Pool reuses mutable objects instead of allocating new ones.

    // Flyweight: drawables from one resource share a ConstantState until mutate()val star = ContextCompat.getDrawable(context, R.drawable.ic_star)!!val dimmed = ContextCompat.getDrawable(context, R.drawable.ic_star)!! dimmed.alpha = 80             // wrong: star and every other ic_star fade toodimmed.mutate().alpha = 80    // right: dimmed gets its own copy of the state first…
  13. 13

    How does Chain of Responsibility work, and how would you build one in Kotlin, for example to route deep links?

    A request travels along an ordered list of handlers and each one either handles it or passes it on, so the sender does not know who answers.

    sealed interface Route {    data class Product(val id: String) : Route    data class Order(val id: Long) : Route    data object Home : Route}…
  14. 14

    What is the Template Method pattern, why must an Activity call super.onCreate(), and what replaced overriding in newer Android APIs?

    Template Method fixes the steps of an algorithm in a base class and lets subclasses fill in hooks.

    sealed interface SyncResult {    data object Skipped : SyncResult    data class Done(val count: Int) : SyncResult} // the template: the order of steps is fixed in the base class…
  15. 15

    What is the Command pattern, and how would you use it to implement undo in an editor?

    Command turns a request into an object that can be stored, queued, logged or reversed.

    interface EditCommand {    fun execute(doc: StringBuilder)    fun undo(doc: StringBuilder)} class Insert(private val at: Int, private val text: String) : EditCommand {…
  16. 16

    How do you write a state machine in Kotlin with sealed classes, and how does the State pattern differ from Strategy?

    Make each state a sealed subtype that carries only its own data, and put every transition in one function (State, Event) -> State that the compiler checks exhaustively.

    sealed interface Upload {    data object Idle : Upload    data class Sending(val file: String, val attempt: Int) : Upload    data class Done(val url: String) : Upload    data class Failed(val file: String, val reason: String) : Upload}…
  17. 17

    Open/Closed in Kotlin: when is a sealed hierarchy with when a better design than a polymorphic interface, and why does Kotlin rarely need Visitor?

    An interface whose methods each type implements is open to new types and closed to new operations; a sealed hierarchy with when is the reverse, since a new operation is just a new function and a new subtype breaks every when at compile time.

    // open to new TYPES: another module adds a provider without touching this codeinterface PaymentProvider {    val id: String    fun feeCents(amountCents: Long): Long}…
  18. 18

    Every screen in your app extends a BaseActivity that is five levels deep. What goes wrong, and how do you refactor it into composition?

    Every level adds behaviour all screens inherit whether they need it or not, the super call order becomes hidden coupling, and one change can break any screen.

    // before: every screen inherits these checks, in super-call orderopen class BaseActivity : AppCompatActivity() {    override fun onResume() {        super.onResume()        if (!session.isLoggedIn) startActivity(Intent(this, LoginActivity::class.java))        analytics.screen(javaClass.simpleName)…
  19. 19

    What anti-patterns do you look for when you inherit an Android codebase, and in what order do you refactor them?

    The usual suspects are the god Activity, Utils and Manager dumping grounds, global mutable singletons and copy-pasted logic.