/ EXERCISE, PROGRAMMING, STYLE

Exercises in Programming Style, back to Object-Oriented Programming

The post of this week is special, as it’s about Object-Oriented Programming. It’s quite popular nowadays to dismiss OOP. There’s a lot of confusion around it. Some people conflate OOP with accessors (i.e. getters and setters), or shared mutable state (or even both). This is not true, as we will see in this post.

This is the 7th post in the Exercises in Programming Style focus series.Other posts include:

  1. Introducing Exercises in Programming Style
  2. Exercises in Programming Style, stacking things up
  3. Exercises in Programming Style, Kwisatz Haderach-style
  4. Exercises in Programming Style, recursion
  5. Exercises in Programming Style with higher-order functions
  6. Composing Exercises in Programming Style
  7. Exercises in Programming Style, back to Object-Oriented Programming (this post)
  8. Exercises in Programming Style: maps are objects too
  9. Exercises in Programming Style: Event-Driven Programming
  10. Exercises in Programming Style and the Event Bus
  11. Reflecting over Exercises in Programming Style
  12. Exercises in Aspect-Oriented Programming Style
  13. Exercises in Programming Style: FP & I/O
  14. Exercises in Relational Database Style
  15. Exercises in Programming Style: spreadsheets
  16. Exercises in Concurrent Programming Style
  17. Exercises in Programming Style: sharing data among threads
  18. Exercises in Programming Style with Hazelcast
  19. Exercises in MapReduce Style
  20. Conclusion of Exercises in Programming Style

A short reminder on OOP

The tenet of OOP is to model the system as objects that map the real world. However, one of the reason OOP came to be was in reaction to the previous usage of global variables - shared mutable state. Global variables can be accessed and set from anywhere in a program; hence, it’s easy to introduce bugs.

OOP principles are two-fold:

  • The processing and the storage of a specific piece of data should be co-located in an object - encapsulation
  • Objects communicate with each other through messages that may contain data…​ or not

Those principles aren’t about mutability nor getters/setters! While I agree in practice, some languages/frameworks/practices use mutability and accessors, nothing prevents you from doing without. This would still be OOP code - or maybe, only through their removal will you do true OOP.

Modelling the system

The original Python code offers the following model, which directly maps to classes:

Class Responsibilities

DataStorage

  • Read the text file
  • Parse the content into words
  • Store the words
  • Return the words

StopWordManager

  • Read the stop-words file
  • Parse its content
  • Store the stop words
  • Check whether a word is a stop-word or not

WordFrequencyManager

  • Store the word-frequency map
  • Manage the frequency when a word is submitted
    • If the word doesn’t exist yet, add it with a frequency of 1
    • If it does, increment its frequency by one

WordFrequencyController

Manage and order the flow of messages between the previous objects

Improving the initial design with the type system

The original Python code dispatches String messages, which is pretty error-prone and unfriendly to refactoring. To benefit from the type system, the dispatch could be achieved through a Message marker interface.

interface Message

In some cases, additional information is required. For that, a new abstract PayloadMessage<T> class implements Message, and provides a payload of type T the receiver can use.

abstract class PayloadMessage<T> : Message {
    abstract val payload: T
}

This leads to the following design:

The Letterbox class diagram

This is more than enough for the sample application. For more complex system, one could think about offering additional classes to hold more than one payload parameter e.g. PayloadMessage2<T, V>, PayloadMessage3<T, V, X>, etc. Instead, an OOP design could create actually create a concept around all parameters. For example, in order to create a Person, one would want to send the first name, the last name, and the birth date: instead of a PayloadMessage3<String, String, LocalDate> class, one could actually reuse the Person abstraction, or even better, create a dedicated PersonMessage(String, String, LocalDate).

The flow of the application is the following, it just needs to be implemented with Kotlin:

The flow of the application as a sequence diagram

Trading off type-safety for easier future changes

The core feature here is the dispatch() function. In general, classes offer a very strict API through their functions.

To introduce more flexibility, one creates an interface, and then the implementation can change more freely. Unfortunately, this makes designing the interface’s functions a one-time gamble. Once designed, any change beside adding a new function becomes a breaking change.

Having a more generic dispatch() function allows a lot more options. Unfortunately, this comes at a cost. The calling code makes use of the dispatch() method return value. Unfortunately, its return type is of type Any?, because it needs to be used in every context possible.

interface MessageDispatch {
    fun dispatch(message: Message): Any?
}

For that reason, the calling code needs to cast the return value every time, lowering type safety.

Show me the code!

Here’s a sample of one of the classes above:

class DataStorageManager : MessageDispatch {                                   (1)

    class WordsMessage : Message                                               (2)
    class InitMessage(override val payload: String) : PayloadMessage<String>() (2)

    private lateinit var filename: String

    private val words: List<String> by lazy {
        read(filename)
            .flatMap { it.split("\\W|_".toRegex()) }
            .filter { it.isNotBlank() && it.length >= 2 }
            .map(String::toLowerCase)
    }

    override fun dispatch(message: Message) = when (message) {                 (3)
        is InitMessage -> filename = message.payload
        is WordsMessage -> words
        else -> throw Exception("Uknown message $message")
    }
}
1 All classes implement MessageDispatch
2 Classes provide the messages they are able to handle in their dispatch() implementation
3 The dispatch() function is the single entry-point to a class

Conclusion

In this exercise, we were able to use OOP without using accessors or shared mutable state. Hence, one should never conflate them together.

Also, designing OOP classes is a matter of trade-off(s). Types are a great way to catch possible bugs at compile-time. However, this safety has a cost: it makes the initial design a gamble, as one generally doesn’t know what changes will be required in the future. Having a single entry-point function decreases the return type safety, but allows for more changes to be non-breaking.

The complete source code for this post can be found on Github.
Nicolas Fränkel

Nicolas Fränkel

Nicolas Fränkel is a Developer Advocate with 15+ years experience consulting for many different customers, in a wide range of contexts (such as telecoms, banking, insurances, large retail and public sector). Usually working on Java/Java EE and Spring technologies, but with focused interests like Rich Internet Applications, Testing, CI/CD and DevOps. Currently working for Hazelcast. Also double as a teacher in universities and higher education schools, a trainer and triples as a book author.

Read More