javascriptoopdesign-patternsdaosolid-principles

One DAO per entity vs multiple DAO's per entity


Sorry for the long question. I have tried my best to articulate my thoughts as best I can. I am slightly confused with the common practice I hear of one repository per entity. Does this mean we have a Dao class which targets a specific entity whether that entity be a table or a column in our sql database ?

Won’t this Dao class then have multiple methods where only 1 or a subset of methods are used during different events ( requests from the client) especially if some of my business logic is done on the database using joins and sub queries. Only a subset of these methods could be used at any one time depending on the event that takes place.

In my game of monopoly for example I have a Dao class which targets the players money which consists of this :

module.exports = class PlayerMoneyDAO {
constructor() {}

static async deductMoneyFromPlayer(amount, playerId, connection {
 // this simpler method is normally called when completing more logic within the app server 
  try {
    await connection.execute(`UPDATE players SET Money = money - (?) WHERE id = (?) `, [amount, playerId]);
  } catch (e) {
  throw new Error("server problem with deducting a players money on the database");
  }
}

static async addMoneyToPlayer(amount, playerId, connection) {

  try {
    await connection.execute(`UPDATE players SET Money = money + (?) WHERE id = (?) `, [amount, playerId]);
  } catch (e) {
  throw new Error("Failed to inccrease the money for the receiving player");
  }
}

static async moneyAdjustmentToPayStationRent(connection) { // this is they type of logic I do in the database server so the queries tend to be more complex
  try {
    await connection.execute(`UPDATE stations as stationLanded
    INNER JOIN board ON stationLanded.board_id = board.id
    INNER JOIN players as playerLanded ON playerLanded.board_id = board.id
    INNER JOIN game_logic ON playerLanded.id = game_logic.player_turn
    INNER JOIN players as playerOwner ON stationLanded.owner_id = playerOwner.id
    INNER JOIN stations_rent
    set playerOwner.money = CASE WHEN playerLanded.money >= stations_rent.rent_price THEN playerOwner.money + stations_rent.rent_price
    else playerOwner.money end,
    playerLanded.money = CASE WHEN playerLanded.money >= stations_rent.rent_price THEN  playerLanded.money - stations_rent.rent_price
    else playerLanded.money end
    WHERE stations_rent.number_of_stations_owned = (
      SELECT count(id) from stations WHERE owner_id = stationLanded.owner_id);`);
  } catch (e) {
  throw new Error("player money updates failed when landing on an owned station");
  }
}

static async moneyAdjustmentToPayPropertyRent(connection) {
  try {
    await connection.execute(`UPDATE players AS playerLander
    INNER JOIN game_logic ON playerLander.id = game_logic.player_turn
    INNER JOIN board ON playerLander.board_id = board.id
    INNER JOIN properties ON properties.board_id = board.id
    INNER JOIN rent ON rent.property_id = properties.id
    INNER JOIN players as playerOwner ON properties.owner_id = playerOwner.id 
    SET playerLander.money = CASE WHEN playerLander.money >= rent.rent_price THEN    playerLander.money - rent.rent_price
    else playerLander.money end,
    playerOwner.money = CASE WHEN playerLander.money >= rent.rent_price THEN playerOwner.money + rent.rent_price
    else playerOwner.money end
    WHERE properties.rent_price_point = rent.rent_price_point_value;`);
  } catch (e) {
  throw new Error("player money updates failed when landing on an owned property");
  }
}
 static async checkIfUpdatesOccured(connection) {
  const result = await connection.execute("SELECT ROW_COUNT() AS affected_rows");
  return result[0][0].affected_rows;
}

 static async getMoneyOfPlayerWhomHasJustReceivedPayment(connection, id) { 
    try {
      const result = await connection.execute(`SELECT money,piece FROM players   WHERE id = (?)`, [id]);
  return result[0][0];
    } catch (e) {
      throw new Error("couldnt get the updated money of the player that was piad money so no transaction has taken place");
   }
 }

static async getMoneyUpdatesAfterRentPaid(connection, tableName) {
  const result = await connection.execute(`SELECT piece,money FROM players WHERE id = (
    SELECT player_turn FROM game_logic) 
    UNION
    SELECT playerOwner.piece, playerOwner.money FROM ${tableName}
    INNER JOIN board ON board.id = ${tableName}.board_id
    INNER JOIN players ON players.board_id = board.id 
    INNER JOIN game_logic ON players.id = game_logic.player_turn
    INNER JOIN players AS playerOwner WHERE ${tableName}.owner_id = playerOwner.id`);

   return result[0];
   }
 };

There are still some methods to be added. This Dao’s methods all target the same entity ( the players money) column.

I also have service objects which provide methods that depend on the DAO passed into the service object. The higher level mediator class has various service objects dependancies and contains the business logic of when to call methods from each service class. Each service class methods when called from the mediator access the Dao dependancy within the service object and invokes methods from it.Each Service object Dao dependancy represents the one repository per entity theory and has all the methods inside that Dao that target the players money column within the players table.

A service object example may be named something like RentPaymentManager and receives the the PlayerMoneyDAO as a dependancy. Here we only need access to the moneyAdjustmentToPayPropertyRent and getMoneyUpdatesAfterRentPaid methods. These service objects are event specific rather than have generic methods like the DAO is. Some of these service objects only contain the one method which invokes a single method from the DAO dependancy. Some the service objects may need to call 2-3 methods from the DAO like this RentPaymentManager service object. If there is a service object that includes a method I want to re use then I put this into its own class and inherit from it or use it within the composition of my larger object. However at this time each event specific service object still receives the PlayerMoney Dao in full which consists of all methods which access the database. My Service objects often dont need all the methods from the DAO but only 1 or a sub set of those methods as explained. Before I was binding DAO methods to a specific service object when instantiating the service object so it only includes methods that it actually needs from the DAO. I was even creating a generic service object which just invoked the corresponding DAO method.

Now I’m thinking should I create various different Dao repositories classes for each event that occurs or shall I stick to only having one repository class per entity and allow my event specific service object to decide what it needs from the Dao by providing methods that access the Dao ( which is now a dependancy ) from the service object? For example my RentPatmentManager class could no longer have the full single repository for the players money but instead have a DAO specific to the event that has occurred? However If I do this suddenly I have multiple repositories per entity because all the DAO classes are now event specific rather than a generic single repository. Or should me RentPaymentManager class still just receive the one DAO which would be the same DAO passed to all event specific service classes ?

So what I am asking is shall I create an inheritance hierarchy or composite hierarchy in my service class but my Dao’s that are passed into the service classes remain generic ? If I dont do it this way and gave multiple DAO's targeting the same entity then suddenly I have more than one repository per entity which goes against the advice I have received in the past ?


Solution

  • yeah, there are lot of examples and tutorials which shows that a repository should be created per an entity and I am also the adhere of this rule of thumb.

    However, sometimes it is necessary to have more methods than CRUD operations. For example, we are developing a web application for bank clients. I know that there are a lot of opinions about giving names for repositories and services, even the whole book is written about that. So feel free to suggest new names.

    Let me show an example.

    Today, our task is to allow to do the following operations for the bank clients:

    So, we are inspired by new task and want to implement this new feature. We started creating the following repository consumed by service IUserMoneyService:

    IUserMoneyRepository
    {
        bool Withdraw();
        bool Deposit();
        decimal GetBalance();
    }
    

    So far, so good. Next day, our chief says: "Guys, we need to allow for the accountant department of bank to block money of our clients and to see their history of operations". Yeah, great task looks easy. It seems we need to add just a couple of methods to IUserMoneyRepository:

    IUserMoneyRepository
    {
        bool Withdraw();
        bool Deposit();
        decimal GetBalance();
        string GetHistory(); // new methods
        bool Lock(); // new methods
    }
    

    And we will use our new methods in our service IDepartmentService. However, we have made a horrible mistake because now Department can do withdraw, deposit and get balance of our clients. Moreover, it looks like we violated interface segregation principle.

    So for complex operations, I would create a repository per service. And a service should be created per feature. So the above code would look like this:

    IUserBankAccountRepository
    {
        bool Withdraw();
        bool Deposit();
        decimal GetBalance();
    }
    

    And the service would be called like IUserBankAccountService which will use IUserBankAccountRepository to interact with database.

    Then the next feature can be named like this IDepartmentBankAccountService and repository would like this:

    IDepartmentBankAccountRepository
    {
        string GetUserHistory(); // new methods
        bool LockUserAccount(); // new methods
    }
    

    Now our repositories have only essential and necessary methods and we do not violate Interface Segregation Principle here. And it seems that Xerox company has used the approach one entity is per an interface and they had the same issue. However, Robert C. Martin, colloquially called "Uncle Bob", solved it by applying interface segregation principle.