Why My Next ‘Small Script’ Starts with Objects, Not Functions
It always starts the same way.
You just need a small script.
In my case, it was “just” a helper to get a Salesforce data migration over the line. I needed to switch off flows and validation rules, loosen some picklists, load data, then switch everything back on.
So I did what most of us do: I opened a file and started writing functions.
fetch_active_flows(object_id)deactivate_flow_trigger(object_id)activate_flow_trigger(object_id)- “Just one more helper” to glue it together
The script worked. It passed a quick test. I moved on.
Weeks later, I had to run another migration, with more objects and more “things” to toggle. That’s when the cracks showed. The script was no longer “small” — and it definitely wasn’t easy to change safely.
That experience is why my next “small script” now starts with objects, not functions.
This post walks through that shift using real code: how I refactored a procedural Salesforce migration helper into an object‑oriented design built around House, Room, and Switch — and why that’s more than just clever naming.
The First Version: A Script That Grew Fangs
My original approach in oldflow.py was purely procedural. For example:
def deactivate_flow_trigger(object_id):
"""
Deactivate all active flow triggers related to the object
"""
cfv = fetch_active(object_id)
for record in cfv['records']:
flow_id = record['Id']
latest_version_id = record['LatestVersionId']
api_name = record['ApiName']
is_active = record['IsActive']
print(f"Updating...Flow ID: {flow_id}, Latest Version ID: {latest_version_id}, API Name: {api_name}, IsActive: {is_active}")
update_flowdefinition(latest_version_id,0)
def activate_flow_trigger(object_id):
"""
Activate all flow triggers related to the object from the baseline that should be active
"""
should_active = fetch_should_active(object_id)
curr_inactive = fetch_inactive(object_id)
for record in curr_inactive['records']:
flow_id = record['Id']
latest_version_id = record['LatestVersionId']
api_name = record['ApiName']
is_active = record['IsActive']
curr_version_number = record['VersionNumber']
print(f"Looping Inactive Flow ID: {flow_id}, Latest Version ID: {latest_version_id}, API Name: {api_name}, IsActive: {is_active}, Current Vn:{curr_version_number}")
for lkp in should_active:
if lkp['Id'] == flow_id:
update_flowdefinition(latest_version_id, curr_version_number)
There’s nothing “wrong” with this in isolation. It:
- Fetches active flows.
- Deactivates them before migration.
- Later, looks at a baseline and reactivates the right ones.
The problem wasn’t correctness; it was shape. Every time I added a new type of thing to control (validation rules, picklists), I ended up copying the same pattern:
- Fetch stuff.
- Loop over records.
- Do some CLI magic.
- Hope I didn’t miss anything.
The more functions I added, the harder it was to see the higher‑level intent:
“For this object, switch off everything that could interfere, migrate, then restore exactly what should be on.”
That’s when I stopped and asked: “If I rewrote this from scratch, pretending it’s not ‘just’ a script, what would it look like?”
The Mental Shift: Think in Objects, Not Steps
When I described the process out loud, it sounded nothing like my code:
- “Each Salesforce object is like a house.”
- “Flows, validation rules, picklists — they’re rooms in that house.”
- “Each individual flow or rule is a switch I can turn off and on.”
The more I leaned into that image, the clearer the structure became:
- A Switch represents anything that can be turned on/off (flow, rule, etc.).
- A Room manages a set of switches for a given object.
- A House orchestrates rooms for that object.
So I rewrote the script around those objects.
The New Shape: House, Rooms, Switches
Here’s the high‑level orchestration now:
from data import DataService
from flow import FlowRoom
from validationrule import ValidationRuleRoom
from picklist.picklist import PicklistRoom
class House():
def __init__(self, name):
self.Name = name
self.Id = self.get_object_id()
self.Rooms = self.create_rooms()
def get_object_id(self):
return DataService.get_object(self.Name)
def create_rooms(self):
rooms_dict = {}
flow_room = FlowRoom(self.Id)
rooms_dict["Flow"] = flow_room
validation_rule_room = ValidationRuleRoom(self.Id)
rooms_dict["ValidationRule"] = validation_rule_room
picklist_room = PicklistRoom(self.Name)
rooms_dict["Picklist"] = picklist_room
return rooms_dict
def visit(self):
sorted_keys = sorted(self.Rooms.keys())
for key in sorted_keys:
room = self.Rooms[key]
room.enter()
def depart(self):
sorted_keys = sorted(self.Rooms.keys())
for key in sorted_keys:
room = self.Rooms[key]
room.exit()
And the migration flow becomes:
if __name__ == "__main__":
house = House("Account")
# 1. Before migration: turn off flows, rules, picklists
house.visit()
# ... run migration ...
# 2. After migration: restore everything
house.depart()
Same job as before — but now the top‑level code is telling the story directly, not drowning in detail.
The power comes from the OO concepts under the hood: abstraction, encapsulation, inheritance, polymorphism. Here’s how they show up in this very concrete problem.
Abstraction: Separating “What” From “How”
Abstraction is about describing what you can do, without exposing how it’s done.
At the heart of this design are two abstract base classes:
from abc import ABC, abstractmethod
class Room(ABC):
def __init__(self, obj_id):
self.obj_id = obj_id
@abstractmethod
def get_switches():
return []
@abstractmethod
def enter():
pass
@abstractmethod
def exit():
pass
class Switch(ABC):
def __init__(self, Id, Name, InitialState):
self.Id = Id
self.Name = Name
self.InitialState = InitialState
@abstractmethod
def on(self):
pass
@abstractmethod
def off(self):
pass
The abstractions here are:
- A
Switchis “something that can be turned on or off.” - A
Roomis “something that can be entered (prepare) and exited (restore) for a given object.”
The main script doesn’t need to know:
- How flows are actually deactivated.
- How validation rules are disabled.
- How picklists are modified.
It only needs to know:
room.enter() # get ready (e.g., turn things off)
room.exit() # put it back (e.g., turn things on)
That’s abstraction: the calling code deals in concepts, not implementation details.
Encapsulation: Keeping the Gory Details Local
Encapsulation means bundling data and behaviour into self‑contained units.
Take FlowSwitch:
import subprocess, json, os
from interfaces import Switch, Room
class FlowSwitch(Switch):
def __init__(self, Id, Name, InitialState, CurrentState, LatestVersionId, VersionNumber):
super().__init__(Id, Name, InitialState)
self.CurrentState = CurrentState
self.LatestVersionId = LatestVersionId
self.VersionNumber = VersionNumber
def on(self):
if self.InitialState:
if self.CurrentState == False:
self.update(self.VersionNumber)
print(f"FlowSwitch-{self.Name} turned on")
else:
print(f"FlowSwitch-{self.Name} already on")
else:
print(f"FlowSwitch-{self.Name} shouldn't be on - update skipped")
def off(self):
if(self.CurrentState):
self.update(0)
print(f"FlowSwitch-{self.Name} turned off")
else:
print(f"FlowSwitch-{self.Name} already off")
def update(self, version_number):
"""
Update a flow definition record with a version number. If input vn is zero, it will be updated as inactive
If input vn > 0, it will be updated as active
"""
try:
query_res = subprocess.run(
[
"sf",
"data",
"query",
"--query",
f"select Id from FlowDefinition where LatestVersionId='{self.LatestVersionId}'",
"-t",
"--json",
],
capture_output=True,
text=True,
check=True
)
tmp = json.loads(query_res.stdout)
flow_id = tmp['result']['records'][0]['Id']
command = [
"sf",
"data",
"update",
"record",
"-t",
"-s",
"FlowDefinition",
"-i",
f"{flow_id}",
"-v",
f"Metadata='{{\"activeVersionNumber\":{version_number}}}'"
]
print(f"Command:{command}")
#result = subprocess.run(command, capture_output=True, text=True, check=True)
print(f"'{self.LatestVersionId}' successfully updated to {version_number}")
except subprocess.CalledProcessError as e:
print(f"Error updating flow: {e.stderr}")
Everything needed to manage a single flow lives in this class:
- Data: IDs, names, current/baseline state, version numbers.
- Behaviour:
on(),off(),update().
No other part of the system builds CLI commands or parses flow metadata. Outside code says:
switch.off() # for pre‑migration
switch.on() # for post‑migration
This is where OO stops being theory and starts being practical:
- When the Salesforce CLI or FlowDefinition metadata change, you update
FlowSwitch.update()in one place. - You don’t grep through a pile of functions trying to find every “just this once” CLI call.
Encapsulation keeps the messy bits in small, named boxes.
Inheritance: Sharing the Structure, Not the Hacks
Inheritance lets you define a common shape once and have specific kinds of things follow that shape.
Here:
FlowSwitchinherits fromSwitch.FlowRoominherits fromRoom.
class FlowSwitch(Switch):
# Must provide on() and off()
...
class FlowRoom(Room):
def __init__(self, obj_id):
super().__init__(obj_id)
self.switches = self.get_switches()
...
This guarantees:
-
Every switch type (FlowSwitch, ValidationRuleSwitch, etc.) will:
- Have
Id,Name,InitialState. - Implement
on()andoff().
- Have
-
Every room type (FlowRoom, ValidationRuleRoom, PicklistRoom) will:
- Be tied to an object ID.
- Implement
get_switches(),enter(), andexit().
So when you go to add the next “small thing” — say, a new kind of metadata you want to toggle — you’re not guessing how to shape it. You inherit from Switch and Room and follow the same pattern.
Inheritance here isn’t about sharing random helper methods; it’s about sharing a contract.
Polymorphism: One Loop, Many Implementations
Polymorphism is what lets you call the same method (enter, exit) on different object types and have each do what’s appropriate.
The House class leans on this heavily:
def visit(self):
sorted_keys = sorted(self.Rooms.keys())
for key in sorted_keys:
room = self.Rooms[key]
room.enter()
def depart(self):
sorted_keys = sorted(self.Rooms.keys())
for key in sorted_keys:
room = self.Rooms[key]
room.exit()
FlowRoom, ValidationRuleRoom, and PicklistRoom all implement enter() and exit() in their own way:
FlowRoom.enter()usesFlowSwitch.off()to deactivate flows.ValidationRuleRoom.enter()deactivates validation rules.PicklistRoom.enter()relaxes picklists.
But House doesn’t know (or care) which is which. It just walks rooms and calls enter()/exit().
That’s why adding a new type of switchable thing doesn’t change the migration workflow. You:
- Create
NewThingSwitchandNewThingRoomsubclasses. - Register
NewThingRoomincreate_rooms().
The loops in visit() and depart() remain untouched.
Why My Next “Small Script” Starts with Objects
After going through this, I don’t start migration tooling with functions anymore. Even if it feels like “just a script,” I pause and ask:
- What are the nouns in this problem?
- Can I give those nouns clear responsibilities?
- Where can I hide the messy details so they don’t leak everywhere?
In this case, those nouns were House, Room, and Switch, backed by solid OO concepts:
- Abstraction: the main flow talks about visiting houses and rooms, not about CLI flags or JSON structures.
- Encapsulation: each switch knows how to manage itself; the rest of the system doesn’t need to.
- Inheritance: rooms and switches share a predictable structure without copy‑pasting code.
- Polymorphism: one simple loop (
for room in Rooms) drives very different behaviours.
The end result isn’t just “more OO.” It’s code I can:
- Read quickly when a migration is urgent.
- Change safely when Salesforce changes.
- Extend without rewriting the core logic.
That’s why my next “small script” starts with objects, not functions. It’s not about impressing anyone with design patterns; it’s about not dreading the next time I have to touch the code.
And if you’ve ever opened an old “quick script” and wondered “what on earth was I thinking?”, that shift might be worth trying in your next one, too.