from urllib3.filepost import writerfrom os import write
Week 1 (August 2026)
This page contains my work log for August 1 - 7, 2026.
Tasks
- Today I learned: SOLID Principles
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:
- The backend team changed the sales API.
- The analyst team update the business logic.
- The ops team want to change the output format to JSON.
- 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:
- When two people modify this script in the same day then they will get a merge conflict.
- 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-elseinside 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:
- re-test the other format if I added new format.
- get merge conflict if more than one people modify the file.
- 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