6

From my experience I think that having classes/models without behaviour only in my application, next to their repositories is not good OOP. But, this was the way I implemented the repository pattern. I just make everywhere I need an repository instance, to perform some actions. The result of this approach was that all my domain classes didn't have behaviour.

They were just objects holding data with no methods. My teacher said to me that I was using thin models and that I should strive to make fat models. In response to that feedback, I implemented some business logic in the classes, but I ran into some problems:

Example:

public class Movie
{
    private MovieRepository movieRepo = new MovieRepository(new MovieDbContext());
    private PostRepository postRepo = new PostRepository(new PostDbContext());

    public decimal Rating { get; set; }
    public List<Post> Posts { get; set; }

    public void Rate(User user, Movie movie)
    {
        if(movie.Rating < 0 || movie.Rating > 10) 
        {
            throw new Exception("The rating must be a digit between 0 and 10");
        }
        this.Rating = movie.Rating;
        movieRepo.RateMovie(user.Id, movie.Id, (int)movie.Rating);
    }

    public void AddPost(User user, Movie movie, string text)
    {
        int maxId = 0;
        foreach (Post p in Posts)
        {
            if (p.Id > maxId)
            {
                maxId = p.Id;
            }
        }

        Post post = new Post(maxId + 1, user, text, DateTime.Now);
        this.Posts.Add(post);

        postRepo.AddPost(user.Id, movie.Id, text, DateTime.Now);
    }


}

As you can see in the example above, I first handle some domain logic, to perform actions on the class itself and then I persist it to the database using a repository. But why am I even adding the posts to the class itself in the AddPost-method, when I handle it with a repository right after?

Is this just because you can now actually see the changes you made directly on the screen, in-memory? Or is business logic's purpose only to validate parameter's input, as shown in the movie Rate method? But these kind of exceptions can also be thrown in the repository if the repository method also checks if a digit is between 0 and 10. But i think a repository shouldn't be concerned about that. The repository only needs to translate input to database information, am I right?

But having said that, I don't understand exactly the need of performing changes on the object itself, when you handle it with a repository. Instead of that you could to this(anywhere in your application, to display the user object):

postRepo.AddPost(2, 1, "Nice movie!", DateTime.Now);
User user = userRepo.GetById(2);

What are the pros and cons of this difference?

  • 1
    Is this question the repost/rewording of your other one on SO? – kayess Jan 25 '17 at 12:59
  • No, as result of someone's advice to split up my questions in my other post, this one is about domain/business logic's purpose and the other post is about the implementation of a service layer. – Maikkeyy Jan 25 '17 at 13:02
  • I was the one recommending you that, although you might need to check with FAQ here how to structure your question properly, as in not being too broad or asking for POB stuff. – kayess Jan 25 '17 at 13:05
  • why do you have references to your repositories in your domain object? also why are you using multiple ef contexts (MovieDbContext, PostDbContext)? – Fran Jan 25 '17 at 19:36
  • @Fran, I know from people saying that references to my repositories don't belong in my domain object, but in a service layer or something. I haven't implemented that architecture yet. I'm using multiple contexts/repositories because adding a post belongs to the PostRepository and rating a movie to the MovieRepository. By the way, they are my own written contexts, not from Entity Framework if you meant that with 'ef'. – Maikkeyy Jan 25 '17 at 21:31
10

What is the purpose of domain/business logic in classes when having repositories?

This is kind of like asking:

What is the purpose of cars when we have garages?

The reason is that Business Classes and Repositories solve different problems, and therefore are different Concerns in the application. As such, they need to be in separate classes.

A Repository's main purpose is to provide a layer of abstraction between persistence and your code. Switching database vendors, or even storage mediums (database, flat file, web service, etc) shouldn't matter outside of your Repository classes.

The purpose of a Business Class is to enforce business logic.

The purpose of separating business logic from persistence logic is so you can apply business logic without worrying about persistence. Maybe you've got a data import. Unit tests then don't need a database just to validate business rules.

Think of the requirements you have now:

  1. A Movie Rating must have a User
  2. A Movie Rating must have a Movie
  3. A Movie Rating must be between 0 and 10
  4. If a User has previously rated a Movie, the rating will be changed
  5. If a User has not rated a Movie, the rating will be added
  6. A User must have a username
  7. A User has zero or more movie ratings
  8. A User can rate movies

None of these have anything to do with inserting, updating, selecting or deleting data in the database. In fact, these same rules could be applied if you switch persistence to an XML file.

Now, consider these Business Classes:

First, a silly stub for the Movie:

public class Movie
{
    public int Id { get; set; }
    public string Title { get; set; }
}

