DEV Community

Discussion on: The Strategy Pattern Exemplified in TypeScript

Collapse
lifelongthinker profile image
Sebastian Author

Thanks for your input, Yufan. Sure you can just pass in the "meat" of our current SortingStrategy (the compare function) directly.

This is in no way an OOP vs. FP post. I'm sorry if I gave the wrong impression of OOP being the only (or the best) solution here. That was by no means intended.

The sub types of SortingStrategy here are very basic. Design pattern code examples tend to look over-engineered when they are stripped naked of all (potentially distracting) details. In a real-world scenario, though, my Strategy instances tend to be parameterized (and use inheritance). All things FP can do, too, of course.

The indirection you mention is just a theoretical discussion that holds no merit in a post like this. If your mindset is pro-FP then you might find it more suitable to use higher order programming.

Though, I must object to one point you're mentioning: Insinuating FP solves things better than OOP here ("We are past that now"), is just as pointless as making the reverse claim. There is no "object-oriented obsession" involved here.

Collapse
louy2 profile image
Yufan Lou

My bad for coming off combative. I do wonder though, since inheritance in the example is actually an implementation of an interface, for what kind of Strategies do you feel inheritance is more convenient than interface in TypeScript? I initially thought about specialization, but with structural typing that's not a problem. Now what I have in mind is providing default implementation of extra utility functions on some minimal implementation, since TypeScript interfaces cannot contain implementations, but that seems not as related to Strategies.

Thread Thread
lifelongthinker profile image
Sebastian Author

Don't worry, I know what it feels like when the drive for optimization is triggered.

You are guessing correctly, I like to used base classes for default implementations and override certain logic in specialized classes (even those in specialized modules outside of "my" modules).

But this is just my personal preference for ordering and packaging code. The implementation of any interface could just as well resort to 'composition over inheritance', or the like. As long as there is consistency, I'm all fine and happy with it.

Thread Thread
louy2 profile image
Yufan Lou

What I have come to realize is that composition versus inheritance is often not a decision I get to make. It is not a purely stylistic choice either.

For example, if I want to write composition instead of inheritance in TypeScript, I have to either manually delegate the default implementations, or add a dependency or make something like property-tunnel. Because the class part of TypeScript has been designed to accommodate the OOP people, which I think is important for the spread of the language. Using inheritance is the designed way, but that restricts me to one set of default implementations per class. This has led to the mixin pattern, using interface to extend multiple classes, as far as I know.

In Rust, default implementation is delegated automatically, with multiple possible traits on one struct, but specialization is still unstable, as soundness of full specialization is a hard problem. It seems the mixin pattern has similar problems.

Golang does away with inheritance and uses automatic delegation for composition. But it also does away with generics, and it remains to be seen how that would interplay with composition when Go 2.0 comes.

Meanwhile OCaml and Haskell goes higher kind (Functor) for both default implementation and specialization.

Come to really think about it, the interchange of features between OOP languages and functional languages have been going on way longer than impressions imply. OOP has a slogan "everything is an object", which really prioritizes objects in the parameters. Functional programming does not go "everything is a function", but "anything can be a parameter", which I find more inclusive and flexible.

(To quip a bit, for "everything is an X", Lisp is basically "everything is an s-expression", and only Ruby has taken "everything is an object" to a similar level of flexible power. JavaScript had similar potential but people got scared off eval (justifiably), and V8 never saw a pressing need to add proper macro.)

Pardon my rant again :P. Anyway, below I show an interface version of the Strategy pattern. In the end, the spirit is the same.

class CourseView
{
    constructor(public readonly course: Course) { }

    public sortingStrategy: SortingStrategy = () => ComparisonResult.Equal;

    // ...
}

interface SortingStrategy {
    (a: Student, b: Student): ComparisonResult;
}

// ...

cs101View.sortingStrategy = LastNameAscendingSortingStrategy;
cs101View.printParticipants("SORTED BY LAST NAME ASCENDING");

cs101View.sortingStrategy = SatScoreDescendingSortingStrategy;
cs101View.printParticipants("SORTED BY SAT SCORE DESCENDING");

cs101View.sortingStrategy = StudentNrAscendingSortingStrategy;
cs101View.printParticipants("SORTED BY STUDENT NR ASCENDING");

Speaking of which, I'd really appreciate if you can add to the post an example of a similarly parameterized strategy to show the full potential of the pattern. I used to show even beginners what's down the road maybe four or five steps from a basic tutorial, and have often been surprised how much they have figured out themselves towards it the next time I met them.