Skip to content

Instantly share code, notes, and snippets.

@Johnicholas
Created February 25, 2012 17:57
Show Gist options
  • Save Johnicholas/1909783 to your computer and use it in GitHub Desktop.
Save Johnicholas/1909783 to your computer and use it in GitHub Desktop.
Idea regarding styling architecture like CSS
// This is an example of a possible large-scale design idiom.
// (Basically the idea is to imitate cascading stylesheets).
//
// It's hard to give a small, comprehensible example of an
// idea intended to be useful at (medium to) large scale.
// No matter what you do, it's definitely overengineered.
// Sigh.
//
// Often, at the top of an app, or a big component,
// there is a wiring-things-together entity; it might be
// an xml document in some heavyweight dependency injection
// architecture, or it might be just a class whose job is
// to own several subcomponents and introduce them to
// one another.
//
// If you have some configuration of these subcomponents
// to do, then you might do it with mutators in this top
// dependency-injection object. However, configuration
// isn't really the same thing as architecture.
//
// So instead, why not provide a way to configure your
// architecture with a separate stylesheet object?
//
// We use the Visitor design pattern to implement the styling.
// First, we have some javascript language magic,
// which is how we're going to do inheritance.
// Based on yui2 yahoo Lang.js
Object.extend = function (subclass, superclass) {
if (!superclass || !subclass) {
throw new Error("extend failed, please check that all dependencies are included.");
}
var F = function () {}
var i
F.prototype = superclass.prototype
subclass.prototype = new F()
subclass.prototype.constructor = subclass;
subclass.superclass = superclass.prototype
if (superclass.prototype.constructor == Object.prototype.constructor) {
superclass.prototype.constructor = superclass;
}
}
// An element is an interface - it has four methods:
// 1. it needs to 'accept' an element visitor
// 2. it needs to offer a 'get' accessor that takes and returns a string
// 3. it needs to offer a 'getProperties' accessor that returns a
// string->string map
// 4. it needs to offer a 'set' mutator that takes a pair of strings
// An element visitor has one method, visitElement
// which takes an element and an array of elements
// A selector is an interface that has a 'matches'
// method that takes an element and returns a
// truthy or falsey value
// Domain is an interface
// Domain is a Component
// TODO details
// Persistence is an interface.
// Persistence is a Component.
// TODO: details
// Utilities is an interface
// Utilities is a Component.
// TODO: details
// A component is a kind of element,
// that implements set, get, and getProperties in the straightforward way,
// delegating to an internal associative array.
// It doesn't implement accept.
function Component() {
this.properties = {}
}
Component.prototype.set = function (property, value) {
this.properties[property] = value
}
Component.prototype.get = function (property) {
return this.properties[property]
}
Component.prototype.getProperties = function () {
return this.properties
}
// setId is a helper method to abbreviate some things
Component.prototype.setId = function (id) {
this.set('id', id)
return this // for method chaining
}
// setClass is a helper method to abbreviate some things
Component.prototype.setClass = function (classname) {
this.set('class', classname)
return this // for method chaining
}
// Rules is a class representing a stylesheet, a list of rules.
function Rules(representation) {
if (!representation) {
this.representation = []
} else {
this.representation = representation
}
}
// Add is a mutator.
Rules.prototype.add = function (rule_to_add) {
this.representation.push(rule_to_add)
}
// Rules meets the element visitor interface.
Rules.prototype.visitElement = function (to_visit, dependencies) {
for (var i in this.representation) {
this.representation[i].run(to_visit);
}
for (var i in dependencies) {
dependencies[i].accept(this);
}
}
// ByClass is a selector that is constructed with a string
// It's like '.h1' in CSS.
function ByClass(class_name) {
this.class_name = class_name
}
ByClass.prototype.matches = function (to_test) {
return to_test.get("class") === this.class_name;
}
// ById is a selector, that is constructed with a string
// It's like '#body' in CSS.
function ById(id_name) {
this.id_name = id_name
}
ById.prototype.matches = function (to_test) {
return to_test.get("id") === this.id_name
}
// Rule is a class, constructed with a selector.
// It's something like '.h1 { color:red; }' in CSS.
function Rule(selector) {
this.selector = selector
this.properties = {}
}
Rule.prototype.set = function (property, value) {
this.properties[property] = value
return this // for method chaining
}
Rule.prototype.run = function (element) {
if (this.selector.matches(element)) {
for (i in this.properties) {
element.set(i, this.properties[i])
}
}
}
// Renderer is an element visitor that prints out all the properties,
// thereby demonstrating that the stylesheet really did its job.
//
// Note that it keeps track of what elements its seen,
// to prevent showing the same element more than once.
function Renderer() {
this.seen = []
}
Renderer.prototype.renderElement = function (to_render) {
print("Component:")
var properties = to_render.getProperties()
for (i in properties) {
print(" " + i + ": " + properties[i])
}
}
Renderer.prototype.visitElement = function (to_render, dependencies) {
this.seen.push(to_render)
this.renderElement(to_render)
for (i in dependencies) {
if (this.seen.indexOf(dependencies[i]) === -1) {
dependencies[i].accept(this)
}
}
}
function Utilities() {
Utilities.superclass.constructor.call(this)
}
Object.extend(Utilities, Component)
Utilities.prototype.accept = function (visitor) {
visitor.visitElement(this, [])
}
// ...TODO... this would have more methods if it were not an example
function Database(utilities) {
Database.superclass.constructor.call(this)
this.utilities = utilities
}
Object.extend(Database, Component)
Database.prototype.accept = function (visitor) {
visitor.visitElement(this, [this.utilities])
}
// ...TODO... this would have more methods if it were not an example
function Business(persistence, utilities) {
Business.superclass.constructor.call(this)
this.persistence = persistence
this.utilities = utilities
}
Object.extend(Business, Component)
Business.prototype.accept = function (visitor) {
visitor.visitElement(this, [this.persistence, this.utilities])
}
// ...TODO... this would have more methods if it were not an example
function UI(domain, utilities) {
UI.superclass.constructor.call(this)
this.domain = domain
this.utilities = utilities
}
Object.extend(UI, Component)
UI.prototype.accept = function (visitor) {
visitor.visitElement(this, [this.domain, this.utilities])
}
// ... TODO... this would have more methods if it were not an example
// stylesheet is a factory function with no arguments that returns an ElementVisitor.
//
// The idea is to create an entity which is something like this (utterly fake) CSS:
// #infrastructure {
// use-iso-dates:true;
// use-auth:false;
// }
// .stable {
// use-safety-interlocks:true;
// }
// .unstable {
// generate-admin-interface:true;
// }
function stylesheet() {
return new Rules([
new Rule(new ById('infrastructure'))
.set('use-iso-dates', 'true')
.set('use-auth', 'false'),
new Rule(new ByClass('stable'))
.set('use-safety-interlocks', 'true'),
new Rule(new ByClass('unstable'))
.set('generate-admin-interface', 'true')
])
}
// architecture is a factory function that returns an element
//
// The idea is that we have some sort of big layercake architecture,
// with several components depending on one another in a standard way.
// "configuring" the layercake, rewiring the dependency structure,
// is done by different people for different reasons than the
// "configuring" the app, styling it by flipping switches and
// setting parameters that control its behavior.
// architecture is a factory function with no arguments
// that returns something that can accept an element visitor
// and it may be able to run.
//
// Utilities depends on nothing but everything else depends on Utilities
// UI depends on Domain (business)
// Business depends on Persistence (database)
function architecture() {
var utilities = new Utilities().setId('infrastructure').setClass('stable')
var database = new Database(utilities).setId('data').setClass('stable')
var business = new Business(database, utilities).setId('business').setClass('unstable')
return new UI(business, utilities).setId('ui').setClass('unstable')
}
// app is a factory function that combines the architecture with the stylesheet
function app() {
var a = architecture()
var s = stylesheet()
a.accept(s)
return a
}
// This renders the app, demonstrating that the styling really does work.
function render() {
var a = app()
var r = new Renderer()
a.accept(r)
}
// This is more how the app would actually be run,
// but it doesn't actually do anything - this is just an example.
function run() {
var a = app()
a.run()
}
// For the example, we just print out the styled app.
render()
@darius
Copy link

darius commented Feb 27, 2012

This looks like it could have two kinds of value: to save code or to move code around to match some division of responsibilities. To actually save code would need fancier conditions than the ById and ByClass ones -- such as compound conditions -- right?

@Johnicholas
Copy link
Author

Johnicholas commented Feb 27, 2012 via email

@darius
Copy link

darius commented Feb 27, 2012

I was thinking for classes you might write

function setUpAFoo(aFoo) { ... }

and call that on Foo instances. So the configurator would own a bunch of definitions like that and the architect has to import them. But for something like a compound condition in CSS you can't avoid this infrastructure anymore.

@darius
Copy link

darius commented Feb 27, 2012

(Interesting idea, BTW.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment