Error handling in Kotlin

Error handling in Kotlin

How to abolish try-catch blocks and make your code safer and cleaner by using concepts from functional programming

I really don't like try-catch blocks and how exceptions are dealt with in many object-oriented programming languages.

Oftentimes, the caller of a function doesn’t even know that it might throw an exception and this lack of referential transparency makes code more difficult to understand and can lead to unforeseen bugs and issues.

To handle exceptions, we have to surround function calls with try-catch blocks that make the code messy, often just to perform logging or to free resources and to then re-throw the exception, or even worse, return a null value_._ Handling errors is often a bothersome task.

When I made my initial attempts at functional programming with Scala, I remember that what I liked the most was how the language enabled you to handle null-values and errors.

Even though you can write a regular try-catch block in Scala, you can also choose not to, and I never once did. The language features a few algebraic data types in its standard library, one of which provides an alternative for the imperative try-catch block, the aptly named Try.

The basic idea is that you wrap a function that you want to execute but which might throw an error in a Try. The Tryexecutes the function, catching any errors that occur and creates one of its two subtypes, a Successor a Failure.

They respectively either hold the resulting value of the function or the error that is potentially created.

Since Kotlin has decent support for the functional programming paradigm, the first thing I did when learning the language was trying to imitate what I learned from Scala.

I liked Kotlins built-in null-type with a special syntax, and Java now has its Optionaltype in the standard library, but both lack an equivalent for error handling.

Luckily, it is quite simple to implement, and Kotlin’s, with its language features, lends itself very well to that concept.

Try wraps a computation that can fail and gives you one of two results, a Success or a Failure, containing either the result or the error. The exception never leaves the Failure unless you manually access and rethrow it.

Let’s look at a very basic example with an implementation of a Tryin Kotlin.

In the below code, we attempt to assign two values wrapped in a Try, one of which will fail with an exception. The exception that is thrown when assigning resultB will not terminate the function however, and the printing operations will both be executed.

fun main() {
    val resultA : Try<String> = Try { "Hello, World" }
    val resultB : Try<String> = Try { throw Exception("Not Today!") }

    println(resultA)
    println(resultB)
}

BasicExample.kt

The result you would see in the console is the following. As expected, the first is a Successholding a string value and the second is a Failurecontaining the exception. To know which one you’re dealing with, you can perform a type check.

Success("Hello, World")
Failure("Not Today!")

This provides you with the benefit of encapsulating any error that might occur and having an abstraction over potential failure.

When using the Tryas the return type of a function, it indicates to the caller that it might fail and the result could be an error, since, for example, a return type that would usually be a String now becomes a Try<String>.

That is not all though, the Tryalso provides a set of higher-order functions that allow transforming the value it contains.

This has the effect that you can keep working with the Try, passing it around and manipulating the data inside without having to determine whether you are dealing with a Successor a Failureuntil the very last moment when the final result is ultimately required, kind of like Schrödinger’s value.

Let’s look at a slightly more involved example of using a Tryin Kotlin, before looking at the implementation.

fun main() {

    val lines = Try {
        File("./my-pets.csv").readLines().map { it.split(',') }
    }

    val pets : Try<List<Pet>> = lines.map { it.map(::toPet) }

    when (pets) {
        is Success -> println(pets.value)
        is Failure -> println(pets.error)
    }
}

fun toPet(values: List<String>): Pet {
    val name = values[0]
    val age = values[1].toInt()
    val type = PetType.lookup(values[2])

    return Pet(name, age, type)
}

data class Pet(
    val name: String,
    val age: Int,
    val type: PetType
)

enum class PetType(val type: String) {

    Cat("Cat"),
    Dog("Dog"),
    Ferret("Ferret");

    companion object {
        val mapping = PetType.values().associateBy(PetType::type)
        fun lookup(type: String) = mapping[type] ?: throw Exception("No Pet Type: $type")
    }
}

SimpleExample.kt

A simple example of a Try at work

A typical exercise, we want to read the lines from a CSV file which could throw all kinds of I/O exceptions and, usually, we’d surround this either with a try-catch or just let any potential exception fly and terminate the program or jump into the next catch-block further up the stack if there is any.

The Tryencapsulates the operation we want to execute and holds on to the result, the List containing the lines from the file.

Commonly, you would want to perform an operation on the data, in this example, we want to convert each line from the file into a Pet object, and we can see that the toPet function involves a few operations that can fail.

