from urllib3.filepost import writerfrom os import write

Week 1 (August 2026)

This page contains my work log for August 1 - 7, 2026.

Tasks

Saturday, August 1, 2026

Sunday, August 2, 2026

TIL: SOLID Principles

Today, I want to recall my knowledge in the SOLID principles. Long time ago when I worked at Bukalapak, my team always encourage this principle when building software for our data platform. Therefore, I want to recall it so that I always remember how to create a good software.

Single Repository

The official words are:

A class should have one reason to change.

My words are:

One class, one owner. If multiple teams ask for a change, and it should be changed in one class then we need to break the class.

In my works, the "S" (Single Responsibility) can be applied at the data pipeline. Details below:

class DailySalesJob:
    def fetch(self):
        # calls the sales API
        ...

    def clean(self, rows):
        # drops test orders, fixes currency
        ...

    def to_csv(self, rows):
        # formats the output file
        ...

    def notify(self):
        # sends an email when done
        ...

    def run(self):
        rows = self.fetch()
        rows = self.clean(rows)
        self.to_csv(rows)
        self.notify()

From the above script there are 4 reasons to change, and it's violated the "S" principle. Details below:

  1. The backend team changed the sales API.
  2. The analyst team update the business logic.
  3. The ops team want to change the output format to JSON.
  4. The data team want to change notification channel to Slack.

Those actions need to be addressed by modifying the DailySalesJob class. And it's definitely violate the principle.

The pain you feel later are:

  1. When two people modify this script in the same day then they will get a merge conflict.
  2. When the analyst team want to change the business rules then we need to re-test the script including the fetch sales API part.

The fix is to separate into several class and each class should only have one owner. Details below:

class SalesApiClient:
    def fetch(self):
        ...

class SalesCleaner:
    def clean(self, rows):
        ...

class CsvWriter:
    def write(self, rows):
        ...

class EmailNotifier:
    def notify(self, message):
        ...

class DailySalesJob:
    def __init__(self, client, cleaner, writer, notifier):
        self.client = client
        self.cleaner = cleaner
        self.writer = writer
        self.notifier = notifier

    def run(self):
        rows = self.client.fetch()
        rows = self.cleaner.clean(rows)
        self.writer.write(rows)
        self.notifier.notify("daily sales done")

The DailySalesJob class still exists, and it's only for ordering the methods. The analyst want to change the business login, then they only need to modify the SalesCleaner class. And so on and so forth.

Open/Closed

The official words are:

Software should be open for extension, but closed for modification. Open for extension means can add a new behavior. Closed for modification means can add a new behavior without modify the old code that already works.

My words are:

New feature should be in a new class, not in if-else inside a working file.

The example is the Exporter class below, If I want to add an Avro format then I need to modify the if/else within this class. And, it's definitely violates the rule.

class Exporter:
    def write(self, rows, fmt, path):
        if fmt == "csv":
            # write csv
            ...
        elif fmt == "json":
            # write json
            ...
        elif fmt == "parquet":
            # write parquet
            ...
        else:
            raise ValueError(fmt)

The pain I feel later are:

  1. re-test the other format if I added new format.
  2. get merge conflict if more than one people modify the file.
  3. probably introduce new switch for compression, extension, etc. missed one then there will be alert at 2 a.m.

To address this separate the writer into several class and call it with abstraction. Details below:

from abc import ABC, abstractmethod

class Writer(ABC):
    @abstractmethod
    def write(self, rows, path):
        pass

class CsvWriter(Writer):
    def write(self, rows, path):
        pass

class JsonWriter(Writer):
    def write(self, rows, path):
        pass

# add Parquet Writer class
# add Avro Writer class

class Exporter:
    def __init__(self, writer: Writer):
        self.writer = writer

    def run(self, rows, path):
        pass

WRITERS = {
    "json": JsonWriter,
    "csv": CsvWriter,
    # add parquet
    # add avro
    # add ocr
}    
exporter = Exporter(WRITERS["json"])

Please note that, do not implement this if there is no use case to write into multiple format. Instead, limit the Exporter class to only support one format. Moreover, If there is a need to support another format then we can start to refactor the class with above style.

Liskov Substitution

The official words are:

If S is a subtype of T, then objects of type T can be replaced with objects of type S without breaking the program.

My words are:

Anything that claims to fill the contract must actually keep the promise.

The example is below, the script looks find until we run it. The path ("/path/to/dir") will make the script raise ValueError. Details below:

from abc import ABC, abstractmethod

class Writer(ABC):
    @abstractmethod
    def write(self, rows, path):
        pass

class S3Writer(Writer):
    def write(self, rows, path):
        if not path.startswith("s3://"):
            raise ValueError("path must be starts with s3://")
        pass

class JsonWriter(Writer):
    def write(self, rows, path):
        pass

class Exporter:
    def __init__(self, writer: Writer):
        self.writer = writer

    def run(self, rows, path):
        self.writer.write(rows, path)

rows = None
path = "/path/to/dir"
Exporter(writer=S3Writer).run(rows=rows, path=path)

If you address it with the following script, then you

class Exporter:
    def __init__(self, writer: Writer):
        self.writer = writer

    def run(self, rows, path):
        if isinstance(self.writer, S3Writer):
            self.writer.write(rows, f"s3://{path}")
        else:
            self.writer.write(rows, path)

I

Dependency Inversion

The official words are:

The high-level module should not depend on the low-level module. Both modules should depend on abstractions.


Monday, August 3, 2026

Tuesday, August 4, 2026

Wednesday, August 5, 2026

Thursday, August 6, 2026

Friday, August 7, 2026