Object Calisthenics
9 steps to better OO code
About me
Paweł Lewtak
Senior Developer at Xstream
@pawel_lewtak
Agenda
Learn how to make our code more:
- readable
- reusable
- testable
- maintainable
Things worth knowing
- DRY
- KISS
- SOLID
- YAGNI
- GRASP
Calisthenics
Cal • is • then • ics
/ˌkaləsˈTHeniks/
"Calisthenics are exercises consisting of a variety of gross motor movements; often rhythmical and generally without equipment or apparatus."
Wikipedia
Object Calisthenics
Written for Java
Why bother?
Code is read more
than it is written
Author unknown
You need to write code that minimises the time it would take someone else to understand it - even if that someone else is you
Art of Readable Code by Dustin Boswell, Trevor Foucher
Rule #1
Only one level of indentation per method
class Board(object):
def __init__(self, data):
# Level 0
self.buf = ""
for i in range(10):
# Level 1
for j in range(10):
# Level 2
self.buf += data[i][j]
class Board(object):
def __init__(self, data):
self.buf = ""
self.collect_rows(data)
def collect_rows(self, data):
for i in range(10):
self.collect_row(data[i])
def collect_row(self, row):
for j in range(10):
self.buf += row[j]
class UserService(object):
def register(self, username, email, promo_code = False):
user = self.create_user(username)
if email:
send_email(user, email)
if promo_code:
send_promo_code(user, promo_code)
products = self.get_products_by_user(...)
if products is None:
products = self.get_products_by_media(...)
if products is None:
products = self.get_products_by_domain(...)
if products is None:
products = self.get_any_products(...):
if products is None:
raise Exception('Access denied')
else:
...
else:
...
else:
...
else:
...
products = self.get_products_by_user(...)
if products is None:
products = self.get_products_by_media(...)
if products is None:
products = self.get_products_by_domain(...)
if products is None:
products = self.get_any_products(...):
if products is None:
raise Exception('Access denied')
# else...
Chain of command
class Command(object):
next_command = None
def add(self, next_command):
if self.next_command is None:
self.next_command = next_command
else:
self.next_command.add(next_command)
def _process(self, *args, **kwargs):
""" computations """
pass
def process(self, *args, **kwargs):
result = self._process(*args, **kwargs)
if result is None:
if self.next_command is not None:
return self.next_command.process(*args, **kwargs)
else:
return result
return None
class GetProductsForUser(Command): pass
class GetProductsByMedia(Command): pass
class GetProductsByDomain(Command): pass
class GetAnyProducts(Command): pass
commands = GetProductsForUser()
commands.add(GetProductsByMedia)
commands.add(GetProductsByDomain)
commands.add(GetAnyProducts)
products = commands.process(...)
Benefits
- Single responsibility
- Better naming
- Shorter methods
- Reusable methods
Rule #2
Do not use else keyword
def login (self, request):
if request.user.is_authenticated():
return redirect("homepage")
else:
messages.add_message(request,
messages.INFO,
'Bad credentials')
return redirect("login")
def login (self, request):
if request.user.is_authenticated():
return redirect("homepage")
messages.add_message(request,
messages.INFO,
'Bad credentials')
return redirect("login")
def function(param):
if param is not None:
value = param
else:
value = "default"
return value
def function(param):
value = "default"
if param is not None:
value = param
return value
def function(var_a, var_b, var_c, var_d):
if var_a:
if var_b:
# some code
else:
# some code
elif var_b and var_c:
if not var_d:
# some code
else:
# some code
elif var_b and not var_c:
# some code
else:
# some code
Extract code
Default value
Polymorphism
Strategy pattern
State pattern
https://www.quora.com/Is-it-true-that-a-good-programmer-uses-fewer-if-conditions-than-an-amateur
Benefits
- Avoids code duplication
- Lower complexity
- Readability
Rule #3
Wrap primitive types if it has behaviour
Value Object in DDD
class Validator(object):
def check_date(self, year, month, day):
pass
# 10th of December or 12th of October?
validator = Validator()
validator.check_date(2016, 10, 12)
class Validator(object):
def check_date(self, year: Year, month: Month, day: Day) -> bool:
pass
# Function call leaves no doubt.
validator.check_date(Year(2016), Month(10), Day(12))
def calculate_distance(source_x, source_y, target_x, target_y):
pass
calculate_distance(1, 2, 3, 4)
from collections import namedtuple
class Point2D(namedtuple("Point2D", "x y")):
pass
def calculate_distance(source_point, target_point):
pass
calculate_distance(Point2D(1, 2), Point2D(3, 4))
Benefits
- Encapsulation
- Type hinting
- Attracts similar behaviour
Rule #4
Only one dot per line
OK: Fluent interface
class Poem(object):
def __init__(self, content):
self.content = content
def indent(self, spaces):
self.content = " " * spaces + self.content
return self
def suffix(self, content):
self.content = self.content + " - " + content
return self
Poem("Road Not Travelled").indent(4)\
.suffix("Robert Frost")\
.content
Not OK: getter chain
class CartService(object):
def get_token(self):
token = self.get_service('auth')\
.auth_user('user', 'password')\
.get_result()\
.get_token()
return token
# 1. What if None is returned instead of object?
# 2. How about exceptions handling?
class Field(object):
def __init__(self):
self.current = Piece()
class Piece(object):
def __init__(self):
self.representation = " "
class Board(object):
def board_representation(self, board):
buf = ''
for field in board:
buf += field.current.representation
return buf
class Field(object):
def __init__(self):
self.current = Piece()
def add_to(self, buffer):
return self.current.add_to(buffer)
class Piece(object):
def __init__(self):
self.representation = " "
def add_to(self, buffer):
return buffer + self.representation
class Board(object):
def board_representation(self, board):
buf = ''
for field in board:
buf = field.add_to(buf)
return buf
Benefits
- Encapsulation
- Demeter's law
- Open/Closed Principle
Rule #5
Do not abbreviate
Why abbreviate?
Too many responsibilities
Name too long
def register_user_send_welcome_email_and_add_to_default_groups():
pass
# vs
def handle_user_registration():
user = create_user()
send_welcome_email(user)
add_to_default_groups()
Avoid confusion
acc = 0
// accumulator? accuracy?
pos = 100
// position? point of sale? positive?
auth = None
// authentication? authorization? both?
Duplicated code
class Order(object):
def ship_order(self):
pass
order = Order()
order.ship_order()
// vs
class Order(object):
def ship(self):
pass
order = Order()
order.ship()
Split & extract
Refactor!
Think about proper naming
Benefits
- Clear intentions
- Indicate underlying problems
Rule #6
Keep your classes small
What is small class?
- 15-20 lines per method
- 50 lines per class
- 10 classes per module
Benefits
- Single Responsibility
- Smaller modules
- Coherent code
Rule #7
No more than 2 instance variable per class
Class should handle single variable state
In some cases it might be two variables
class CartService(object):
def __init__(self):
self.logger = Logger()
self.cart = CartCollection()
self.translationService = TranslationService()
self.auth_service = AuthService()
self.user_service = UserService()
class CartService(object):
def __init__(self):
self.logger = Logger()
self.cart = CartCollection()
Benefits
- High cohesion
- Encapsulation
- Fewer dependencies
Rule #8
First class collections
Python's collections module
Doctrine's ArrayCollection
Benefits
- Single Responsibility
Rule #9
Do not use setters/getters
Accessors are fine
Don't make decisions outside of class
Let class do it's job
Tell, don't ask
class Game(object):
def __init__(self):
self.score = 0
def set_score(self, score):
self.score = score
def get_score(self):
return self.score
# Usage
ENEMY_DESTROYED_SCORE = 10
game = Game()
game.set_score(game.get_score() + ENEMY_DESTROYED_SCORE)
class Game(object):
def __init__(self):
self.score = 0
def add_score(self, score):
self.score += score
# Usage
ENEMY_DESTROYED_SCORE = 10
game = Game()
game.add_score(ENEMY_DESTROYED_SCORE)
Benefits
- Open/Closed Principle
Recap
- Only one level of indentation per method,
- Do not use else keyword,
- Wrap primitive types if it has behavior,
- Only one dot per line,
- Don’t abbreviate,
- Keep your entities small,
- No more than two instance variable per class,
- First Class Collections,
- Do not use accessors
Homework
Create new project up to 1000 lines long
Apply presented rules as strictly as possible
Draw your own conculsions
Customize these rules
Make them your own
Final thoughts
These are not best practices
These are just guidelines
Use with care!
Questions?
Links
Thank you!
@pawel_lewtak
Object Calisthenics (Code Europe)
By Paweł Lewtak
Object Calisthenics (Code Europe)
- 322