Fails include converting a string to an integer to resolve the age and looking up an enum by a string to get the PetType.

For reference, the content of the file I’m using for this exercise looks like this, we can see that with the above code the last two lines will cause issues, Rambo has no age and Mike is not a valid PetType.

Spot,7,Dog
Alice,14,Cat
Rambo,,Dog
Mike,3,Raccoon

Since the mapoperation on a Tryalso catches any errors that might occur, there is nothing that can really go wrong. Later, when we want to know if our operation succeeded, we perform the type-check to either extract the result value from a Success or an error from the Failure.

Printing the result of the above example will yield the following; notice how we receive the first error that occurred, which happens when we try to extract Rambo's age. In the case that the file wouldn't be accessible for any reason, the Failure would contain the message of some I/O exception.

Failure(For input string: "")
A First Try

A basic Tryas seen above is rather easy to implement, let's start with the most common cases, constructing it, mapping the value, and combining it with another Try.

sealed class Try<T> {

    companion object {
        operator fun <T> invoke(func: () -> T): Try<T> =
            try {
                Success(func())
            } catch (error: Exception) {
                Failure(error)
            }
    }

    abstract fun <R> map(transform: (T) -> R): Try<R>
    abstract fun <R> flatMap(func: (T) -> Try<R>): Try<R>

}


class Success<T>(val value: T) : Try<T>() {

    override fun <R> map(transform: (T) -> R): Try<R> = Try { transform(value) }

    override fun <R> flatMap(func: (T) -> Try<R>): Try<R> =
        Try { func(value) }.let {
            when (it) {
                is Success -> it.value
                is Failure -> it as Try<R>
            }
        }
}

class Failure<T>(val error: Exception) : Try<T>() {
    override fun <R> map(transform: (T) -> R): Try<R> = this as Try<R>
    override fun <R> flatMap(func: (T) -> Try<R>): Try<R> = this as Try<R>
}

Try.kt

A simple implementation of a Try in Kotlin

A data type can be implemented with a sealed class in Kotlin, which is an abstract class that can only be extended in the same file. That guarantees that there will be no other implementations elsewhere since a Try only ever has two subtypes, Successand Failure.

The sealed class Tryhas a companion object which implements invoke, which allows you to call Try as if you would call its constructor, as seen in the above example, which is not necessarily required but makes it more expressive.

The invoke contains the potentially last try-catch block that you’ll have to write, which executes the function you pass to it and either returns the Successor the Failure.

The Try also defines the signatures for the mapand flatMapoperations, so that we can use them regardless of which type we end up with.

“Do or do not, there is no try” — Yoda

Implementing it for the Failurecase is very straightforward as it only holds on to an exception, otherwise, it does not do anything for now and the two operations simply return the Failureagain, since we have no value to operate with.

Successholds the actual value and can manipulate it, the mapsimply takes the function that you pass to it and plucks the value in, wrapped in a new Try, which also gives us immutability.

The flatMapis a bit more tricky since the function that is passed in returns a Tryas well and you don’t want to end up with a Try<Try<T>> but instead rather with a Try<T>, so we need to extract the result of the operation we get handed in, which allows combining two Tryinto one.

In the following examples, we will see why this is an important operation.

The workflow of a Try. Perform operations on Success otherwise, do nothing and skip to the end.

Not So Fast!

If you would start adopting this kind of programming style, you probably wouldn’t have a function like toPet which just randomly throws exceptionsaround.

One point of this construct is, after all, to achieve more referential transparency, so instead, the toPet function would probably return a Try<Pet>.

Furthermore, the lookup of the PetType would also return a Try<PetType>. This would make things more clear for the caller and safer to use, but it also makes them more complicated to handle since we now have all these Trycontaining our data and we need to combine them.

Sure, that's what we made the flatMapoperation for. Let’s look at an adapted version of toPet.

fun toPet(values: List<String>): Try<Pet> {
    val name = values[0]
    val ageTry = Try { values[1].toInt() }
    val typeTry = PetType.lookup(values[2])

    return ageTry.flatMap { age -> typeTry.map { type -> Pet(name, age, type) } }
}

enum class PetType(val type: String) {

    Cat("Cat"),
    Dog("Dog"),
    Ferret("Ferret");

    companion object {
        val mapping = PetType.values().associateBy(PetType::type)
        fun lookup(type: String) = Try { mapping[type] ?: throw Exception("No Pet Type: $type") }
    }
}

