October 12, 2017

...Learn TDD with Codemanship

Manual Refactoring : Extract Class



A core principle of software design is that modules (I'll leave you to interpret that word for your own tech stack) should have a single distinct responsibility.

There are two good reasons for this: firstly we need to separate code that's likely to change at different times for different reasons, so we can make one change without touching the other code. And it provides us with much greater flexibility about how we can compose systems to do new things reusing existing modules.

Consider this simple example.



Arguably, this Python class is doing two jobs. I can easily imagine needing to change how movie ratings work independently of how movie summaries work.

To refactor this code into classes that each have a distinct single responsibility, I can apply the Extract Class refactoring.

First, we need a new class Ratings to move the ratings fields and methods to.



NOW RUN THE TESTS!

Next, paste in the features of Movie we want to move to Ratings.



AND RUN THE TESTS!

Now, we need to substitute inside Movie, delegating ratings methods to a field instance of the new class Ratings.



RUN THE TESTS!

Okay, so - technically - that's Extract Method completed. We now have two classes, each with a distinct responsibility. But I think we can clean this up a bit more.

First of all, another core principle of software design is that dependencies should be swappable. Let's introduce a parameter for ratings in Movie's constructor.



We can now vary the implementation of ratings - e.g., to mock or stub it for testing - without changing any code in Movie.

If Movie was part of a public API, we'd leave those delegate methods rate() and average_rating() on it's interface. But let's imagine that it's not. Could we cut out this middle man and have clients interact directly with Ratings?

Let's refactor the test code to speak to Ratings directly.



AND RUN THE TESTS!

Now, arguably, the first two tests belong in their own test fixture. Let's extract a new test class.



RUN THE TESTS!

Then we can remove the now unused delegate methods from Movie.



DON'T FORGET TO RUN THE TESTS!

And, to finish off, put each class (and test fixture) in its own .py file.

AND RUN THE TESTS!

This was a fairly straightforward refactoring, because the methods we wanted to move to the new class accessed a different field to the remaining methods. Sometimes, though, we need to split methods that access the same fields.



If I extract a new class for generating HTML, it will need to access the data of the Movie object it's rendering. One choice is to pass the Movie in as a parameter of to_html().



This has necessarily introduced Feature Envy in HtmlFormatter for Movie, but this may be a justifiable trade-off so that we can render movies in other kinds of formats (e.g., JSON, XML) without changing Movie. Here we trade higher coupling for greater flexibility.

In this refactored design, Movie doesn't need to know anything about HtmlFormatter.

Whether or not that's the right solution will depend on the specific context of your code, of course.




Posted 1 week, 3 days ago on October 12, 2017