AAtsushi's Blog
Architecture

Book Notes: Introduction to Design Through Good and Bad Code

→ 日本語版を読む

Overview

Notes on important points from "Introduction to Design Through Good and Bad Code."

Some of the code written in the book will be converted to Python.

Class Design (Chapter 3)

  • Create both instance variables and methods within the class
  • Create instances. Basically don't use class methods.
  • Check for invalid values when creating an instance. If invalid, throw an exception.
    • Conditions that exclude a method's processing target, defined at the beginning of the method, are called guard clauses.
class StorePoint(object):

    def __init__(self, point: int):
        if point < 0:
          raise ValueError
        self.__point = point

  • Make instance variables immutable and prohibit reassignment (in Python, using double underscores like self.__variable explicitly indicates no modification)
  • Instead of modifying instance variables, create a new instance and return it with return
class StorePoint(object):

    def __init__(self, point: int):
        self.__point = point

    def add(self, additional_point: int):
        point = self.__point + additional_point
        return StorePoint(point)

    def show(self):
        print("Your points are ", self.__point, " pts!")

  • In cases where performance is important, allow reassignment (mutation) of instance variables as an exception.
    • Even then, avoid freely accessing instance variables from outside, and allow instance variable access only through properly operating methods.
class StorePoint(object):

    def __init__(self, point: int):
        self.__point = point

    def add(self, additional_point: int):
        self.__point += additional_point

    def show(self):
        print("Your points are ", self.__point, " pts!")

  • Instead of using primitive types like int or str, create custom classes (value objects) and pass them as arguments to prevent passing arguments in the wrong order.
class StorePoint(object):

    def __init__(self, point: int):
        self.__point = point

    def add(self, store_point_class: cls):
        point = self.__point + store_point_class.__point
        return StorePoint(point)

    def show(self):
        print("Your points are ", self.__point, " pts!")

Main Effects and Side Effects of Functions (Methods)

Main effects

  • The function returns a value

Side effects:

  • Making state changes
  • ※ Changing state outside the function. Examples: instance variables, arguments
  • ※ Changing local variables inside the function is not considered a side effect

Limit the Impact

  • Changing instance variable values makes results hard to predict and maintenance difficult
  • Ideally, receive data (state) as arguments, don't change the state, and return values as the function's return value

Static Methods (Class Methods)

  • Static methods cannot use instance variables, so data and data manipulation logic become separated, leading to low cohesion
class TestClass(object):

  @classmethod
  def TestMethod():
    pass

  • Use them for things unrelated to cohesion, such as log output methods or format conversion methods
  • Or use static methods as factory methods

Be Careful with Common Processing Classes (Common, Util)

  • Tend to put common processing methods in classes called Common or Util
  • Since data and logic are not paired together, they tend to become low cohesion

Law of Demeter

  • Should not know the internals of the objects you use

Tell, Don't Ask

  • Rather than asking other objects about their internal state (variables) and having the calling side make judgments based on that state, design it so that the calling side simply commands via a method, and the commanded side makes appropriate decisions and controls.
  • Be careful when frequently using getters/setters

Early Return

  • Eliminates nested conditional branches
  • Invert the condition and return early
  • Easy to add even when additional conditional branches are added
def not_early_return(a, b, c):
    if a >= 0:
        if b >= 100:
            if c >= 300:
                pass

    return a + b + c

def early_return(a, b, c):
    if a < 0:
        return

    if b < 100:
        return

    if c < 300:
        return

    return a + b + c

Interface

  • Can calculate and display area with show_area() without writing class-determination branches
class Circle(Shape):
    def __init__(self, radius):
        if radius < 0:
            raise ValueError
        self.radius = radius

    def area(self):
        return self.radius * self.radius * 3.14

class Rectangle(Shape):
    def __init__(self, width, height):
        if width < 0:
            raise ValueError

        if height < 0:
            raise ValueError

        self.width = width
        self.height = height

    def area(self):
        return self.width * self.height

def show_area(shape: Shape):
    print(shape.area())

show_area(Circle(2))
show_area(Rectangle(2,3))

Delegation Over Inheritance

  • Unless you handle inheritance very carefully, you'll quickly fall into tight coupling.
  • The first thing to convey is that inheritance is dangerous unless handled with extreme care — the book's stance is that inheritance is not recommended.
  • Inheritance is a mechanism introduced in many introductory books on object-oriented languages.
  • It's often used casually because it appears in introductory books.
  • However, in communities of experienced engineers, there are views that question or consider inheritance dangerous.
  • In an inheritance relationship, subclasses are heavily dependent on the structure of the superclass (superclass dependency).
  • Subclasses must constantly be aware of the superclass's structure.
  • Unless you pay very close attention to the superclass's behavior, changes to the superclass will cause bugs.
  • Delegation means assigning an instance of another class to an instance variable, and calling that instance's methods in a method.
  • For code, refer to "2. Adapter via instance (delegation)" below:

Dead Code, Unreachable Code

  • Code that will never be executed under any conditions in a conditional branch
  • Reduces readability. Has the potential to become bugs in the future.
  • IDEs have static analysis features that can detect dead code — use them

Magic Numbers

  • Numbers without explanation
  • If modifications are missed, they become bugs
  • Define them as constants

Don't Return or Pass null

  • Don't assign null to variables
  • Don't make null the return value of methods

Don't Swallow Exceptions

  • If you catch an exception with try-catch and do nothing, you lose all ability to detect errors
  • You won't be able to tell where the problem is

Naming Appropriate to the Concern

  • Instead of "product," use names matched to concerns like "ordered item," "reserved item," "shipped item"
  • "Product" would be a class used in many places and would become a huge class
  • Don't use class names like Data or Info
  • Class names like Manager, Processor, Controller tend to grow huge — be careful

Separation of Commands and Queries

  • State changes (commands) and queries (returning state) are easier to use when separated

Make Methods One Verb If Possible

  • Make methods one verb if possible. Example: add

Design Patterns in This Book

Remember the outline of the following design patterns:

  • Complete constructor
  • Value object
  • Factory method
  • Strategy
  • Policy
  • First-class collection
  • Repository pattern

Complete Constructor

  • A design pattern to guard against invalid states
  • Prevents invalid values with guard clauses in the constructor (instance creation)

Value Object

  • A design pattern to represent values as classes
  • Handles various values such as amounts, dates, phone numbers
  • Also has methods for handling those values
  • Value objects and complete constructors are almost always used together. The basic form of object-oriented design.

Factory Method

  • When there are multiple initialization (creation) logics for instances, use factory methods to prevent dispersal of initialization processing
  • With the code below, instances can be created with init, so a bit more ingenuity is needed
  • ↓ looks like it could work. Still want to find a better approach.
  • https://qiita.com/17ec084/items/1bad1d79428c6008e66b
class PointClass(object):
    '''
    Class to grant points to customers
    '''
    __instance = None
    __MIN = 0
    __STANDART_JOINING_CAMPAIGN_POINT = 0
    __PREMIUM_JOINING_CAMPAIGN_POINT = 5000

    def __init__(self, point):
        if point < self.__MIN:
            raise ValueError
        self.point = point

    # Joining gift points for standard members
    @classmethod
    def standard_joining_point(cls):
        initial_point = cls.__STANDART_JOINING_CAMPAIGN_POINT
        return cls(initial_point)

    # Joining gift points for premium members
    @classmethod
    def premium_joining_point(cls):
        initial_point = cls.__PREMIUM_JOINING_CAMPAIGN_POINT
        return cls(initial_point)

    def add_point(self, additional_point):
        addition = PointClass(additional_point)
        point = self.point + addition.point
        return PointClass(point)

def main():
    point_standard = PointClass.standard_joining_point()
    print(point_standard.point)

    point_premium = PointClass.premium_joining_point()
    print(point_premium.point)

    added_point_class = point_premium.add_point(3000)
    print(added_point_class.point)

Policy Pattern

This is easy to understand:

https://zenn.dev/ryutaro_h/articles/ed58ee31dcd2b4

First-Class Collection

Skipping.

Metrics for Evaluating Code Quality

Number of Lines in Classes and Methods

  • Classes designed to comply with the single responsibility principle generally have around 100 lines, at most 200 lines
  • The Ruby code analysis library RuboCop sets 100 lines as the upper limit for classes and 10 lines for methods

Cyclomatic Complexity

  • Cyclomatic complexity (McCabe complexity) is a metric that shows the structural complexity of code.
  • Complexity increases as conditional branches and loop processes increase, or when they are nested.
  • A cyclomatic complexity of 10 or less indicates a very good structure
  • Can be measured with analysis tools

Cohesion

  • A metric representing the strength of the relationship between data and logic within a module
  • LCOM (Lack of Cohesion in Methods) is a metric for this
  • Can be measured with measurement tools

Coupling

  • A metric representing the degree of dependency between modules
  • Can be measured with analysis tools

Tools for Evaluating Code

Code Climate Quality

  • Visualizes areas with problems in metrics such as code line count and complexity

Understand

  • Can measure metrics such as code line count, complexity, cohesion, and coupling

Visual Studio

  • Code metrics measurement feature is available