NotSoSimple.kt

There is a clear advantage of having every call to toPet error safe, now, if something goes wrong and for example, a single line in your file doesn't work out, it will not terminate your computation and you don't lose all the other results. Instead, you have a list containing Successesand Failures.

You might have realized that what we’re dealing with now is a Try<List<Try<Pet>>>, which is admittedly pretty awful, a nested type like this will cause you headaches, but that's the main reason I choose this example because it is a very real scenario.

It also starts to illustrate how, when dealing with dependent values and not a simple chain of operations, we can quickly get ugly and hard to read nested map statements like the above, but we can deal with all that.

The output would now look like the following though and we see that despite having errors we kept the results and the exceptions nicely packed up.

[Success(Pet(name=Spot, age=7, type=Dog)), 
Success(Pet(name=Alice, age=14, type=Cat)), 
Failure(For input string: ""), 
Failure(No Pet Type: Raccoon)]

How would you deal with toPet normally? Would you surround the operations with try-catch, suppressing all errors and returning a null value, or would you let it just throw an exception, or would you maybe use a NullObject pattern?

Collections With Try

As the combination of collections and constructs like Try can get a bit annoying, the implementations of a Tryin the wild usually feature a lot more functions than a mapand a flatMapbut an array of utilities, which cover many use-cases that you will encounter when working with this kind of construct for a while.

In case of the above example, there are a few handy extensions when dealing with collections, for example, traverseor partitionthat we can implement, just to give you an example.

Traversing collections

Traverseis commonly some sort of operation used when encountering collections and basically allows us to collapse a List<Try<T>> into a Try<List<T>> giving you a Success with a List only if all operations succeeded or a failure if any operation failed.

That, in turn, means you would lose all successful results again, but often we don’t really care, and it allows us to seamlessly use a function returning a Trytogetherwith a collection type.

We could implement a traverse like the following:

fun <T, R> Try.Companion.traverse(list: List<T>, transform: (T) -> Try<R>): Try<List<R>> = Try {
    val newList = mutableListOf<R>()
    for (value in list) {
        when(val result = transform(value)) {
            is Success -> newList.add(result.value)
            is Failure -> throw result.error
        }
    }
    newList
}

TryTraverse.kt

The pet example with traversewould look like the following.

Notice how we replaced the list-mapping with the traverseoperation and used a flatMapto combine the inner Tryfrom toPet with the outer Tryfrom reading the file into one which will contain the final result or an error.

fun main() {

    val lines = Try {
        File("./my-pets.csv").readLines().map { it.split(',') }
    }

    val pets : Try<List<Pet>> = lines.flatMap { Try.traverse(it, ::toPet) }

    when (pets) {
        is Success -> println(pets.value)
        is Failure -> println(pets.error)
    }
}

ExampleWithTraverse.kt

Using traverse to map the lines from the file to Pet objects.

Since the first error that occurs happens when getting Rambo’s age, we have the same output as in the first example, but, therefore, we have a very neat Try<List<Pet>> which is easy to handle.

Failure(For input string: "")
Partitioning collections

A partition, on the other hand, would give you a Pairwith two Lists, one with all Successesand one with all Failures, so a Pair<List<Success>, List<Failure>>>, which allows you to keep results as well as errors and handle both separately and when and how you want to.

Notice how we introduce a type-alias for the pair of lists in the below code, which I would recommend if you ever want to have functions taking or returning this type, simply for readability.

Though it might seem unnecessarily complicated at first, we can create further extension methods for this type for easier use, for example, a function that mapsthe values in the list of Successes.

This way, we can easily and efficiently work with the successful results while carrying the errors around without having to think much about it anymore.

typealias ResultSet<T> = Pair<List<Success<T>>, List<Failure<T>>>

fun <T> List<Try<T>>.asResultSet(): ResultSet<T> = this.partition { it is Success } as ResultSet<T>

fun <T, R> ResultSet<T>.map(transform: (T) -> R): ResultSet<R> =
    this.first.map { it.map(transform) }.asResultSet().let { result ->
        Pair(result.first, this.second as List<Failure<R>> + result.second)
    }

TryPartition.kt

Partitioning a list with a predicate will put all values for which the predicate holds true into the left side and all that are false into the right side of a Pair.

When we now consider our original pet example again, we can make use of partition and the type alias.

As an example, we convert all names of successfully imported pets to upper-case letters, by making use of the map method of our ResultSet type, which hides the awfulness of the nested type that we’re actually handling here.

