Reflections from a Programming Design course

Viral Tagdiwala
6 min readDec 21, 2021

--

“It’s programming if ‘clever’ is a compliment, but it’s software engineering if ‘clever’ is an accusation.”― Titus Winters, Software Engineering at Google: Lessons Learned from Programming Over Time

September 2021 — I enrolled (was forced to, since it’s a mandatory course) in what many graduates consider to be the most grueling course in the master's program at Northeastern, CS5010 (Programming Design Paradigms), and what follows is my key takeaways from it!

The Object-Oriented Scare

Coming from a world of Typescript, I was attuned to strong-typed languages, but what I was not attuned to was the strong adherence to the Object-Oriented principles that the course follows. It’s so strong that the course can most likely be renamed from Programming Design Paradigms to Object-Oriented Programming Design Paradigms in Java, the latter serving a rather befitting title.

In the initial month or so, the course felt out-of-touch to industry (especially startups) and while the latter can be argued, there still were some nifty takeaways that can be applied even if one wishes to stick to Functional Programming Languages!

Mapping all of it back to familiar territory

The whole point of this small exercise that follows is twofold —

  1. For me to have actual takeaways from the course in the syntax and language I am familiar with.
  2. To show that design principles, can be language-agnostic in 2021!

Mapping the patterns

Strategy

We add a context class on top

Now we can do something as simple as .executeStrategy to execute the correct function internally!

Command

As we can see, the more commands we add the higher the complication of this switch block becomes — reducing the readability and the ease with how changes can be made.

Now, we simply create a map with the command name and type of command to be run

Decorator

The decorator pattern attempts to add behavior to an already existing object during run-time. In a way, you can think of this as a dynamic inheritance because even though you’re not creating a new class to add the behavior, you’re creating a new object with extended functionality.

… I do realize there are many, many more — but to keep this article succinct had to get rid of those.

Mapping the principles

Single Responsibility

Single responsibility is one of those that can be pretty much applied to any language across the board, one function, one class does one thing only! This is one of the most language-agnostic principles out there & pretty much the easiest one to adhere to if you plan your design ahead of time (to some degree).

setTime(d: Date) {    
this.currentTime = d;
}

Notice how setTime does just one thing — sets the value of current time to the variable that’s being passed.

Open-closed

The open-closed principle emphasizes that we should be able to add new functionalities without changing the existing code.

Say, we have two instances of a digital clock and we need to compare the difference in their time?

This would be simple, just have a function that takes in two objects of Digital Clock and then getTime(), right?

Now, what if we sprinkle in an analog clock object into the mix? A naive approach here would be to re-write a version of compare that takes in a digital and an analog clock object and compares the two. But, it doesn't end there, now you need to write another version that takes in two analog clock objects too.

Sure, one can argue “We could just specify that the argument can be of type Analog OR digital clock” and while that’s a nifty feature offered by typescript, it can (and most likely will) lead to unreadable code.

function compareTime(obj1: AnalogClock|DigitalClock, obj2: AnalogClock|DigitalClock)

What if you forgot to implement the setTime in AnalogClock? Or what if the AnalogClock returns a different value for the same function? The possibilities are endless here.

We can stick to the Open-Closed principle here, by having both Analog & Digital Clock implement a Clock interface, and then the compare function takes in a “Clock” object.

Class AnalogClock implements Clock {
...
}
Class DigitalClock implements Clock {
...
}
function compareTime(obj1: Clock, obj2: Clock){
...
}

Liskov substitution

This principle at its core is ensuring unrelated parts of your system don’t break due to changes made in any other part.

Let’s take a look at an example to understand what this means.

We are going to declare a class whose responsibility is to persist some objects into some kind of storage. We will start by declaring the following interface:

interface IPersistanceService {
save(entity : any) : Date;
}

After declaring the IPersistanceService interface we can implement it. We will use cookies the storage for the application’s data:

We will continue by declaring a class named BirthdayController, which has a dependency on the IPersistanceService interface:

We can finally create an instance of BirthdayController and pass an instance of CookiePersitanceService via its constructor.

const birthday = new BirthdayController(new CookiePersitanceService());

The LSP allows us to replace a dependency with another implementation as long as both implementations are based on the same base type. For example, we decide to stop using cookies as storage and use the HTML5 local storage API instead without having to worry about the BirthdayController code being affected by this change:

We can then replace it without having to add any changes to the BirthdayController controller class:

const favController = new FavouritesController(new LocalStoragePersitanceService());

Interface segregation

The interface segregation principle states that no client should be forced to depend on methods it does not use. By putting too many properties in our interfaces, we risk breaking the above rule.

Typescript has MANY tricks up its sleeve for this rule.

It allows interfaces to extend other interfaces

interface Shape {
color: string;
}
interface Square extends Shape {
sideLength: number;
}

Interfaces can extend other classes

class Control {
private state: any;
}
interface SelectableControl extends Control {
select(): void;
}

By using either of these techniques, we can avoid bloating a single interface, packing it up with a bunch of functions.

Dependency Inversion

The course went on to focus a lot on interfaces, the primary way of integrating ensuring “D” (Dependency Inversion) in SOLID — this can be done in typescript too! Sticking to generalizable interfaces means that you can ensure all classes adhere to this contract.

See how we can use this ClockInterface in typescript to ensure each implementation of ClockInterface exposes a function to setTime().

Why (I feel) Typescript might be better suited for this course…

The last section of the course went over “view” in the MVC pattern — the implementation of it, however, was on the age-old Swing library. This brought about an unnecessary learning curve, now we had to not only learn a library (that was fairly limiting in and of itself) but also convert a project that was written just with a model and controller to use a view.

An Example View of a Dungeon game (the final project) implemented using swing

The takeaways and knowledge of swing are the bits of the course that I couldn’t map out to any tasks that I perform at work & felt like wasted effort, the same however if implemented in typescript, we could use React/or even plain HTML/CSS to have our view be rendered on the screen.

--

--

Viral Tagdiwala
Viral Tagdiwala

No responses yet