Now we know a Movie Rating is composed of three things: A user; a movie, and a number rating.

The User class:

public class User
{
    public User(string username)
    {
        // Requirement #6
        if (string.IsNullOrEmpty(username))
            throw new ArgumentNullException("username");

        Username = username;

        // Requirement #7
        movieRatings = new Collection<MovieRating>();
    }

    // Requirement #6
    public string Username { get; private set; }

    // Requirement #7
    private ICollection<MovieRating> movieRatings;

    // Requirement #6
    public IEnumerable<MovieRating> MovieRatings
    {
        get { return movieRatings; }
    }

    // Requirement #4
    public MovieRating GetRating(Movie movie)
    {
        return MovieRatings.FirstOrDefault(rating => rating.Movie.Id == movie.Id);
    }

    // Requirement #8 and #1
    public MovieRating RateMovie(Movie movie, int rating)
    {
        // Requirement #2
        if (movie == null)
            throw new ArgumentNullException("movie");

        var movieRating = GetRating(movie);

        if (movieRating == null)
        {
            // Requirement #5
            movieRating = new MovieRating(this, movie, rating);
            movieRatings.Add(movieRating);
        }
        else
        {
            // Requirement #4
            movieRating.ChangeRating(rating);
        }

        return movieRating;
    }
}

The MovieRating class:

public class MovieRating
{
    // Requirement #8 and #1
    internal MovieRating(User user, Movie movie, int rating)
    {
        // Requirement #1
        if (user == null)
            throw new ArgumentNullException("user");

        // Requirement #2
        if (movie == null)
            throw new ArgumentNullException("movie");

        // Requirement #3
        if (IsValidRating(rating))
            throw new ArgumentOutOfRangeException("rating", "Rating must be between " + MIN_RATING + " and " + MAX_RATING);

        User = user;
        Movie = moview;
        Rating = rating;
    }

    public User User { get; private set; }
    public User Movie { get; private set; }
    public int Rating { get; private set; }

    // Requirement #3
    public const int MIN_RATING = 0;
    public const int MAX_RATING = 10;

    // Requirement #3
    public static bool IsValidRating(int rating)
    {
        return rating >= MIN_RATING && rating <= MAX_RATING;
    }

    // Requirement #4
    public void ChangeRating(int newRating)
    {
        // Requirement #3
        if (IsValidRating(newRating))
            throw new ArgumentOutOfRangeException("newRating", "Rating must be between " + MIN_RATING + " and " + MAX_RATING);

        Rating = newRating;
    }
}

I've put comments in the C# code to illustrate how the Business classes (User, Movie and MovieRating) enforce business logic.

Noteworthy features of this code:

  • The constructor for the MovieRating class is marked internal restricting who can create instances of this class to code inside the same Assembly as the class.

  • The RateMovie method on the User class is public and is the only thing that creates the MovieRating objects. This ensures that you have correctly linked the right User with the movie when adding it to the private movie ratings collection

  • The User.movieRatings field is private so the User class has full control over how MovieRating's are created

  • The User.MovieRatings property is an IEnumerable<MovieRating> so that client code must call the RateMovie method on the User class in order to rate a movie for that user.

  • The minimum and maximum ratings are codified as constants on the MovieRating class

  • A static IsValidRating method is public so any code, regardless of whether or not a MovieRating object is available, has one central place to know if a rating is valid or not. Think form field validators in the presentation/web layer of your application.

  • The RateMovie method finds an existing rating and changes it, or creates a new MovieRating object if one doesn't exist (requirement #4)

  • None of these features have anything to do with how data is inserted or updated

  • In your opinion, how these classes are related with Entity classes ( dbcontext models)? I mean, MovieRating is an entity? Inherits from and entity? Contains an entity for persist operation ? – dani herrera Mar 29 at 15:28
  • @daniherrera: It really depends on the ORM you are using. Entity Framework for the .NET Framework treats your "entities" as a data transfer objects between database and application. In this case, I have no idea what the business classes would look like (I've never done this approach). Entity Framework for .NET Core gives you more robust mapping options, allowing you to map database tables directly to your business classes (e.g. Domain Models). NHibernate, which I've used more, gives you a similar level of flexibility. – Greg Burghardt Mar 29 at 17:40
  • At this moment I use .Net Core. But I see EF core closer to EF6, I don't see why EF Core allows you to map Business Objects as entities and EF6 not. Can you elaborate? Also, do you know any open business project with EF Core, business objects, business rules, ... to learn from it. Thanks and sorry about off topic questions. – dani herrera Mar 29 at 17:49

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.