Skip to main content
Ben Nadel at InVision In Real Life (IRL) 2019 (Phoenix, AZ) with: David Fraga
Ben Nadel at InVision In Real Life (IRL) 2019 (Phoenix, AZ) with: David Fraga

Domain Models Expose Behavior, Not State

By
Published in Comments (9)

A couple of weeks ago, I read an "anti-patterns" ORM (Object-Relational Mapping) blog post by Mehdi Khalili. In the post, Khalili said something that I found very interesting (paraphrased):

The Domain Model should never be in an Invalid state because it doesn't expose state - it only exposes behaviors.

As part of my journey towards an understanding of Object-Oriented Programming (OOP) and Model-View-Controller (MVC) architecture, I've been playing around with building a Task/ToDo application. This application is so small in scope that it's easy to just start thinking about the data and the data persistence; but, what about the behaviors?

To explore the statement above, I wanted to try and build a Doman Model for Tasks that didn't have any "Setters". I wanted to see what it would look like if all model mutations had to be done through behavior-oriented method calls.

A Task (in my context) is a relatively simple concept. And, if I were going to create a database table for Tasks, it would probably have the following columns:

  • id
  • description
  • isComplete
  • dateCompleted
  • dateCreated

Now, the data-oriented part of my brain would see this list of columns and say, "Great, now I just need to create a Doman Entity with Getters and Setters for each of these columns." But this would expose the state of the object. And, in doing so, would likely push any task-related behavior out of the component and into the calling code.

To explore this new approach, I created a JavaScript class (ie. "Newable" object) that didn't expose any setters:

<!doctype html>
<html>
<head>
	<title>Domain Models Expose Behavior, Not State</title>

	<script type="text/javascript">


		// I model a Task that can be completed.
		function Task( id, description, isComplete, dateCreated, dateCompleted ){

			// Store internal, private properties.
			this._id = id;
			this._description = description;
			this._isComplete = isComplete;
			this._dateCreated = dateCreated;
			this._dateCompleted = dateCompleted;

		}

		// Define class propertiers.
		Task.prototype = {

			// I set the new description.
			changeDescription: function( description ){

				this._description = description;

			},


			// I complete the task.
			complete: function(){

				// Make sure the task isn't already complete.
				if (this.isComplete()){

					throw( new Error( "TaskAlreadyComplete" ) );

				}

				// Flag as complete.
				this._isComplete = true;
				this._dateCompleted = new Date();

			},


			// I determine if the task is completed.
			isComplete: function(){

				return( this._isComplete );

			},


			// I determine if the task is open.
			isOpen: function(){

				return( !this.isComplete() );

			},


			// I re-open a completed task.
			open: function(){

				// Make sure the task isn't already open.
				if (this.isOpen()){

					throw( new Error( "TaskAlreadyOpen" ) );

				}

				// Flag as open.
				this._isComplete = false;
				this._dateCompleted = null;

			},


			// -- Acccessors for output and persistence. -- //


			getDateCompleted: function(){
				// ..
			},


			getDateCreated: function(){
				// ..
			},


			getDescription: function(){
				// ...
			},


			getID: function(){
				// ...
			},


			getIsComplete: function(){
				// ...
			}

		};


		// -------------------------------------------------- //
		// -------------------------------------------------- //


		// Create our existing task.
		var task = new Task( 1, "Buy flowers", false, "2012/07/02", null );

		// Mess with task.
		console.log( "Task open?", task.isOpen() );
		task.complete();
		console.log( "Task open?", task.isOpen() );
		console.log( "Task complete?", task.isComplete() );

		// Try to close the task twice in a row.
		try {
			task.complete();
		} catch (error){
			console.log( "Error:", error.message );
		}


	</script>
</head>
<body>
	<!-- Left intentionally blank. -->
</body>
</html>

As you can see, there are no obvious setters in this JavaScript class. There are ways to change the state of the class instance; however, this is only done through behaviors that coordinate the change of the internal state. When we run the above code, we get the following console output:

Task open? true
Task open? false
Task complete? true
Error: TaskAlreadyComplete

There is a changeDescription() method, which basically does what a setDescription() mutator would have done. But, using a slightly different terminology, I feel like I have provided a behavior rather than a naked mutator. While this change may seem rather superficial, I think it is profound in its mindset: rather than providing a way to change the state, I'm providing the domain entity with a "reason" to change.

I have no idea if this is a sane approach; but, I do like the idea of delaying the creation of "Setters" for as long as possible. I think doing so will help me think about why my domain model will actually need to change without simply assuming (by default) that every property of an entity should be changeable.

Want to use code from this post? Check out the license.

Reader Comments

19 Comments

Does this thinking line up with a mindset that the domain model has public and private properties? Or maybe external and internal might be better terminology?

Or is the idea to not think about the composition of the domain model at all and instead ONLY think about the actions of the entities and what internal properties those action affect?

116 Comments

In general this is a good way to think about it. However, in the real world, one must sometimes sacrifice purity for efficiency. A decision like this (to have no getters or setters) can have wider implications.

