how to write maintainable code without tests

53
How to write MAINTAINABLE CODE without tests

Upload: juti-noppornpitak

Post on 28-Jan-2018

208 views

Category:

Software


1 download

TRANSCRIPT

Page 1: How to write maintainable code without tests

How to write MAINTAINABLE CODE without tests

Page 2: How to write maintainable code without tests

– That’s me.

“How can we keep moving - like musicians who keep performing during a concert after making

mistakes?”

Page 3: How to write maintainable code without tests

JUTI NOPPORNPITAKSPEAKER SENIOR SOFTWARE DEVELOPER AT STATFLO

Page 4: How to write maintainable code without tests

Awesome, the test automation is.It’s just like rehearsal. Practice makes you confident.

Page 5: How to write maintainable code without tests

Sadly, not everyone has the luxury of time or the resources to

achieve test automation.Especially for those in the start-up community,

where every second counts.

Page 6: How to write maintainable code without tests

This topic is not about ignoring test automation.

Page 7: How to write maintainable code without tests

It is about how to design software before thinking

about testing.

Page 8: How to write maintainable code without tests

Also, it is about what you should expect if writing tests is not an option.

Page 9: How to write maintainable code without tests

ใ(ー 。ーใ)

Page 10: How to write maintainable code without tests

MAINTAINABILITY

Page 11: How to write maintainable code without tests

The maintainability of software

• isolate defects or causes, • correct defects or causes, • repair or replace defective

components without having to modify still working ones,

• make future maintenance easier, or • resist external changes.

Maintainability is a measure of the ease with which a

piece of software can be maintained in order to:

Page 12: How to write maintainable code without tests

SANS TESTS

Page 13: How to write maintainable code without tests

Maintainability without testsIn this case, all points become more difficult to achieve but it is still within the realm of possibility.

Page 14: How to write maintainable code without tests

(ー x ー)ง

Page 15: How to write maintainable code without tests

Keep things lean and simple!

When writing tests is not an option,then speed is all that matters.

In this case it is all about how fast any developers can understand the source code in a short amount of time so that they start either understanding or resolving the defects as soon as possible.

Page 16: How to write maintainable code without tests

Keep things lean and simple!

When writing tests is not an option, then speed is all that matters.

No unnecessary defensive code

Defensive code usually guards against improper use of code components.

However, many of us are working with the internal code. As we have full access to the source code, misuse is less likely.

Page 17: How to write maintainable code without tests

Keep things lean and simple!

When writing tests is not an option, then speed is all that matters.

No unnecessary defensive code

Only catch the exceptions you need.

Page 18: How to write maintainable code without tests

Keep things lean and simple!

When writing tests is not an option, then speed is all that matters.

No unnecessary defensive code

Only catch the exceptions you need.

Keep the cyclomatic complexity low.

The low cyclomatic complexity of any given subroutine indicates that the subroutine is not too complicated as the number of linearly independent execution paths is very small.

Page 19: How to write maintainable code without tests

Keep things lean and simple!

When writing tests is not an option, then speed is all that matters.

No unnecessary defensive code

Only catch the exceptions you need.

Hence, if you did it right…

Simple Implementation

Low Cyclomatic Complexity

Easily Understandable

Low Test Complexity

Keep the cyclomatic complexity low.

Page 20: How to write maintainable code without tests

The cyclomatic complexity directly affects the test complexity.

When we say we have test coverage, the "coverage" is not about how many lines have been executed. It’s about how many independent execution paths are covered by test automation.

Practically, we should aim to achieve the minimum test coverage where there exists enough test cases to test all independent execution paths.

FUN FACT

Page 21: How to write maintainable code without tests

Separation of Concerns

It is about separating a computer program into distinct sections such that each section addresses a separate concern. (Source: Wikipedia)

You can think of concerns as sub problems and sections as solutions to a corresponding sub problem.

Page 22: How to write maintainable code without tests

Separation of Concerns

The benefits are (A) you can implement test cases for a particular section without testing the whole program or subroutine and (B) the program can be easily refactored.

In term of maintainability, the separation of concerns helps us isolate the program by concern.

Page 23: How to write maintainable code without tests

Suppose we have the code to parse CLI arguments and configuration files.

import argparse, json

def main(): parser = argparse.ArgumentParser() parser.define('config_path') args = parser.parse_args()

with open(args.config_path, 'r') as f: config = json.load(f)

# ...

if __name__ == '__main__': main()

Page 24: How to write maintainable code without tests

Let's separate them.

