I recently read Modern Software Engineering: Doing What Works to Build Better Software Faster by David Farley. This article explains, technically, one way, described in the book, that will make your code better.
What is great code?
All the example provided will revolve around a cart in which you can add or remove some items.
Great code is easy to understand, testable, has minimum coupling, etc. One of the qualities that we will often hear is that it does one thing only.
The very bad
In some codebases, we will see code like this one:
def add_to_cart(self, item):
self.cart.add(item)
conn = sqlite3.connect('my_db.sqlite')
cur = conn.cursor()
cur.execute('INSERT INTO cart (name, price) values (item.name, item.price)')
conn.commit()
conn.close()
return self.calculate_cart_total()
This code is (not exactly) from "Modern Software Engineering: Doing What Works to Build Better Software Faster" by David Farley, page 240
In the above example, our code is highly coupled. This is often considered to be bad code since everything is bound together, it is "hard" to read, and is hard to test.
A Bit Better
In a lot of teams I joined through my career, most organize their code around services. As an example, the code will contain a CartService
class which might contain multiple methods. An example implementation might look like this:
class CartService
def __init__(self, repository):
self.repository = repository
def add(item: Item):
...
def get_items():
...
...
This is arguably a better alternative to our above function as the access to our database has been abstracted away behind a repository. This way:
- our add method will be easier to read,
- our service is testable,
- our implementation is not coupled to sqlite: we could easily inject a different repository implementation.
Even if the above code is better, I tend to push the teams I join away from it:
The service approach often grows in a bad way. You will always have more than 2 or 3 methods per service and these methods all have different dependencies. In the end, you are still getting highly coupled code, since all your Cart
related code is shipped together.
One Step Further
I tend to organize my code around use cases. Use cases are a great way of organizing code as it isolates your processes independently from one another.
“One class, one thing. One method, one thing.”
This motto is great when you wish to reduce cohesion. If we use the same cart example as above, our add_to_cart
use case would look like this:
class AddToCart:
def add_to_cart(self, item):
self.cart.add(item)
self.repository.add(item)
return self.calculate_cart_total()
In this case, our "service" contains only one function: "add_to_cart", and is responsible for one process: the one of adding items to our cart. This "use case" can be injected anywhere and will not pull a lot of useless dependencies as the "service" approach will.
This said, there is still one caveat: the repository.
The repository has the same problem our service had... It contains all the logic responsible for interaction with the persistence part of our code...
Best Approach
In his book, David Farley does not even mention repositories. Instead, he shares 2 alternative implementations:
def add_to_cart2(self, item):
self.cart.add(item)
self.store.store_item(item)
return self.calculate_cart_total();
def add_to_cart3(self, item, listener):
self.cart.add(item)
listener.on_item_added(self, item)
Code from "Modern Software Engineering: Doing What Works to Build Better Software Faster" by David Farley, page 240
His book discusses the advantages of using one version versus the other, but this is not what I want to discuss here (I encourage you to read the book to find out).
What is great here is that the "repository" concept:
- is abstracted: either completely hidden from the implementation (add_to_cart3) or replaced by a store (add_to_cart2).
- requires an implementation that does one thing only.
In add_to_cart2
, our "repository" is reduced to another use case the one of storing the item via the store.
One of the examples shown in the book is the following:
public interface WordSource
{
String[] words();
}
This is a perfect "repository" abstraction that does only one thing.
Summary
“One class, one thing. One method, one thing.”
Top comments (0)