fun main() {

    val lines = Try {
        File("./my-pets.csv").readLines().map { it.split(',') }
    }

    val pets : Try<ResultSet<Pet>> = lines.map {
        it.map(::toPet).asResultSet().map { pet -> pet.copy(name = pet.name.toUpperCase()) }
    }
    
    when (pets) {
        is Success -> println(pets.value)
        is Failure -> println(pets.error)
    }
}

ExamplePartition.kt

Undeniably, when working with this level of abstraction first there will be many use-cases to discover.

Though it sort of seems like a rabbit hole in the beginning, there is an end to it which, as the result, hopefully, holds safer and cleaner code.

The output from the above example is, as expected, the following. We can see that we successfully transformed the Successeswhile keeping all the Failuresas well.

([Success(Pet(name=SPOT, age=7, type=Dog)), 
Success(Pet(name=ALICE, age=14, type=Cat))], 
[Failure(No Pet Type: Raccoon), 
Failure(For input string: "")])

Time to Recover

Sometimes, an error is not the end of the world, and in certain situations, you might not care that much and would rather have some default value instead of an error message, or maybe you find that a certain error message that an exception produces does not convey enough information.

Similar to mapand flatMapwhich are used to transform the value in a Success, you can add equivalent methods, sometimes referred to as recoverand recoverWith, to transform the error inside a Failure and perform an action to compensate for the error.

The recoverbasically allows you to provide a function that returns a default value in case of an exception, whereas the recoverWithallows providing an alternative Try.

Coming back to our pet example, let’s say we are not satisfied with the error message we get when trying to parse the age and also business requirements have changed and now we’re supposed to import pets with an invalid type, marking them as Invalid.

Let’s check out how we can adapt toPet to accommodate these requirements using recovery.

fun toPet(values: List<String>): Try<Pet> {
    val name = values[0]
    val ageTry = Try { values[1].toInt() }.recoverWith { Failure<Int>(Exception("Cannot extract age!")) }
    val typeTry = PetType.lookup(values[2]).recover { PetType.Invalid }

    return ageTry.flatMap { age -> typeTry.map { type -> Pet(name, age, type) } }
}


enum class PetType(val type: String) {

    Cat("Cat"),
    Dog("Dog"),
    Ferret("Ferret"),
    Invalid("");

    companion object {
        val mapping = PetType.values().associateBy(PetType::type)
        fun lookup(type: String) = Try { mapping[type] ?: throw Exception("No Pet Type: $type") }
    }
}

ExampleWithRecover.kt

To change the error message that we receive when parsing of the age doesn't work out, we use recoverWithand basically replace the Failure with another Failure that contains a “better” error message.

To discard any error that occurs when extracting the type and using Invalid as a default PetType, we simply use recover.

Now, when running the last example with this adapted version of toPet, we get a different output.

As you’d expect we only have one Failure, now telling us that we couldn’t extract an age, where Mike is now being successfully imported and gets the default pet type.

[Success(Pet(name=Spot, age=7, type=Dog)), 
Success(Pet(name=Alice, age=14, type=Cat)), 
Failure(Cannot extract age!), 
Success(Pet(name=Mike, age=3, type=Invalid))]

“A program is a spell cast over a computer, turning input into error messages.” — Anonymous

#A Bit of Sugar

I don’t like the nested flatMapand mapexpressions in the above version of toPet, in Scala, there is a special for-expression that you can use on every algebraic data type that supports these two operations, which makes this a lot more expressive.

Even though Kotlindoes not have an equivalent, it is a very powerful language that allows for a lot of shenanigans. I borrowed the idea for this approach from the Arrowlibrary, which has a similar syntax for all their algebraic data types, that I want to introduce before finishing up.

The idea is that dependent values, calculated in sequence, can simply be unpacked in order and any error that occurs will interrupt the sequence.

fun toPet(values: List<String>) = Try.sequential {
    val name = values[0]
    val (age) = Try { values[1].trim().toInt() }
    val (type) = PetType.lookup(values[2].trim())

    Pet(name, age, type)
}

SequentialToPet.kt

Dealing with dependent types in an imperative style

Actually, I like this abbreviation a lot, since the brackets for the destructuring declaration also kind of make clear that a result is potentially not going to work out.

Implementing this is rather simple, at least for a Try, making use of receiver objects and scoped extension methods.

