Object Calisthenics

9 steps to better OO code

Agenda

Learn how to make our code more:

  • readable
  • reusable
  • testable
  • maintainable

Raise you hand if you know one of the following:

  • DRY
  • KISS
  • SOLID
  • YAGNI
  • Zen of Python

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's written

Author unknown

You need to write code that minimizes 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]

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:
    ...

Chain of command

Benefits

  • Single responsibility
  • Better naming
  • Shorter methods
  • Reusable methods

Rule #2

Do not use else keyword

if options.get_categories() is None:
    ...
elif len(options.get_categories()) == 1:
    ...
elif SPECIAL_CATEGORY in options.get_categories():
    ...
elif options.get_categories() and options.get_query():
    ...
elif options.get_content_type():
    ...
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

Extract code

Default value

Polymorphism

Strategy pattern

State pattern

Examples

https://github.com/gennad/Design-Patterns-in-Python

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))

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?

Split & extract

Duplicated code?

Refactor!

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()
acc = 0
// accumulator? accuracy?

pos = 100
// position? point of sale? positive?

auth = None
// authentication? authorization? both?

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

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()

Benefits

  • High cohesion
  • Encapsulation
  • Fewer dependencies

Rule #8

First class collections

collections module

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

Catch 'em all! 

Catch 'em all! 

  1. Only one level of indentation per method,
  2. Do not use else keyword,
  3. Wrap primitive types if it has behavior,
  4. Only one dot per line,
  5. Don’t abbreviate,
  6. Keep your entities small,
  7. No more than two instance variable per class,
  8. First Class Collections,
  9. Do not use accessors

Catch 'em all! 

  1. Only one level of indentation per method,
  2. Do not use else keyword,
  3. Wrap primitive types if it has behavior,
  4. Only one dot per line,
  5. Don’t abbreviate,
  6. Keep your entities small,
  7. No more than two instance variable per class,
  8. First Class Collections,
  9. Do not use accessors
  10. ???
  11. PROFIT!

Homework

Create new project up to 1000 lines long

Apply presented rules as strictly as possible

Draw your own conculsions

Customize these rules

Final thoughts

These are not best practices

These are just guidelines

Use with caution!

Questions?

Links

https://www.slideshare.net/PaweLewtak/object-calisthenics-pycon-sk-2017

https://www.cs.helsinki.fi/u/luontola/tdd-2009/ext/ObjectCalisthenics.pdf

https://pragprog.com/book/twa/thoughtworks-anthology

https://github.com/gennad/Design-Patterns-in-Python

https://en.wikipedia.org/wiki/Law_of_Demeter

https://www.quora.com/Is-it-true-that-a-good-programmer-uses-fewer-if-conditions-than-an-amateur

Thank you!

Made with Slides.com