import argparse, json

def define_parser(): parser = argparse.ArgumentParser() parser.define('config_path')

return parser

def parse_json_config(config_path): with open(config_path, 'r') as f: config = json.load(f)

return config

def main(): parser = define_parser() args = parser.parse_args() config = parse_json_config(args.config_path)

# ...

if __name__ == '__main__': main()

Define the CLI arguments

Load and parse the JSON file

The main script

Page 25: How to write maintainable code without tests

Single Responsibility

Page 26: How to write maintainable code without tests

– Robert C Martin

“Module or class should have responsibility on a single part of the functionality provided by the software, and responsibility should be entirely encapsulated by the

class. All of its services should be narrowly aligned with that responsibility.”

Page 27: How to write maintainable code without tests

Basically, think of responsibility like

DUTYto solve one particular problem.

く(ー_ー)

Page 28: How to write maintainable code without tests

Single responsibility isolates the program by functionality.

Page 29: How to write maintainable code without tests

With proper design and implementation, any components of a computer program can be easily maintained with minimal or no impact on other

components.

Page 30: How to write maintainable code without tests

You can use contract-based design (or Design by Contract) to define expected functionalities.

Page 31: How to write maintainable code without tests

From the the previous example where each concern is clearly separated…

import argparse, json

def define_parser(): parser = argparse.ArgumentParser() parser.define('config_path')

return parser

def parse_json_config(config_path): with open(config_path, 'r') as f: config = json.load(f)

return config

def main(): parser = define_parser() args = parser.parse_args() config = parse_json_config(args.config_path)

# ...

if __name__ == '__main__': main()

Define the CLI arguments

Load and parse the JSON file

The main script

Page 32: How to write maintainable code without tests

After separating responsibilities...

# app/cli.py import argparse

class Console(object): def __init__(self, loader): self.loader = loader

def define_parser(self): parser = argparse.ArgumentParser() parser.define('config_path')

return parser

def run(self): parser = define_parser() args = parser.parse_args() config = self.loader.load(args.config_path)

# ...

# app/loader.py import json

class ConfigLoader(object): def load(self, config_path): with open(config_path, 'r') as f: config = json.load(f)

return config

# main.py from app.cli import Console from app.loader import ConfigLoader

def main(): loader = ConfigLoader() console = Console(loader) console.run()

if __name__ == '__main__': main()

To handle user input from the terminal

To be the main application script

To handle configuration loading

Page 33: How to write maintainable code without tests

Dependency Injection (DI)

Page 34: How to write maintainable code without tests

Let’s say… We want to decouple our classes from their dependencies so that these

dependencies can be replaced or updated with minimal or no changes to the classes.

Page 35: How to write maintainable code without tests

Additionally, if the time permits, we want to test our classes in isolation, without

using dependencies, e.g., unit tests.

Page 36: How to write maintainable code without tests

Lastly, we do not want our classes to be responsible for locating and

managing dependency construction and resolution.

Page 37: How to write maintainable code without tests

Service The object you want to use

Client The object that depends on other services

InjectorResponsible for constructing services and injecting them into the clients

General terms on roles in Dependency Injection

Page 38: How to write maintainable code without tests

Previously in Single Responsibility

# app/cli.py import argparse

class Console(object): def __init__(self, loader): self.loader = loader

def define_parser(self): parser = argparse.ArgumentParser() parser.define('config_path')

return parser

def run(self): parser = define_parser() args = parser.parse_args() config = self.loader.load( args.config_path )

# ...

# app/loader.py import json

class ConfigLoader(object): def load(self, config_path): with open(config_path, 'r') as f: config = json.load(f)

return config

# main.py from app.cli import Console from app.loader import ConfigLoader

def main(): loader = ConfigLoader() console = Console(loader) console.run()

if __name__ == '__main__': main()

Page 39: How to write maintainable code without tests

# app/cli.py import argparse

class Console(object): def __init__(self, loader): self.loader = loader

def define_parser(self): parser = argparse.ArgumentParser() parser.define('config_path')

return parser

def run(self): parser = define_parser() args = parser.parse_args() config = self.loader.load( args.config_path )

# ...

# app/loader.py import json

class ConfigLoader(object): def load(self, config_path): with open(config_path, 'r') as f: config = json.load(f)

return config

# main.py from app.cli import Console from app.loader import ConfigLoader

def main(): loader = ConfigLoader() console = Console(loader) console.run()

if __name__ == '__main__': main()

This module is acting as the injector.

Page 40: How to write maintainable code without tests

