7 min read

Why My Next ‘Small Script’ Starts with Objects, Not Functions

An innocent “small script” for a migration grew into a tangle of fragile helper functions. Refactoring it into objects - a House with Rooms and Switches showed me how abstraction, encapsulation, inheritance, and polymorphism turn quick hacks into sustainable systems.
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 Switch is “something that can be turned on or off.”
  • A Room is “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:

  • FlowSwitch inherits from Switch.
  • FlowRoom inherits from Room.
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() and off().
  • Every room type (FlowRoom, ValidationRuleRoom, PicklistRoom) will:

    • Be tied to an object ID.
    • Implement get_switches(), enter(), and exit().

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() uses FlowSwitch.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:

  1. Create NewThingSwitch and NewThingRoom subclasses.
  2. Register NewThingRoom in create_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.