In the beginning was the Class. The class had fields (or instance variables or members ) and methods (or messages or member functions) . The programmer created the class and said “I created thy from nothing. Thou shalt exist and breedeth instances of thyself which would be objects”. And the class was happy and the programmer was satisfied. It was all in a day’s work for him and he rested before he created the next class. But before long he realized that he was creating too many classes and could not re-use them across projects. The class was confused about what it was supposed to be doing since all kinds of application functionality percolated into it in some form or the other.
Take any textbook of OOAD and there would be classes mentioned with the aforesaid fields and member functions. These classes would use the members to contain “state” and the associated methods to execute various operations on them. A class such as Account class would contain fields such as id, description, customerId and balance. It would also contain various operations that can be performed on the class – operations such as debitAcccount(), creditAccount(), computeInterest() and so on. But as we progressed to creating more elaborate applications we realized that a design such as the above, that appears in a textbook on encapsulation would be far too simplistic to achieve functional scalability. Functional scalability can be defined as the ability to add functionality without impacting on the existing code base in a catastrophic manner.
An Account class that has all the responsibility for maintaining accounts and performing operations on them would become the recipe for a code bottleneck. It would also tend to clutter all kinds of functionality into one accounting module (of which the class is a part) and hence hampers modularization.
So an application can only scale functionally if the responsibility can be split between multiple classes each having a single responsibility. This realization is codified into a principle called “Single Responsibility Principle”. (SRP) But how do we achieve SRP?
There are several ideas around it. But the one that I think succeeded the most is the stipulation to separate the “data container” part from the “operations” part. However, having said that, I would still urge the reader to consult patterns such as Naked Objects pattern to see the alternate viewpoint.
The data container acts as a mere carrier of data from one layer to the other. All the business logic, core algorithms, business rules and persistence logic resides in designated objects (which I call strategies after the Strategy pattern) that act on the data containers to accomplish the task of the application.
So, does this imply a regression to the C era where we had a “structure” that contained data and various methods that acted on the structure? Well the answer is “it is and it is not”. It is a regression, if viewed simplistically and it is not if this is viewed as two class hierarchies – one that contain the data and the other that contain the algorithms. The interplay between these two hierarchies is the essence of programming an application. Layered architecture would be impossible if the operations are tightly coupled with the data containers. In a layered application, all layers would not have all kinds of services available. For instance, in the UI, is is quite possible that there is no persistence service available. In the data layer, there may not be access to a UI. Combining all these disparate operations into one massive class results in a considerable amount of tight coupling even if we discount the clutter that has been caused. Multiple implementations of a persistence service for instance, would not be possible.
Below is a rough description of the two different types of classes:
Examples: Domain objects such as Account, Customer etc. Value objects such as Address. Data Transfer objects such as AccountBalanceInput, HttpServletRequest etc. Canonical data model objects such as AccountBalanceInput, AccountBalanceOutput, XML with a canonical XML Schema , JSON objects etc.
Layer Data: These classes chiefly help in carrying data from one layer to the other. Some of these classes can be specific to one layer (Ex:HttpSession would be probably specific to the UI layer) while some other objects span across multiple layers. There might be a mapping effort involved in converting these objects from one form to the other. Example: It might be required to convert back and forth from XML to Java (or .NET or any other technology that is being used)
Typical Methods: These classes contain getter and setter methods to allow access to the underlying data. They also contain identity methods (getId(), hashCode(), equals() etc.) and comparator methods(such as compareWith() etc.) to compare one object with another. In short, they contain methods that enable others to utilize the data that is contained within the class.
Creation and Instantiation: The objects of these classes are instantiated from storage or from the UI which populates them from UI forms.
Create Optimization: Multiple objects need to be created, each one with its own state (We need a different Account object for John’s Savings account and Barb’s Checking Account) . Hence these objects would need to be created for every request. Object creation can be optimized by caching. Some of these objects may be very light weight. Example: the AccountType object which can only have a few values (such as Savings account, Checking account etc.) These objects can benefit from the flyweight pattern.
Examples: A servlet. An MVC controller. A facade. A Data Access Object (DAO). Practically, any command that does not store state but executes an action.
The underlying principle is the strategy pattern. The strategy is the implementation of a certain algorithm that is usually done on the data container. The strategy itself is an implementation of an interface that fronts all similar strategies. For instance, there may be a PersistenceStrategy which is responsible for persisting the domain object. This also enables natural layer separation because more often than not, the strategy is dependent on the layer of the application. For instance, a UI Controller only functions in the front end layer while a strategy that utilizes persistence resides in the data layer.
Creation: Strategies are typically designed to be created once and used multiple times as singletons. A good implementation would create strategies when the application is initializing and then utilizes the created strategy to serve each user request.
Members: Strategies are best utilized in a “stateless” manner. Hence they do not contain data that would be specific to a user request. However, the strategy can still hold “request agnostic” state. Ex: Let us say I wanted to render data into text and hence created a Renderer command or strategy. This command can have a flag inside which indicates if the text that it creates must be XML or comma-separated. This kind of information would be independent of the user request.
Runtime Management: Strategies are created once and the created object is re-used multiple times. Hence if the created strategies can be “discovered” during runtime, then various members in the strategy can be set during runtime thereby leading us to control their behavior during runtime. This is very useful and is often exploited by technologies such as JMX (Java Management Extensions) to alter runtime behavior of the application.
Raja has been blogging in this forum since 2006. He loves a technical discussion any day. He finds life incomplete without a handy whiteboard and a marker.