Summary
Can you divide your logical layers into separate deployable units without changing a single line of code? You should be able to!
Advertisement
Recently I suggested that we should be separating layers into separate deployable units. Not everyone agreed with this physical separation, but I do think we can all agree that applications should be layered. So what exactly does it mean for an application to be layered? Does it mean we only layer our classes? How about our packages? Maybe something more?
To help answer these questions, let's look at some of the reasons why we layer an application. We'll assume three principle layers - user interface, domain logic, and data source. Here are some of the benefits of layering an application.
Lower layers provide a level of abstraction that helps make higher level layers easier to develop. For instance, our domain layer can obtain data from the data source layer without knowing the underlying storage and communication mechanism (RDBMS, VSAM, or external system).
We can swap lower level layers with new implementations. If we're using Vendor X today, and are switching to Vendor Y tomorrow, change is isolated to the datasource layer.
We promote consistency. Accessing our data is done the same way, through a common API, all the time.
Once a layer is built, we can use it for higher level services. In other words, we reuse our lower level layers.
Dependencies are minimized. We know that changing the UI will not cause changes to propogate to the domain and data source layers.
We modularize the application by encapsulating logic in granular layers. So when our business rules change, we know the change is isolated to the domain layer.
I suppose we could pretty easily come up with other advantages, and maybe even some disadvantages, but that's not the point here. Instead, let's consider for a moment that we at least separate layers based on class responsibility. That's a start, but I suspect it will become a maintenance burden since it's difficult to distinguish layers within a package unless we come up with a class naming convention.
So minimally, we want to separate layers at the package level, which brings a bit more distinction to the various layers. Now we can begin to think about enforcing the layering scheme. JDepend allows us to create test cases that enforce package dependencies. If we violate these dependencies, the tests fail. And to a certain extent, we've achieved some of the advantages cited above. But how are we going to reuse layers in different contexts if all layers are bundled into the same deployable unit? How easy is it going to be to swap layers with new implementations? And if everything is bundled into the same deployable unit, are we ever really certain that the test suite is sufficient enough to verify all layer dependencies? Even one violation in the dependency graph between layers means we really don't have a layered application.
Nonetheless, separating layers by package is the most common approach. We take all that good design stuff we've done to help us realize the benefits listed above, and then bundle it into deployable unit(s) that give no attention to layer boundaries. We say we have a layered application, but in reality, do we?
Alas, we can prove we have a layered application that achieves the benefits listed above. By performing a levelized build, where each layer is compiled with only the desirable dependencies in the build classpath, we can verify the application layers. We should be able to do this without changing a single line of code. Instead, we should only need to change the build script. Any violations among the various layers results in a failed build. Even if we don't presently divide layers into separate deployable units, if we have a layered application, we should be able to do so. I challenge you to try it.
When my applications need to be changed (say a user adds a new field on a screen I have to plumb this attribute through the entire stack of layers)
This tells me that layering is a poor abstraction.
I think it is better to lump all the code about a particular attribute as close to one spot as possible. If this means scattering sql throughout the application, so be it. I will change attribute information far more often than I will change databases.
If you're rigorous about applying the Dependency Inversion Principle (see writings by Robert C. Martin), then you do have reduced coupling, but not necessarily layers...
> But what do you use as the criteria to separate classes > based on responsibility? Only change?
lets say you have a program about dogs and cats. I put everything to do with dogs in one place and cats in another. Or at least I try to, it never is as simple as that. > > How do you enforce any degree of architectural or design > consistency? Don't you find lumping everything together > negatively impacts maintenance? > > You must have some method...care to share?
Well, I usually end up with a bunch of objects that each look a lot like their respective database table. I also end up with a bunch of servlets/jsps that corresponds to each page the user sees.
Once you do that, the rest of the architecture of the application is pretty well done.
> >>If this means scattering sql throughout the application, > > >>so be it. > Don't you ever want to reuse one of those SQL statements?
Why would I reuse that query? Most of the time, my sql is really simple and is attached to the corresponding object. I don't try to build up complicated query building classes as I find it too difficult to figure out what is happening when it doesn't work.
If you can see the query, then you can see what the database is doing.
Nor do you have any sense of cohesion since everything is muddled together. And if you do separate based on ui, business logic, and data access, then you must have cycles.
In HiveMind, it is possible for a module to define a service point but not provide the implementation. Another module can provide the implementation.
So, if you can agree on service interfaces, you can have a pluggable presentation layer talk to a pluggable back end layer.
An example that HiveMind was specifically designed to address was database agnostic behavior. We had a database access objects (DAO) layer for accessing an Oracle database. We wanted to support SQLServer and PostgreSQL as well, but were making use of Oracle and WebLogic extensions.
By seperating the interfaces from the implementations, we would be able to deploy a DAO JAR that contained the proper implementation based on the matching backend ... i.e., a dao-oracle.jar, a dao-sqlserver.jar or a dao-postgres.jar.
Alas, I left WebCT long before we got to the point where we were ready to implement this, but it is fundamentally part of HiveMind.
Starting in HiveMind 1.1 (currently in alpha) we go further, and allow the implementation of a service to be overridable.
So, in summary, this is one of the advantages of a proper lightweight container ... the late binding makes it practical to seperate the layers of an application properly.
>>Well, I usually end up with a bunch of objects that each >>look a lot like their respective database table In the data access layer this makes sense, especially if you're using a tool like Hibernate. But I usually find my business object structure differs from the database structure. For instance, I might have an Employee table with a classification column on it, and I might have an EmployeeDataBean. But in the business object layer, I'll have an Employee class that contains an EmployeeClassification interface to help me hide some of the behavioral complexities when dealing with the different types of employees. Otherwise, my Employee class becomes littered with conditionals based on classification.
>>Most of the time, my sql is really simple and is attached >>to the corresponding object. It seems that would couple the object pretty tightly to the database. That impedes testing, since you can't effectively test the object without relying on data retrieved from the database. Also, if you have a composite object, you lose the flexibility to build the composite object differently, such as maybe one query per object in some cases versus a single multi-table query in others.
I'll have to dig a bit deeper into HiveMind. I'm curious though...does HiveMind actually promote separating layers into deployable units, or simply make it possible?
> It seems that would couple the object pretty tightly to > the database.
It seems to me that the object that retrieves data from a database would naturally be coupled quite closely to that database. You could add a data abstraction layer that 'hides' the database, but within that you'll still need an object that can talk to the database.
Over the years, I've found that (for ease of maintenance by coders not involved in the original development) keeping the SQL simple is more important than keeping the Java simple.
> That impedes testing, since you can't > effectively test the object without relying on data > retrieved from the database.
How do you test a data retrieval object without retrieving data. You can, sort of, but without actually retrieving data, all you can prove is that you have an object that looks like it should be able to retrieve data. In essence that is all you can expect from a unit test on such an object. Outside acedemia, we want to know if it works in practice, not in theory.
"must have cycles" -- Dependency Inversion breaks cycles of dependencies, at the cost of multiplying the number of layers. You might have cycles of objects at run-time, but a good garbage-collecting language can cope with that.
>>I've found that (for ease of maintenance by coders not >>involved in the original development) keeping the SQL >>simple is more important than keeping the Java simple.
Sometimes. But there are times when performance issues related to the heavy I/O make this not possible. If you have a 1:M relationship, in most cases you'd want to retrieve the M side with a single database hit. Or maybe you'd want to retrieve the 1 and M in a single database hit. And each record in the database maps to a single object. So you still have to build your business objects from the result set. Decoupling your business object layer from your data access layer affords you the flexibility to build your business object structure using different data retrieval mechanisms.
>>How do you test a data retrieval object without >>retrieving data. Maybe I misunderstood the previous post, but I was assuming the object that retrieved the data was also a business object. Decoupling the business object from the data access object allows you to test the business object separately from the data access object. Certainly the point of testing a data access object is to make sure it connects to the database and retrieves the information correctly. Though these tests are less valuable than those that test my behavioral logic. Keeping the two separate allows me to test my behavior without depending on the database to do it.
HiveMind applications are broken into modules; all the modules are peers of each other ... you can consider them to be layers if you wish. Or multiple modules may comprise a single layer. You can use naming conventions if you wish to imply such relationships.
Dividing your application into layers is a first-class concept in HiveMind, in that you can define a service point in terms of an interface, have code in various modules (layers) access that service and invoke methods on it, but only supply the implementation of the service in some other module. Runtime checks ensure that each service point does have a single implementation. All the modules will be on the runtime classpath (there isn't any magic) ... HiveMind dynamically binds together the static structure of your application. Simple and effective.
Making business objects substantially different from your database tables can lead to all sorts of grief (like complicated SQL and slow performance) I try and avoid the allure of Java's object modelling ability as I have to squish it into a relational database anyways and it never quite fits.
As far as testing my business objects, yes you do need a database because they are persistent objects but you need it anyways and JUnit can handle setup and cleanup nicely.
Its a matter of slicing vertically not horizontally.
>>Dependency Inversion breaks cycles of dependencies, at >>the cost of multiplying the number of layers
That was my point and the challenge. If you avoid cycles, then dependencies move in a single direction...top to bottom. And you should be able to break the various layers into separate deployable units. If you can't, then you have cycles you aren't aware of.
Flat View: This topic has 21 replies
on 2 pages
[
12
|
»
]