# app/cli.py import argparse

class Console(object): def __init__(self, loader): self.loader = loader

def define_parser(self): parser = argparse.ArgumentParser() parser.define('config_path')

return parser

def run(self): parser = define_parser() args = parser.parse_args() config = self.loader.load( args.config_path )

# ...

# app/loader.py import json

class ConfigLoader(object): def load(self, config_path): with open(config_path, 'r') as f: config = json.load(f)

return config

# main.py from app.cli import Console from app.loader import ConfigLoader

def main(): loader = ConfigLoader() console = Console(loader) console.run()

if __name__ == '__main__': main()

Define loader as a service without dependencies

Page 41: How to write maintainable code without tests

# app/cli.py import argparse

class Console(object): def __init__(self, loader): self.loader = loader

def define_parser(self): parser = argparse.ArgumentParser() parser.define('config_path')

return parser

def run(self): parser = define_parser() args = parser.parse_args() config = self.loader.load( args.config_path )

# ...

# app/loader.py import json

class ConfigLoader(object): def load(self, config_path): with open(config_path, 'r') as f: config = json.load(f)

return config

# main.py from app.cli import Console from app.loader import ConfigLoader

def main(): loader = ConfigLoader() console = Console(loader) console.run()

if __name__ == '__main__': main()

Define console as a service with loader as the only dependency.

Page 42: How to write maintainable code without tests

# app/cli.py import argparse

class Console(object): def __init__(self, loader): self.loader = loader

def define_parser(self): parser = argparse.ArgumentParser() parser.define('config_path')

return parser

def run(self): parser = define_parser() args = parser.parse_args() config = self.loader.load( args.config_path )

# ...

# app/loader.py import json

class ConfigLoader(object): def load(self, config_path): with open(config_path, 'r') as f: config = json.load(f)

return config

# main.py from app.cli import Console from app.loader import ConfigLoader

def main(): loader = ConfigLoader() console = Console(loader) console.run()

if __name__ == '__main__': main()

As you can see, it is possible to replace loader with anything by injecting any services that satisfy the contract required by console, e.g., the replacement must have the method load whose the first argument is a string and the returning value is a dictionary.

Page 43: How to write maintainable code without tests

Sadly, there are very few dependency-injection frameworks available for Python like:

• pinject (github.com/google/pinject) • imagination (github.com/shiroyuki/imagination).

Page 44: How to write maintainable code without tests

These are a few foundations for achieving code maintainability

even if you do not have complete or usable test automation.

Page 45: How to write maintainable code without tests

There are also many interesting patterns that can help you.

For example, SOLID Design Principles, Adapter Pattern, Factory Pattern, Repository & Unit of Work Pattern etc.

Page 46: How to write maintainable code without tests

But always watch out for…

Page 47: How to write maintainable code without tests

Code Readability

When time is not on our side, as humans are not capable of remembering something forever, we have to make sure that the source code is implemented clearly, in a way that’s readable and understandable in short time.

Messy source code may psychologically make software developers feel like it is complicated even when it isn’t.

Messy Python code can make you feel like you’re reading machine code.

Page 48: How to write maintainable code without tests

def prep(u_list): return [ { key: getattr(u, k) if k != ‘stores’ else [ { ‘id’: s.id, ‘name’: s.name, } for s in s_list ] for k in (‘id’, ’fullname’, ‘email’, ‘stores’) } for u in u_list ]

Page 49: How to write maintainable code without tests

Over-engineeringFor example, early/premature/excessive abstraction, attempts to solve universal-yet-unknown problems, etc.

Page 50: How to write maintainable code without tests

Circular DependenciesSuppose two services depend on each other. This tight coupling between objects creates the "chicken & egg" situation.

The very first few problems are:

• No one know what should be constructed first. • Why are they separated in the first place?

Page 51: How to write maintainable code without tests

Contact Information• Homepage: http://www.shiroyuki.com

• GitHub: @shiroyuki

• Twitter: @shiroyuki

• 500px: shiroyuki

• Medium: @shiroyuki

• LinkedIn: jnopporn

Page 52: How to write maintainable code without tests

This link to this slide should be available soon at

https://www.shiroyuki.com/talks/201611-pycon-ca/

Page 53: How to write maintainable code without tests

Statflo is actively hiring developers who are passionate about building and owning something great.

If you'd like to hear more, we'd love to talk.

Send an email to [email protected] with “PyCon Application” in the subject line, and we'll reach out.

Looking for an opportunity to put these principles to work?