Just for example, what if you need to serialize the entitiy to JSON or AMF? Without getters and setters, there's no way to do it. Which then leads you over to needing to create DTOs to represent the state of the object. So now you've got at least one additional class to create and maintain (and possibly more depending on how granular you make the DTOs). Which means you need manual or automated code to construct and deconstruct DTOs to send. Which means the entities need some public method that knows how to return a DTO for the current state, and to take a DTO and update the current state (since there aren't getters and setters for something eternal to the object to call to build the DTO). But in that case, can't someone who *really* wants to change the state of the object just do it with a DTO? How far do you go to protect the developers from themselves?

It's definitely a good idea to think about the domain model in terms of behavior. I'm just pointing out that a really strict adherence to some of these ideas can lead to an unexpectedly large amount of extra work to maintain that level of encapsulation. The extra work might be acceptable and worth it if you're really concerned that some other developer will misuse the object. But a lot of the time it can be hard to justify, and you may have to settle for being pragmatic about it.

19 Comments

Just realized I should have been saying "persistence model" instead of "domain model". This is what I get for posting before reading the originating source material.

11 Comments

Your example works up until you need to update the task, then all bets are off. It's also a bit simplistic, in that a task may also have a priority, a flag, tags, relate to the individual responsible, to the individual assigning the task, have a type, and on.

You can get away with your single instantiation method when you have 4 fields. When you have 30, however...

One approach some people have been taking is MOVE, which takes MVC and spins things out differently, adding Operations and Events. Operations can be likened to Behaviors, with the advantage that operations can know more things about the system.

A complete task operation, for example, might know how to integrate the task model with the notification system, sending an email to the assignee that the task has been completed.

Which, of course, limits the model's knowledge of the rest of the system and helps maintain encapsulation.

15,841 Comments

@Justin,

I think the point is to think more about the interface, and less about the data being encapsulated.

@Brian,

I didn't mean to imply that I was any "getter". And, in retrospect, perhaps leaving them commented-out in the code was a bad idea. I put them in with "//..." notation just to say that they were there but that their implementation was not worth noting (ie. everyone knows how to return properties).

At some point data needs to be rendered; and so, data needs to be accessed. I have no problem with the Controller or other Domain Models (ex. Reporting services) being able to access the data.

The only real point of this, at least for me, was to force myself to think less about data by putting off "setter" methods for as long as possible (and to use as few as necessary).

If I don't I could quite easily see myself using procedural code like:

if (form.isCompleted == false){
 
	task.setIsComplete( false );
	task.setDateCompleted( "" );
 
}

... where I am clearly putting my business logic into my Controller (ala, how I've been doing it for the last 10 years).

@Michael,

I can definitely see that being able to update any field is going to be necessary for some kinds of entities. No doubt! And, the more properties that an entity has, the more likely that is probably the case.

The real mind-shift for me was to not create setters by default. It's like asking a student to do as much as they possibly can before they have to ask the teacher - they may have to ask eventually; but, don't ask as your first move.

That said, from a lot of what I've read lately, it seems that you want to keep things simple. If you have an entity that has 30 or 40 fields, it sounds like that would be the exception to the rule, as opposed to the rule.

Of course, that opinion is just based on presentations that I've recently watched - not from personal experience (as we can all see).

The MOVE stuff sounds very much in alignment with what I've been referring to as "Application Services" or "Workflow Services" in my recent research. These would coordinate the various parts of a workflow that surround the altering of the domain model. This feels like a really nice way to look at the system; but, again, I haven't had much chance yet to put the rubber on the road.

11 Comments

Ben, I'd amend that to as simple as possible, but no simpler.

Take something like an order record. It could point to a customer record for name and address information, or it could store a copy of all of the name and address information, which in turn increases the number of fields dramatically.

In the first instance, having two distinct entities makes each one simpler, but you then have to manage and maintain the relationships, which introduces a different kind of complexity.

Normalization would argue for separate records, but then again business logic might argue for maintaining a complete copy of the name and address information as it existed at the time of that particular order.

Setters could be defined as needed, but really, something as simple was a back-end admin form is going to require them eventually, yes?

As to your other example, moving information back and forth between views and controllers is what a controller is for, after all. And strictly speaking, business logic shouldn't be in the model, either. (From a DTO standpoint.)

Also, how often do you need to do the if (form.isCompleted == false) logic? If once, then going by your previous rule you're going to add a boatload of behaviors to your model. Incomplete. Completed. Etc.

As Brian mentioned earlier, real-world constraints tend to have an impact on rules for the sake of rules.

15,841 Comments

@Michael,

An Order is also an interesting thing because you don't necessarily want to maintain "ongoing" relationships with the data. By that I mean that you may want a "Snapshot" of the relationship at the time of the order. Just because a user's "address" changes doesn't mean that it should change, retroactively, in orders. So, definitely a seeming denormalization of the data makes a lot of sense... which, as you are saying, can increase the number of fields dramatically.

On a tangential point, Orders is one of the examples that people always bring up in terms of "Document Databases" (ie. NoSQL) for storing related information all in one place. But that's neither here nor there.

So maybe, the take-away from all of this is simply to try to create non-setters first, as a means to think about behavior... then, create all the getters and setters anyway.

And, since CF9+ makes synthesizing getters/setters as easy as adding some CFProprety tags, it's really not even a big deal.

@Mehdi,

I like the Revealing Module Patterns because it allows for private values. But, there's something I just find so comforting about the "newableness" of a function constructor.

That was how I was taught JavaScript; so, it's probably just what will forever be stuck in my mind :)

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel