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.__variableexplicitly 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
intorstr, 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