skip to Main Content

I often reach a crossroad in my code when it comes to passing methods vs generic objects vs specialised objects. Let me explain.

I am using the mediator pattern. I have DAO’s which have generic methods based on a specific entity of the database it’s targeting.If a method is targeting that specific entity I am putting it in that class. Sometimes the entity targeted is a table, other times it’s a column within a table depending on whether this DAO class starts to be complex or not and needs breaking down. Then I have service type classes where it receives the DAO as the dependancy. This is an additional layer but it prevents me from passing my DAO classes directly into a mediator. This is so that I can unit test each class outside of the main meditator and call methods from each service class inside the mediator. This also allows me to potentially pick service objects which mediate together and pass that mediator into the main mediator if my dependancies start to grow.

However I often struggle when deciding upon the next step. Do I

  1. pass the whole service object to the mediator giving it access to things it never intends to use. Ive read this goes against the Least Privilidge principle. Like this

    const turn = new ServerTurnMediator(
     new DiceService(DiceDAO),
     new PlayerMoneyService(PlayerMoneyDAO)
    )
    
  2. Pass only methods from the specific service class to the mediator using and bind the methods I am passing in to the instance of the service object like this :

     const playerMoneyService = new PlayerMoneyService(PlayerMoneyDAO);
     const methodFromPlayerMoneyService = playerMoneyService.addMoneyToPlayer.bind(playerMoneyService);
    
     const turn = new ServerTurnMediator(
      new DiceService(DiceDAO),
      methodFromPlayerMoneyService
     )
    

here I pass methodFromPlayerMoneyService into the mediator class that requires it. If the mediator then needs 3 other methods from this service objects shall I repeat this binding process for each method?When is this too much ?

  1. Start to create more event specific service objects that take in the DAO but rename the service objects around what they are trying to achieve and then pass the service object in full into the mediator. The methods are cohesive and are all referenced and possibly used in specific situations within the mediator class and there are no derelict methods within the class for the given mediator its passed into. If I did this I could use either inheritance or composition to extend each event specific class so that I prevent repeating methods in different event specific classes.

    const turn = new ServerTurnMediator(
     new DiceService(DiceDAO),
     new PlayerMoneyIncreaserService(PlayerMoneyDAO)
    )
    

This also feels like I am adhering to SRP more, but suddenly I will need a lot more classes specific to each event that occurs rather a single service object.

  1. I could do the same as 3 but also create the same inheritance hierarchy or composition within my DAO classes so that the correct service object receives the DAO that has access to the functionality the service object is trying to achieve.

I am using interfaces where appropriate and my service classes often receive different DAO implementations with the same named methods or abstract methods.

Is there a right or wrong answer here or any specific moments that scream out for refactoring ? If so then why ?

2

Answers


  1. I would stay with the third option because it is a way to encourage use SOLID principles in code.

    This also feels like I am adhering to SRP more, but suddenly I will
    need a lot more classes specific to each event that occurs rather a
    single service object.

    yeah, you are right. However, when your code has less lines of code, it is:

    • easier to read it for future readers (this person can also be you in two weeks)
    • easier to make changes in it
    • easier to write tests
    • easier to give name for methods and classes

    If I did this I could use either inheritance or composition to extend
    each event specific class so that I prevent repeating methods in
    different event specific classes.

    yeah, you are right. You can read more here when to use composition or inheritance

    Login or Signup to reply.
  2. What are you asking is probably interfaces. An interface as an abstract class with all methods being abstract.

    In JS you can implement any interface mocking as you want.

    In this fast implementation I don’t use abstract classes. If you need abstract classes as interfaces just throw any error on calling an not-overridden interface’s method through Proxy.

    So you provide interfaces to your mediators and they are restricted to use only provided interfaces.

    Note that returning bound to this methods isn’t good since you can re-bound it. That relates to your second approach too. So a more complex Proxy or object with smart prototypes should be returned from getInterace().

    class Int1{
      method1(){
        console.log('method1 of Int1');
      }
    }
    
    class Int2{
      method2(){
        console.log('method2 of Int2');
      }
      method3(){
        console.log('method3 of Int2');
      }
    }
    
    function implement(self, ...interfaces){
    
      let proto = self;
      while(proto = Object.getPrototypeOf(proto)){
        if(Object.getPrototypeOf(proto).constructor === Object){
          break;
        }
      }
      for(const int of interfaces){
        Object.setPrototypeOf(proto, proto = new int);
        proto = Object.getPrototypeOf(proto);
      }
      Object.defineProperty(self, 'getInterface', {
      get(){
        return function(int){
          let proto = self;
          while(proto = Object.getPrototypeOf(proto)){
            if(proto.constructor === int){
              const props = Object.getOwnPropertyDescriptors(Object.getPrototypeOf(proto));
              delete props.constructor;
              const selfProto = Object.getPrototypeOf(self);
              for(const name in props){
                if(Object.hasOwn(selfProto, name)){
                  props[name] = selfProto[name];
                }
                return new Proxy(self, {
                  get(target, prop){
                    if(prop in props){
                      if(typeof self[prop] !== 'function'){
                        throw new Error('Requested interface prop isnt a function');
                      }
                      return self[prop].bind(self);
                    }
                    throw new Error('Method ' + prop + ' doesn't exists on interface ' + int.name);
                  }              
                });
              }
              break;
            }
          }
          return null
        }
      }
    });
    }
    
    class Test{
      constructor(){
        implement(this, Int1, Int2);
      }
      method1(){
        console.log('overridden method1() of Int1');
      }
      method2(){
        console.log('overridden method2() of Int2');
      }
    }
    
    const test = new Test;
    test.method3();
    const int = test.getInterface(Int1);
    int.method1();
    int.method2();
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search