Due to the extension method only being available in the TrySequence object, we can only use it in a lambda expression that has it as its receiver type.

object TrySequence {
    operator fun <T> Try<T>.component1(): T = when (this) {
        is Success -> this.value
        is Failure -> throw this.error
    }
}

fun <T> Try.Companion.sequential(func: TrySequence.() -> T): Try<T> {
    return Try { func(TrySequence) }
}

TrySequenceExtension.k

In the end, the sequential operation is not much different than the regular invokeonTry. We can give it a function that produces a value, and we pluck that into a Try.

The only difference is that we add the extension to destructure a Try, which will return a value or throw an exception. This exception terminates the lambda but is caught by the surrounding Try.

It does nothing else but to provide us with more expressive ways of dealing with Try.

Final Thoughts

Whether functional programming is your jam or not, there are some clear advantages of adopting this style of error handling.

Making clear to the caller of a function that it could produce an error adds a big benefit to your code.

Containing errors and not throwing exceptions around makes things more stable and predictable and your program easier to reason about, as exceptions will not be something that happen outside your code anymore, but instead, the whole concept of error handling merely becomes a new type that can be extended and controlled however you like.

Exceptions are problematic by nature because of their runtime cost. An exception in Java captures a snapshot of its call stack on creation which is a rather costly operation, so using exceptions and, subsequently, Tryto control program flow is not necessarily recommended.

Since Try is only compatible with exceptions, there is a more generic algebraic data type that abstracts over failure in a more general way, as its error case is not fixed to exceptions which would, for example, allow you to simply use strings to communicate error messages, called Eitherwhich works in a very similar way.

The Tryon the other side definitely has its place as a mechanism to deal with exceptions since there are many cases where you don’t have a choice and just need to handle exceptions.

The final code of the Try developed and used here looks like the following:

sealed class Try<T> {

    companion object {
        operator fun <T> invoke(func: () -> T): Try<T> =
            try {
                Success(func())
            } catch (error: Exception) {
                Failure(error)
            }

        object TrySequence {
            operator fun <T> Try<T>.component1(): T = when (this) {
                is Success -> this.value
                is Failure -> throw this.error
            }
        }

        fun <T> sequential(func: TrySequence.() -> T): Try<T> = Try { func(TrySequence) }
        
    }

    abstract fun <R> map(transform: (T) -> R): Try<R>
    abstract fun <R> flatMap(func: (T) -> Try<R>): Try<R>
    abstract fun <R> recover(transform: (Exception) -> R): Try<R>
    abstract fun <R> recoverWith(transform: (Exception) -> Try<R>): Try<R>

}


class Success<T>(val value: T) : Try<T>() {

    override fun <R> map(transform: (T) -> R): Try<R> = Try { transform(value) }

    override fun <R> flatMap(func: (T) -> Try<R>): Try<R> =
        Try { func(value) }.let {
            when (it) {
                is Success -> it.value
                is Failure -> it as Try<R>
            }
        }

    override fun <R> recover(transform: (Exception) -> R): Try<R> = this as Try<R>
    override fun <R> recoverWith(transform: (Exception) -> Try<R>): Try<R> = this as Try<R>

    override fun toString(): String {
        return "Success(${value.toString()})"
    }

}

class Failure<T>(val error: Exception) : Try<T>() {

    override fun <R> map(transform: (T) -> R): Try<R> = this as Try<R>
    override fun <R> flatMap(func: (T) -> Try<R>): Try<R> = this as Try<R>

    override fun <R> recover(transform: (Exception) -> R): Try<R> = Try { transform(error) }
    override fun <R> recoverWith(transform: (Exception) -> Try<R>): Try<R> = Try { transform(error) }.let {
        when (it) {
            is Success -> it.value
            is Failure -> it as Try<R>
        }
    }

    override fun toString(): String {
        return "Failure(${error.message})"
    }
}

FinalTry.kt

The world of functional programming is filled with many such algebraic data types and many more concepts and patterns to explore.

When it comes to Kotlin, there seems to be a more or less comprehensive library that I mentioned a few times throughout this article, called Arrow, that might be worth checking out if you’re interested in going further with this_._

Think carefully about whether you’re going to write your own library of algebraic data types, or adapt an existing one.

Be aware that if you start using this kind of construct, it will be all over your codebase very quickly.

Having to refactor because of a botched implementation of your own or because the library of your choice is going a different way and just deprecated your favorite data types, the headache will be guaranteed, but it will be worth it.

