encapsulation violations and object boundaries
==============================================

a practical guide using real examples from kollabor-cli.
written for developers who learn by seeing the pattern in their own code.


what is encapsulation?
----------------------

an object should own and control its own state.
other objects should interact through methods (the public API),
not by directly reading/writing internal attributes.

  bad:  obj.internal_counter = 0
  good: obj.reset()

when you see yourself setting 3+ attributes on another object,
that's a code smell. the logic belongs inside that object.


the tell, don't ask principle
-----------------------------

instead of asking an object for its state, making decisions,
then telling it to update... just tell it what to do.

  bad (ask then act):
    if logger.message_count > 100:
        logger.message_count = 0
        logger.session_file = new_path

  good (tell):
    logger.reset_session(new_id)

the object knows how to reset itself. you don't need to know
its internal structure.


law of demeter (don't talk to strangers)
-----------------------------------------

a method should only call methods on:
  1. its own object (self)
  2. objects passed as parameters
  3. objects it creates
  4. its direct components

  bad:  self.api_service._profile.provider
  good: self.api_service.get_provider()

each dot (.) you chain adds coupling. if _profile's structure
changes, everything that reaches through api_service breaks.


real example: session_manager.py (before)
-----------------------------------------

  restart_session() was doing this:

    self.conversation_logger.session_id = new_id
    self.conversation_logger.session_file = ...
    self.conversation_logger.message_count = 0
    self.conversation_logger.conversation_start_time = datetime.now()
    self.conversation_logger.current_thread_uuid = None

    self.conversation_manager.current_session_id = new_id
    self.conversation_manager.messages = []
    self.conversation_manager.message_index = {}
    self.conversation_manager.context_window = []
    self.conversation_manager.current_parent_uuid = None
    self.conversation_manager.conversation_metadata = { ... }

  11 direct attribute assignments across 2 objects.

  problems:
    - session_manager knows the internal structure of both objects
    - if conversation_logger adds a new field, session_manager must update
    - the reset logic is duplicated (conversation_manager already had
      clear_conversation() doing the same thing)
    - tests had to check individual attributes instead of method calls


real example: session_manager.py (after)
----------------------------------------

    self.conversation_logger.reset_session(new_id)
    self.conversation_manager.reset_session(new_id)

  2 method calls. each object owns its reset logic.

  bonus: conversation_manager.clear_conversation() was refactored
  to delegate to reset_session(), eliminating duplication:

    def clear_conversation(self):
        if self.messages and self.save_conversations:
            self.save_conversation()
        self.reset_session(generate_session_name())


how to spot violations
----------------------

  patterns to grep for:

    self.dep.attr1 = ...      3+ assignments to same dep = violation
    self.dep.attr2 = ...
    self.dep.attr3 = ...

    self.dep._private = ...   accessing underscore attrs = violation

    obj.a.b.c                 chaining 3+ dots = law of demeter violation

  questions to ask:
    - "does this class need to know this structure?"
    - "if i add a field to X, does Y need to change?"
    - "could X do this itself?"


how to fix violations
---------------------

  1. identify the cluster of attribute assignments
  2. create a method on the target object that does the same thing
  3. name it by intent (reset_session, set_context, reload_config)
  4. update the caller to use the new method
  5. update tests to verify method calls, not internal state
  6. look for existing methods that do the same thing (DRY opportunity)


severity levels
---------------

  minor:    1-2 direct attrs on objects you constructed
  moderate: 3+ attrs or accessing private (_underscore) attrs
  serious:  5+ attrs across multiple objects
  critical: modifying internal collections (lists, dicts) directly


other violations found in kollabor-cli
---------------------------------------

  ✖ llm_coordinator.py reload_config()
    reaches into tool_executor and api_service to set timeouts/streaming.
    fix: tool_executor.reload_config(config)

  ✖ application.py initialization wiring
    sets 7+ attributes across renderer, layout_renderer, input_handler.
    fix: constructor injection or configure() methods.
    note: init wiring is sometimes acceptable in orchestrators - judgment call.

  ○ llm_coordinator.py property passthrough to _queue_processor
    8 properties that forward to queue_processor's attributes.
    this is a legitimate delegation/facade pattern, not a violation.
    the coordinator IS the public API for queue_processor state.


related concepts to explore
----------------------------

  - SOLID principles (especially Single Responsibility and Interface Segregation)
  - dependency injection (pass deps through constructors, not reaching into globals)
  - information hiding (only expose what's needed)
  - facade pattern (simplified interface over complex subsystem)
  - composition over inheritance
  - immutable value objects (dataclasses with frozen=True)

  books:
  - "Clean Code" by Robert Martin (chapters on objects vs data structures)
  - "Refactoring" by Martin Fowler (the "feature envy" smell)
  - "Object-Oriented Software Construction" by Bertrand Meyer

  the "feature envy" smell from Fowler is exactly what session_manager had:
  a method that's more interested in another class's data than its own.


testing implications
--------------------

  violation tests:
    self.assertEqual(self.logger.message_count, 0)   # tests implementation
    self.assertEqual(self.logger.session_id, new_id)  # tests implementation

  encapsulated tests:
    self.logger.reset_session.assert_called_once_with(new_id)  # tests contract

  when objects own their state, tests become:
    - simpler (fewer assertions per test)
    - more stable (don't break when internals change)
    - more meaningful (test behavior, not structure)
