The single responsibility principle (SRP) is one of the most widely followed ideals in object oriented programming. For decades, developers have been striving to ensure their classes take on just enough, but not too much, responsibility. A valiant effort and by far one of the best ways to produce maintainable code.
SRP is hard, though. Of all the SOLID design principles, it is the most difficult to embrace. Due to the abstract nature if its definition, based purely on example instead of directive process, it's hard to concretize. More specifically, it's difficult to define "responsibility", in general or in context. There are some rules-of-thumb to help, like reasons for change, but even these are enigmatic and hard to apply.
Simply put, SRP says a class should be comprised of just one responsibility, and only a single reason should force modification.
Let's take a look at the following class which clearly has three responsibilities and therefore breaks SRP:
class User < ActiveRecord::Base def check_in(location); ... end def solr_search(term); ... end def request_friendship(other_user); ... end end
This class would require churn for a variety of reasons, some of which include:
1. The algorithm for checking in changes.
2. The fields used to search SOLR are renamed.
3. Additional information needs to be stored when requesting a friendship.
So, based on the rules of SRP, this class needs to be broken out into three different classes, each with their own responsibility. Awesome, we're done, right? This is often the extent of discussions around SRP, because it's extremely difficult to provide solutions beyond contrived, minute examples. Theoretically, SRP is very easy to follow. In practice, it's much more opaque. It's too pie in the sky for my taste; like most OOP principles, I think SRP should be more of a guideline than a hard-and-fast rule.
The real difficulty of SRP surfaces when your project grows beyond 100 lines of code. SRP is easy if you're satisfied with single method classes or decide to think about responsibility exclusively in terms of methods. In my opinion, neither of these are suitable options.
DCI provides more robust guidelines for following SRP, but we need to redefine responsibility. That's the focus of this article.
OK, let's try to elucidate responsibility, but first, let's talk about object orientation.
The word "object" can be defined as a resource that contains state, behavior and identity. Its state is the value of the class's attributes. Its behavior is the actions it can perform. And its identity is...well...it's object id? It feels strange to narrow the definition of identity into a mere number. I certainly don't identify myself by my social security number. My identity is derived from my name, the things I enjoy doing, and potentially my environment. More importantly, my identity is always changing.
While building a class, when was the last time you thought about the forthcoming object ids of the instances of that class? I hope never. We don't program classes with identity in mind, yet if we're trying to model the world, it's an intrinsic component. Identity means nothing to us while building classes, yet everything to us in the real world.
Therefore, it's appropriate to say that the mental model of the programmer is to map identity to state and behavior, rather than to object id. Object id is a quality of uniqueness.
Identity is closely related to responsibility. As expressed above, I don't identify by my social security number, but by my state and behavior. When we attempt to find the appropriate location for a method definition, we look at the responsibility of the prospective classes. "Does this method fit into this class's single responsibility?" If we consider that identity should truly be a representation of an object's state and behavior, we can deduce that identity is a derivative of responsibility.
An example of this observation is polymorphism; probably the most predominant and powerful object-oriented technique. When we consider the possible duck-typed objects in a scenario, we don't think, "this object will work if its object id is within the following set..." We think "this object will work if it responds to the right methods." We rarely care about the object id. What's important is the true identity of an object: the methods it responds to.
Object ids mean nothing to programmers. Defining identity by the memory location of an object is very rarely a means for writing effective software. Computers care about memory locations, not programmers. Programmers care about state and behavior, hence, the reason we build classes.
SRP is about acutely defining state and behavior, thus, identity. In DCI, we define state through data objects and behavior through roles. Generally speaking, behavior changes state so the primary focus is on behavior. If we ask, "what can an object do?", we can subsequently ask, "how does its state change?"
We still haven't really defined responsibility. As a human being, my responsibilities change on a regular basis. At one point, I'm responsible for writing an email. At another, I'm responsible for mentoring a developer. Rarely, although occasionally, am I responsible for doing both. Enter role-based programming.
We can reprogram the example class above using DCI data objects and roles:
# Data class User < ActiveRecord::Base; end # Roles module Customer def check_in(location); ... end end module Searcher def solr_search(term); ... end end module SocialButterfly def request_friendship(other_user); ... end end
Now, each role has a single responsibility. If we define responsibility by a role played by a data object, it becomes obvious where new methods should go and when you're breaking responsibility. Let's give it a shot.
Say we want to add functionality that allows users to accept a requested friendship. First ask the question in terms of business objectives, "As a social butterfly, can I accept a friendship?" By converting our expectations into English, we can then easily map business rules to code. It would clearly be wrong if we ask, "As a searcher, can I accept a friendship?" Therefore, #accept_friendship should belong in SocialButterfly:
module SocialButterfly def request_friendship(other_user); ... end def accept_friendship(other_user); ... end end
By defining responsibility as a role, we can converge on contextual identity, the true essence of object orientation. While building a role, we are building identity, a crucial part of a programmer's mental model. Roles are the inverse abstraction of classes. While classes focus on abstracting away identity, roles focus on highlighting it. It's no wonder it's so difficult to define responsibility when we're programming classes, not object.
It's hard to define responsibility. It's even harder to program for it. As an artifact, the responsibility of a class often ends up being either too narrowly or too broadly defined. Define responsibility too narrowly, and it's daunting to wrap your head around 1000 classes. Define responsibility too broadly, and it's arduous to maintain and refactor.
By defining responsibility as a role, we have a clear notion of behavioral locality. We can ask questions like, "as a customer, can I add an item to my cart?" If the answer is yes and we've appropriately named our roles, the method belongs in that role. This gives us a means for defining responsibility, and we can refactor accordingly.
Roles won't alleviate potential clutter, but they can give us a structure for defining responsibility. With DCI, we can talk about responsibility in terms of directive process instead of contrived examples.
First, understand the business objectives of the system and subsequently understand the roles. Understand the roles and subsequently understand the responsibilities.
Posted by Mike Pack on 03/12/2013 at 08:28AM
The open/closed principle (OCP) is a fundamental "run of thumb" in object-oriented languages. It has hands in proper inheritance, polymorphism, and encapsulation amongst other core properties of object-oriented programming.
The open/closed principle says that we should refine classes to the point at which we eliminate churn. In other words, the less times we need to open a file for modification, the better. With DCI, we can compose objects while still following OCP.
Wikipedia's definition of inheritance:
Inheritance is a way to reuse code of existing objects, or to establish a subtype from an existing object, or both, depending upon programming language support.
By using #extend to modify objects at runtime, we are both reusing code from data objects while also forming a new subtype of the data object.
A typical DCI context might look like this:
customer = User.find(1) # customer is a data object customer.extend(Customer) # inject the Customer role customer.purchase(book) # invoke Customer#purchase
After calling #extend, the user object can be used as both a data object and a purchasing customer. The #purchase method likely uses attributes of the user object to create joins between him and his book. We're reusing code from its former self.
Similarly, the new object is now a subtype of its former self. That is, the Customer version of user can be used polymorphically in place of the data object itself.
The open/closed principle is often discussed in the context of inheritance; we use inheritance to adhere to the "closed" aspect of OCP. In order to follow OCP, a class can be open for extension, but closed for modification. Let's look at how the principle could be applied with classical inheritance to reimplement the above scenario.
We have a dumb data object:
class User # A dumb data object end
To abide by the "closed" aspect of OCP, we define a subtype of the User class; we do not modify the class itself:
class Customer < User def purchase(book) # Update the system to record purchase end end
Somewhere else in our codebase, we tell a user to purchase his book:
customer = Customer.new customer.purchase(book)
This is great, we've accomplished OCP by ensuring that any customer related aspects of a User are neatly tucked away in the Customer class. In order to change the behavior of a user, we formed a new class while leaving the User class alone.
This is the guts of the open/closed principle. We want to structure our classes in such a way as to ensure they never need to change. Guaranteeing the classes don't change is also a function of the method bodies.
Following OCP without incorporating the Data, Context and Interaction architecture has proven to lead to looser coupling, stronger encapsulation, and higher cohesion. Just apply it and your world will be rainbows and ponies!
Wrong. While OCP has absolutely helped in producing higher quality code, it's just another lofty object oriented principle. It's very difficult to adhere to all principles, and some may be entirely inappropriate in various scenarios. The SOLID principles (of which OCP is one) are a great frame-of-reference when discussing software design, however, heeding to them 100% of the time is frankly, impossible.
I often find it very difficult to ensure the first iteration of my core software, test suite, and ancillary code meet the qualifications of the SOLID principles. Not because I don't understand or refuse to apply them, but because I'm human and I'm working with frequently-varying business rules. Principles in general end up being this pie-in-the-sky goal; I prefer to just write software.
One of the reasons I love DCI so much is because it forces you to work in an orthogonal way. It breaks the cemented programming models we've seen for over 20 years. Models which, in my opinion, do not lend themselves towards these principles. DCI acts much like a lighthouse: guiding you towards proper object orientation.
DCI enables you to automatically apply many best practice principles in object oriented programming. The open/closed principle is one.
The whole point of DCI is to decouple what changes from what remains constant. In DCI, our data objects are strictly persistence related, and as such, do not change frequently. The way in which we use data objects is often what changes.
So, when we build out a data object...
class User < ActiveRecord::Base # A dumb data object end
...it's closed for business.
DCI tells us that if we want to add behavior to this class, we should be doing so within a role. A deliberate effect of this is that our class remains closed. OCP is telling us to optimize our classes so that we never need to modify them. This aspect of OCP is baked into the core of DCI.
The name says everything. The best way to accomplish DCI in Ruby is to use #extend. We seek to inject roles into objects at runtime to accomplish our behavioral needs. Let's create our Customer role:
module Customer def purchase(book) # Update the system to record purchase end end
We would then join our data and roles within a context:
class CustomerPurchasesBook def initialize(user, book) @customer, @book = user, book @customer.extend(Customer) end def call @customer.purchase(@book) end end
The open/closed principle states that a class should be open for extension. Within the above context, we extend our user object with the Customer role. Our DCI code adheres to this rule.
OCP talks a lot about extension of classes via inheritance. Demonstrations of OCP are usually forged with classes, instead of objects. In the above paragraph, I say that classes are open for extension, but the user object is extended. When we define a class, it's simply a container in which methods live. That container then becomes part of an object's lookup hierarchy. So, behaviorally speaking, there's no semantic difference between composing an object from scratch with DCI and creating an instance of a class.
In the customer example above, we use #extend as a means of composing the customer object to include its necessary behavior. We do this in lieu of classical inheritance. As I mentioned earlier in this article, extension is inheritance.
By applying DCI, you are ever-so-nicely nudged into following OCP. DCI is a paradigm shift, but it's coated with reward. By simply working in objects and extending them at runtime, you are guided towards many well-respected, object-oriented principles. The strong emphasis DCI puts on decoupling static classes from dynamic behavior means that your classes remain closed for modification.
DCI contexts are naturally built for OCP. Use cases rarely change. If a user is buying a book, the use case of that purchase remains relatively constant. Since contexts act as simple glue between data and roles, if a use case changes, it's likely to be a new context. In this regard, contexts remain closed for modification.
DCI won't help you properly construct your roles, but it does guide you in the right direction. Since roles are actor-based, their methods tend to be use case specific. This means that role methods don't need to accomodate for drastic variations. If variation increases, I tend to reach for service objects to abstract that complexity.
There is no silver bullet to following object-oriented principles. We're always making tradeoffs. Managing complexity is inherently complex. DCI can help you cope by ensuring your objects remain open for extension, yet closed for modification.
Posted by Mike Pack on 12/18/2012 at 08:14AM