Welcome To Obvious
Obvious is a clean architecture. The goal is to make it easy to write software using TDD and to expose the intent of the software just by looking at the file structure. By using Obvious you also gain the benefit of having a system that treats the external data sources and delivery mechanisms as implementation details that can be easily replaced as needed.
- Obvious Functionality
- Framework Independence
- Extreme Testability
- Maintenance Minded
When looking at the app directory, it should be obvious what kinds of things the application does. This architecture highly values the "glance factor" of the application structure as well as being obvious in where to find things while working with the project.
Your application does not need to be a web app, api app, desktop app, or console app. It also doesn't need to be a MySQL, MongoDB, or SQLServer app. Your app is just a set of data structures and functionality related to those data structures. How your app delivers your app or stores data are implementation details.
Implementation details can and should change based on your implementation needs, your app logic shouldn't have to change when your implementation requirements do.
This is a TDD biased structure. Each layer and pattern is designed with testing in mind. More importantly, we designed this architecture so that testing would be easier for developers, which should lead to more testing and better software quality.
When given the choice between short term productivity or long term maintenance, we believe that the right decision is long term maintenance. Many decisions have been made that are counterintuitive from a short term productivity standpoint, but allow for much easier maintenance. Obviousness, framework independence, and testability all work together to make the day to day maintenance more enjoyable.
There are three well defined folders of each Obvious project, which define the three core divisions of a given project - app, delivery, and external. In the default Obvious structure, they exist as actual directories, but they are designed to be decoupled, so there is no reason you couldn't put the app, external, or delivery mechanisms into separate gems if that made sense for your project, but for demonstration purposes we assume them to all live in the same directory.
App is where the entities, actions, and data contracts of your application are housed.
Entities represent data in your system. They are fairly simple data structures that mostly just contain data and do validation on the data they contain. Simple entities usually only need a shape method to make writing contracts easier, a populate method of populating the object, and a to_hash method for using the object elsewhere.
Actions are the use cases of the system. They are where most of the business logic of the system happens. They are single action objects that take in Jacks as constructor arguments to enable pluggable data sources.
Contracts define the data transport structures and perform format validation on the hashes. Data flows through the contracts to the jacks and plugs and back. Because the contracts validate both the input and the output, invalid data structures raise an error and actions will fail. This ensures a standard mechanism for pluggable persistence.
Delivery is where you implement the delivery mechanism of your application itself. Delivery is where your app is integrated with external data sources and shown to the user. This means in simplest terms the UI, but it also means creating concrete versions of the external objects, such as data jacks, and also calling the actions of the app itself.
If your system contains multiple delivery mechanisms - multiple web apps, api's, command line apps, etc. each project would live in a subdirectory of the delivery directory. In the example Obvious app below there is a sinatra app in delivery/web/, a sinatra json api app in delivery/api/, a purple_shoes desktop app in delivery/desktop/ and a commander cli app in delivery/cli/. You also could make your app and delivery directories into gems and have each delivery mechanism be its own directory.
External is where data transport between your app and various external infrastructure lives. This could be datbases, queues, or caching layers connected to various data systems such as Redis, MongoDB, MySQL, the filesystem etc.
Jacks are the classes that inherit from Contracts. A contract is designed to wrap a jack method so that validation on the input and output hashes can be done. Jacks don't define a particular persistance mechanism, but instead act more as a routing mechaism to various plugs.
Plugs are the objects that talk to external systems such as queues, databases, caching, 3rd party API's and so on. A single Jack can have multiple plugs that plug into it. For example, you might have a BlogPostJack that has a BlogPostPlugForMySQL and a BlogPostPlugForMongo and a BlogPostPlugForFS. In that scenario you could swap out MySQL for Mongo or the Filesystem at any time.
Hashes as Data Transport
The fundamental data structure of data transport between layers is a hash, ideally an immutable hash or struct. This is for both incoming and outgoing data from the each layer. There are many benefits to this approach. The biggest benefit is that each layer stays decoupled from the other layers and that hashes act as messages passed between the layers. Immutable hashes or structs are preferable to mutable ones so that the messages stay in their original state.
Other benefits include:
Your delivery mechanism is only handed data, so you have less likelhood of logic in your controllers or views (unless you do a bad thing). Your Jacks are only handed data, so contracts are able to validate their structure to ensure that Jacks and Plugs won't break your system. Also, Plugs only handing back data means your application isn't tied to the data structures of a particular ORM or persistence library. Lastly, entities are populated with hashes, so it is easy to validate returned from the Jacks using the Entity.populate method.
Obvious Status - A twitter clone status update app. It is designed as an example app, so you can follow the commits to get an idea of how an app would progress step-by-step.
Currently Obvious Status can be run as a web app, JSON api, command line app, or a desktop app using jruby. Data can be stored with JSON files, MySQL, MongoDB, or even the JSON api. You can pick from any of those options without changing your app folder code.
You can start using Obvious in Ruby by installing the ruby gem. Just 'gem install obvious'. To run it, call 'obvious generate' in a folder containing a "descriptors" folder with obvious yml descriptor files inside. The obvious generator will look at all of the action descriptors and will generate stub classes and pending tests for your project.
NOTE: descriptors are awesome for getting a new app folder structure going, but are basically ignored after your app is created. At some point in the future, the Obvious app generator will get a bit smarter and descriptors could be useful in an ongoing design process, but for now they make bootstrapping easier and that's it.
Example Descriptor File
An example obvious descriptor file is available as a gist. It's a yaml file. It is designed to be for generate pseudocode and related tests. the c: elements are comments and the requires: elements determine what we think that method might need for that comment.
Obvious Language Choice
For a myriad of reasons, Ruby is not really the best choice to use for Obvious, but we have done our best to make it an enjoyable experience.
If you are interested in porting Obvious to another language, contact Brian on Twitter.
The Obvious Team:
Uncle Bob Martin - The keynote at Ruby Midwest 2011 outlined the structure and ideas that became the Obvious Architecture. Uncle Bob's blog posts on The Clean Architecture and Screaming Architecture were also useful in furthering our understaanding of Clean Architecture as defined by Uncle Bob.
Alistair Cockburn - The Hexagonal Architecture ports and adapters concept directly influenced the idea of jacks and plugs in Obvious. In fact, the biggest reason they aren't named ports and adapters in Obvious is to avoid architecture confusion.