Search code examples
ruby-on-railsoopdesign-patternsstatedecoupling

Preventing State-Coupling?


I have the following objects and relationships,

Lecture >- Tests
Test >- Questions

Business rules

When the lecture is started, a test can be given
If a test is being given, questions can be asked

Inference

Therefore questions shouldn't be asked if the lecture hasn't been started.

Question Model

class Question
  belongs_to :test
  belongs_to :lecture, :through => :test

  def ask_question
    raise "Test not started!" unless test.started?
    raise "Lecture not started!" unless lecture.started?
  end
end

So clearly the state of the question model is now coupled to the state of test and class.

When creating unit tests, to test this I need to set up all this state, which gets quite unwieldy, especially as the business cases get more and more complicated.

How can I avoid this?


Solution

  • I'm not experinced with Ruby associations, but it seems to me that somehow data model is cofused with run-time logic here.

    If I'd make a data model for question and tests, I'd want to re-use my questions across tests and also re-use prepared tests (sets of questions) across lectures. In that case I'd write something like

    class Lecture
      has_and_belongs_to_many :tests
    end
    
    class Test
      has_and_belongs_to_many :lectures
      has_and_belongs_to_many :questions
    end
    
    class Question
      has_and_belongs_to_many :tests
    end
    

    Separately from that structure I'd have some structure corresponding to real-time lectures, tests, questions and a notion of a result. A result is an attempt to answer a real-time question by a given student.

    I'd also "delegate" the check of the lecture session state to the test session. If test session cannot be started for whatever reason the question session cannot be started too.

    To unit-test a question session you will only need to mock a test session, to unit test a test session you will need to mock a lecture session, and so on.

    class Lecture_Session
      has_many :tests_sessions
      belongs_to :lecture
    end
    
    class Test_Session
      belongs_to :lecture_session
      belongs_to :test
      has_many :question_sessions
    
      def validate
        raise "Lecture not started!" unless lecture_session.started?
      end
    end
    
    class Question_Session
      belongs_to :question
      belongs_to :test_session
    
      def validate
        raise "Test not started!" unless test_session.started?
      end
    end
    
    class Result
      belongs_to :lecture_session
      belongs_to :test_session
      belongs_to :question_session
      belongs_to :student
    
      def validate
        raise "Question is not active!" unless question_session.active?
      end
    end
    

    Hope this helps.