Thank you very much for reading!

Kotlin Coroutines on Android - How to use Coroutines on Android

Kotlin Coroutines on Android - How to use Coroutines on Android

Coroutines are a Kotlin feature that convert async callbacks for long-running tasks, such as database or network access, into sequential code. This Kotlin Coroutines tutorial will show you how to use coroutines on Android, and how the new androidx-concurrent library makes it easy to use them to get things off the main thread. You'll also learn how the new library helps coroutines work with Architecture Components. This session also covers coroutine patterns, best practices, and even how to test coroutines!

Kotlin Coroutines on Android - How to use Coroutines on Android.

Coroutines are a feature of Kotlin that help convert callback-based code into sequential code, making code easier to read, write, and understand. This session will show you how to use coroutines on Android, and how the new androidx-concurrent library makes it easy to use them to get things off the main thread. You'll also learn how the new library helps coroutines work with Architecture Components. This session also covers coroutine patterns, best practices, and even how to test coroutines!

How to use Kotlin - from the Lead Kotlin Language Designer

How to use Kotlin - from the Lead Kotlin Language Designer

In this Kotlin tutorial, the lead Kotlin language designer will show you how you can write more idiomatic Kotlin, what the benefits are, and help you discover some of the most powerful yet lesser known features of Kotlin. Kotlin is similar to the Java programming language, so it's natural that your Kotlin code looks very much like Java code when you are first start to use the language.

Kotlin is similar to the Java programming language, so it's natural that your Kotlin code looks very much like Java code when you are first start to use the language. While this is fine to begin with, you're probably not taking full advantage of all the language benefits. In this session, the lead Kotlin language designer will show you how you can write more idiomatic Kotlin, what the benefits are, and help you discover some of the most powerful yet lesser known features of Kotlin.

Kotlin Programming Fundamentals Tutorial - Learn Kotlin for Beginners

Kotlin Programming Fundamentals Tutorial - Learn Kotlin for Beginners

Kotlin Programming Fundamentals Tutorial - Learn Kotlin for Beginners: Learn programming fundamentals using the Kotlin programming language. Kotlin is an excellent language for GUI Architectures, Libraries, and Server Side Applications. This course will start you off the right way, no matter which path you take with the language. The course features hands-on coding exercises to teach you both Functional, Event Driven, and Object Oriented design patterns.

Kotlin Programming Fundamentals Tutorial - Learn Kotlin for Beginners

Learn programming fundamentals using the Kotlin programming language. Kotlin is an excellent language for GUI Architectures, Libraries, and Server Side Applications. This course will start you off the right way, no matter which path you take with the language. The course features hands-on coding exercises to teach you both Functional, Event Driven, and Object Oriented design patterns.

💻 Code: https://github.com/BracketCove/KotlinCourseSamples

⭐️ Course Contents ⭐️
Section 1
⌨️ (0:00:00) Course Overview: About Me, You, and this Course
⌨️ (0:09:23) How to Run the Examples
⌨️ (0:10:59) Kotlin Syntax Practice for Beginners

Section 2
⌨️ (0:39:26) Data Landscape: Memory Spaces and Named Addresses (References)
⌨️ (0:44:21) How to use "val" and "const val" References to promote Immutability/Efficiency:
⌨️ (0:51:55) Using "var" Reference Types, and the problems with Shared Mutable State!
⌨️ (0:58:58) Giving Structure to Data with Classes

Section 3
⌨️ (1:19:45) A Fundamental Divide: Computation and Control Logic
⌨️ (1:22:52) Computing Data means Solving Problems
⌨️ (1:32:11) Controlling the Flow of Data
⌨️ (1:37:24) Event Driven Programs
⌨️ (1:57:33) Functional versus Imperative Program Style (mild introduction)

Section 4
⌨️ (2:18:38) What is Software Architecture?
⌨️ (2:21:14) Separation of Concerns
⌨️ (2:34:13) Dependency Inversion: Using Interfaces Effectively for Front End and Back End
⌨️ (3:06:22) Extension versus Abstraction: Open/Closed Principle
⌨️ (3:17:00) Dependency Injection: How, What, and Why?
⌨️ (3:30:23) Inversion of Control via the Service Locator Pattern

Section 5
⌨️ (3:44:25) Proving Programs with Tests (a light introduction to Testing)
⌨️ (4:01:42) Solving Problem (Domains) by Analysis