-
MARK MENARD: So, many thanks to the organizers
-
here at RailsConf. This is my first time
-
talking at RailsConf. It's, frankly,
-
kind of intimidating to be up here
-
and see so many people out there.
-
My name is Mark Menard. I'm gonna be talking
-
about small code today. And I've got a lot
-
of code. About seventy-nine slides. A hundred
and thirty-seven
-
transitions. Not quite as much as Sandi had,
but
-
it's a lot to get through. So let's get
-
going.
-
So, I'm just gonna let this quote up there
-
sink in.
-
So. All of us have that file filled with
-
code that you just don't want to open. As
-
you heard earlier, maybe it's your User class.
That
-
class that has comments like, woe to ye who
-
edit here. The problem with this code is that
-
it does live forever. It encapsulates business
logic that
-
ends up getting duplicated elsewhere, cause
no one wants
-
to go in there and look at that code.
-
It's also very hard to understand.
-
I'm gonna be talking about ways to avoid this
-
situation. I'm gonna be talking about code.
Code at
-
the class level and the method level. Having
small
-
code at the class and method level is fundamental
-
to being able to create systems that are composed,
-
composed of small, understandable parts.
-
I'm gonna lay out a few base concepts so
-
that we can start with a clean sheet and
-
on the same page. I think there's a lot
-
of problems with what people conceive of as
small
-
or well-designed code.
-
It's not about the actual amount of code you
-
write, but how the code is organized and the
-
size of the units of code. Fundamentally,
writing small
-
code is really design discipline, because
the only way
-
you can write small code is use good design
-
and refactoring.
-
Design and refactoring the way we write small
code.
-
You can't just sit down and write small code,
-
perfectly well-designed code on the first
draft. It doesn't
-
work that way. It's iterative process.
-
So what do I mean by small? It's not
-
about total line count. Well-designed code
will typically have
-
more lines of code than bad code. Just the
-
overhead of declaring methods and classes
is gonna increase
-
your line count.
-
It's not about method count. Well-factored
code's gonna have
-
more, smaller methods. It's not about class
count. Well-designed
-
code is almost definitely going to have more
classes
-
than what I call undesigned code.
-
Although I've seen some cases of over-abstraction,
I find
-
that's pretty rare unless someone goes pattern
crazy. So
-
small code is definitely not about decreasing
the number
-
of classes in your system. It's about well-designed,
it's
-
about well-designed classes that aren't poorly
designed.
-
So what do I mean by small? Small methods,
-
small classes. Small methods are the foundation
of writing
-
small code. Without the ability to decompose
large methods
-
into small methods, we cannot write small
code. And
-
without small methods, we can't raise the
level of
-
abstraction.
-
To write small code, we have to be able
-
to decompose large classes into smaller classes,
and abstract
-
responsibilities out of them and separate
them on higher-level,
-
and base them on higher-level abstractions.
-
It's important that our classes are small,
because small
-
classes are what lead to reusability and composability.
-
So, why should we strive for small code? Why
-
is it important?
-
We don't know what the future is going to
-
bring. Your software requirements are going
to change. Software
-
must be amenable to change. Any system of
software
-
that's going to have a long, successful life,
is
-
going to change significantly. Small code
is simply easier
-
to work with than large, complex code. If
the
-
requirements of your software are never gonna
change, you
-
can ignore everything that I have to say here.
-
But I doubt that that's the case.
-
We should write small code because it helps
us
-
raise the level of abstraction in our code.
It's
-
one of the most important things we do to
-
create readable, understandable code. All
good design is really
-
driving toward expressing ubiquitous language
of our problem domain
-
in our code.
-
The combination of small methods and small
classes is
-
going to help us raise that level of abstraction
-
and express those higher-level domain concepts.
-
We should also write small code so we can
-
effectively use composition. Small classes
and small methods compose
-
together well. As we compose instances of
small objects
-
together, our systems will become message-based.
In order to
-
build systems that are message-based, we have
to use
-
delegation. And small, composable parts. Small
code makes small
-
composable parts. It's gonna help our software
have flexibility
-
and lead to a suppleness over time, and allow
-
us to follow those messages. And eventually
we're gonna
-
see, find our duck types.
-
And all this is about enabling future change.
And
-
accommodate the future requirements without
a forklift replacement.
-
So the goal: small units of understandable
code that
-
are amenable to change.
-
Our primary tools are extract method and extract
class.
-
Longer methods are harder to understand than
short methods.
-
And most of the time, we can shorten a
-
method simply by using the abstract method
refactoring. I
-
use this thing all the time when I'm coding.
-
And once we have a set of methods that
-
are coherent around a concept, then we can
look
-
to abstract those into a separate class and
move
-
the methods to that new class.
-
So, I'm gonna be using the example of a
-
command line option parser that handles booleans
to start
-
with, and then we're gonna see where the future
-
takes us.
-
So, with the command line, I want to be
-
able to run some Ruby program dash v. And
-
handle boolean options. That's where we're
gonna start.
-
In my Ruby program, I want to define what
-
options I'm looking for, using this simple
DSL. And
-
then I want to be able to consume it
-
like this. If options.has and then a particular
option,
-
I do something.
-
Putting it all together. The DSL, the program
at
-
the top, the DSL, and then how we actually
-
consume that options object. Pretty simple.
-
Here's my spec. It's pretty simple. It's true
if
-
the option is defined and it's present on
the
-
command line. And it's false if it's not.
-
So I run my specs and I get two
-
failures. Yes, I used TDD. So, here's my implementation
-
that fits on one slide. Pretty simply, I store
-
the defined options in an array, and I store
-
the arguments, the argv for later reference.
Then I
-
have a has method that checks to see if
-
the option is defined. If it's present in
the
-
argv.
-
And then I've got my option method, which
implements
-
my simple DSL. Nice and readable. Fits on
one
-
slide. Probably very comprehendable.
-
So I run my tests. Zero failures. They pass.
-
I'm done. I get to go home until the
-
future comes along. And my workmate comes
along and
-
says, hey, I really like that library. But,
could
-
we handle string options?
-
Sounds pretty simple. Pretty straightforward.
So I think about
-
that, and I come up with a small extension
-
to the DSL, to just pass a second argument
-
as an option with a symbol representation
of the
-
option type. String, in this case.
-
I also default to being a boolean so I
-
don't have to change the code that other people
-
have done.
-
So, a string option. It's a little different
than
-
a boolean. It actually requires content. So
now I
-
need the concept of validation. If the string
option
-
is missing the content, it's not valid. There's
no
-
string there.
-
So, then I'm gonna normalize how I get the
-
values out of both those string options and
those
-
boolean options. You know, that value. This
is gonna
-
change the API, but sometimes you actually
need to
-
break the API to enable the future.
-
And I'm doing it pretty early. I've only got
-
one guy in my office using the library at
-
the moment. So, again, putting it all together.
I
-
can pass the options on the command line.
I
-
define the options with the DSL, and here's
how
-
I use my valid? and my value methods to
-
find out, get, find out if it's valued and
-
get my values out.
-
So, now here's the class that implements it.
Again,
-
on one slide. Probably not as readable. Probably
not
-
as comprehensible. We're going down what I
call kind
-
of the undesigned path. It's not too big.
Thirty-one
-
lines. But it's got issues.
-
It's got a method that's definitely large.
One that's
-
looking on the verge of being large. It's
got,
-
for only handling booleans and strings, it
has quite
-
a bit of, of conditional complexity in it
already.
-
And as we're soon gonna see, it's not very
-
amenable to change.
-
So we'll look at the pieces and how they
-
work, just so yo understand it.
-
That's my initialize method. It creates a
hash to
-
store the options. Because we have to store
the
-
type now, not just that we have an option.
-
It's either boolean or string. And the rest
of
-
the initialization is the same as it was before.
-
And the valid method, we gotta iterate over
the
-
options, looking to see which ones are strings.
So
-
we're doing checking on type here. And, and
trying
-
to see whether they're present and they actually
have
-
content.
-
Currently, string options are the only ones
that need
-
to validate. Boolean options, there's nothing
really to validate.
-
Either it's there or it's not. No validation.
But
-
strings, we have to.
-
And the value method, it does a lot of
-
stuff. Let's just pretend for a moment this
method
-
is a black box. We're gonna come back to
-
it later. Cause this is, by far, the worst
-
code in this current example.
-
But, everything is spec'd. And all my specs
are
-
green.
-
So let's talk about methods. Cause we've got
some
-
big ones and we need to clean them up.
-
I call it the first rule of method, of
-
methods. Do one thing. Do it well. Do only
-
one thing.
-
Harkens back to that Unix philosophy of tools
that
-
you string together with standard in, standard
out. But
-
how do we determine if a method is actually
-
only doing one thing? This is where your level
-
of abstraction and the abstract, abstractions
in your code
-
come into play. And you need to develop a
-
feel for this over time. That you want one
-
level of abstraction per method.
-
If all of our statements are the same level
-
of abstraction, and they're coherent around
a purpose, then
-
I consider that to be doing one thing. Doesn't
-
mean it has to be one line in a
-
method.
-
I can't tell you how many times I've looked
-
at code and seen a comment on a method
-
that was, like, an excellent description of
what the
-
method did, and if you just took those words,
-
bound together, they'd make a fantastic method
name. But
-
yet the method is named something else that
isn't
-
that descriptive. So use descriptive names.
It's really critical.
-
And the fewer arguments, the better. My personal
goal
-
is zero arguments on methods. One is OK. Two
-
or three. That's when I start to think I've
-
probably missed an abstraction in my code
and I
-
should go back and look at it.
-
Separate queries from commands. If you query
something and
-
it looks like a query method and it changes
-
the state of your object, it's hard to reason
-
about, and people who consume your library
will be
-
confused by that. So separate those.
-
And, don't repeat yourself. I know Sandi talked
about
-
this earlier, and it does take some judgment
to
-
know when it is time to remove the repetition.
-
But you don't want to leave repetition over
the
-
long term, because it will come back to bite
-
you.
-
So, let's look at our methods. We've got repetition
-
here. Both valid? and value are digging through
the
-
argv array to find the options from the command
-
line. This is the perfect candidate for an
extract
-
method, abstraction. Refactoring.
-
We have magic constants scattered around,
and those are
-
a strong indication that we've missed something.
An abstraction.
-
We're violating some other rules. It's hard
to say
-
either of these methods is really doing one
thing.
-
The code is definitely not at the same level
-
of abstraction. Values digging, valid? is
digging into the
-
argv array and value is figuring out different
divergent
-
types and how to return their values. So we're
-
gonna eliminate some of the repetition with
the extract
-
method refactoring.
-
The extract method refactoring entails moving
a part of
-
a method into a new method, with a descriptive
-
name, that's the naming part. And then calling
the
-
new method.
-
This, this refactoring helps us keep the level
of
-
abstraction consistent in the method we're
abstracting from. Here
-
we have one expression on a method that's
a
-
high level of abstraction, and two statements
that are
-
a low level of abstraction. So we move the
-
less abstract code to a new method with a
-
descriptive name, and then we call the new
method.
-
And this results in the old method, method
having
-
a consistent level of abstraction. So back
to our
-
CommandLineOptions class, both valid? and
value are digging through
-
the argv collection to find the option value.
So
-
we're gonna abstract that code and get the
raw
-
value out of argv.
-
Then we call the method from where the original
-
logic was abstracted. Pretty simple. But now
the code
-
left behind in valid? and value says what
I
-
want. Not how to do it.
-
The how has been moved to the abstracted method,
-
raising the level of abstraction just a little
bit
-
in valid? and value.
-
I'm going to do two more abstractions. I've
abstracted
-
the string option value method and the abstract
content
-
method. The naming of the abstracted methods
is very
-
important. They say what they do.
-
But overall, I'm not happy with this code.
It
-
is more explanatory, but it's fairly complex
and hard
-
to understand. It's also not as small as it
-
could be. The methods are large because I
missed
-
an abstraction. And we're gonna go find that
now.
-
I'm referencing the option type symbol to
see if
-
it's a string, which, that's a big smell.
Then
-
there are the magic constants used to dig
into
-
the argv element to find the constant within
that
-
particular string, the substring. If I was
confident that
-
I'd have no future added requirements for
this class,
-
I might leave this alone. It works. It's tested.
-
Until my buddy comes to me and says, hey,
-
I really like that library, but could we handle
-
integers now?
-
I could keep driving down this undesigned
path I've
-
been following, and complicate the valid?
and value methods
-
by switching on the type of the option and
-
digging into those argv elements to find the
value.
-
But, this is our chance to make a break.
-
And make our code more amenable to change.
-
But, to illustrate the point, I'm gonna show
you
-
that undesign method, to show you the OO design
-
actually matters. So we're gonna look at this.
-
This is the undesigned, non OO version of
this
-
code. Is it horrible? I'll leave that to you
-
to decide. Is it small? In my opinion, definitely
-
not. It is not small, by any measure. The
-
class is growing due to changes in specification.
The
-
valid? and value methods are being changed
in lock
-
step. That's a sure sign we've missed an abstraction
-
or a duck type. And those methods are getting
-
big and complicated. And now they're doing
even more
-
things.
-
And we're just doing booleans, strings, and
integers. Not
-
that much.
-
The code has tests. They all pass. That's
good.
-
But it's not satisfying. We've got those large
methods
-
and complex conditional logic. It's time to
refactor now.
-
To make the change easy.
-
And now we've got the tests that are back,
-
so we can do it without fear.
-
And, I want to call your attention to a
-
pattern that clearly emerges when we go down
the
-
non OO path here. We see checking the option
-
type and divergent behavior based on the type.
Don't
-
reinvent the type system. If you have ducks,
let
-
them quack.
-
In this example, the option types of boolean,
string,
-
and integer, those are our ducks. And I'll
bet
-
there's ducks in your code yearning to be
free.
-
And just a further confirmation that we're
dealing with
-
an abstraction or a duck, we see the testing
-
option type again in the value method. Hidden
inside
-
the valid? and value method, there's a case
statement
-
here. It just didn't evolve that way as I
-
was writing the code.
-
I'm gonna show you that. You're gonna see
that
-
it's really clear now.
-
Now it should be really obvious what the duck
-
type is. If you have case statements like
this
-
in your code, you've missed an abstraction.
Here, again,
-
we clearly see the duck type.
-
Now, I would guess, if I was writing this,
-
as soon as I had the string type, I
-
would have gone down the OO path. I just
-
wanted to illustrate to you what an undesigned,
non
-
OO mess you can get yourself into if you
-
keep riding the horse until it's dead.
-
My dad had a saying hanging on his wall
-
in his office. When the horse is dead, get
-
off.
-
But sometimes we don't realize the horse is
dead
-
and we just keep trying to go. Now it's
-
time to take a fresh look at this. So,
-
since class is the fundamental organizational
unit we have
-
to work with, it's time to work at what
-
constitutes a good class. Which principles
are gonna lead
-
us to be able to write small classes.
-
So, how do we write small classes? To make
-
small classes, I think, and this is not just
-
my opinion. It's a lot of peoples' opinion.
The
-
most important thing we should assure is that
our
-
class has one responsibility. And that it
has small
-
methods.
-
All the properties of a class should be cohesive
-
to the abstraction that the class is modeling.
If
-
you have properties that you only use in one
-
or two methods, that's probably something
else that shouldn't
-
be in there.
-
Finding a good name for a class will also
-
help us keep it focused on a single responsibility.
-
I sometimes talk to the class. Have you ever
-
heard the concept of talking to the rubber
duck?
-
Or just explaining your problem to someone?
They don't
-
even have to respond, and it helps you figure
-
it out.
-
Sometimes I just ask my class, hey class,
what
-
do you do? And if it comes out with
-
a long list, you've got a problem.
-
So, the main tools we're gonna use to create
-
new classes from existing code, not from scratch,
but
-
from existing code, is the extract class and
move
-
method refactorings, which we're gonna go
through here.
-
So, those characteristics of well-designed
class. Single responsibility. Cohesive
-
around a set of properties. Additionally,
it has a
-
small public interface that, preferably, handles
a handful of
-
methods at the most. That it implements a
single
-
use-case, if possible, and that the primary
logic is
-
expressed in a composed method.
-
That last one, I'm not gonna be covering the
-
composed method. That's a whole nother talk.
But you
-
should check that practice out. It can really
clarify
-
code and make it much, much more understandable.
-
So, let's look at the code we should have
-
been driving towards as soon as the string
option
-
type showed up. We're gonna imagine right
now that
-
we have a string sheet, and we can write
-
CommandLineOptions the way we would have with
the knowledge
-
that we have now.
-
That needs to support boolean, string, and
integer options.
-
And remember, we have our tests at our back,
-
making sure that we don't break anything.
-
And, here was my first take at it on
-
what I'd write. The class is twenty-eight
lines long.
-
It is cohesive around the properties. When
we're done,
-
most of the methods are gonna deal with the,
-
the hash of options and the array of args.
-
It has a single primary responsibility. Manage
a collection
-
of option objects.
-
So now we've introduced a collaborator. It
also manufactures
-
the option objects, which I could abstract
to another
-
class. But for the moment, I'm gonna leave
it.
-
If I find it hurts in the future, then
-
I'll change it. That's my general rule. My
guideline.
-
Is I refactor when it hurts. When making a
-
change hurts, that's the time to refactor.
-
My CommandLineOptions class has a small public
interface. Just
-
two methods, valid? and value. And it has
no
-
hard-coded external dependencies yet. I could
mess that up
-
and introduce those, but we're gonna avoid
that.
-
Another interesting characteristic is that,
is that there are
-
no conditional statements in this class, and
we're gonna
-
keep it that way. In Sandi Metz's 2009 Gerupo??
-
talk, on the Solid Principles, she said something
along
-
the lines of, a conditional in an OO language
-
is a smell.
-
And that's a really powerful statement. I
don't think
-
Sandi's saying that we can't use conditionals
in our
-
code, but that we use conditionals to hide
abstractions.
-
To hide our ducks.
-
The first time I saw that talk, I don't
-
even know if I heard her say it. It
-
was when I went back and rewatched it. I
-
thought, really? Then, as the years have gone
on
-
and I've been working, I've gotten to the
point
-
where I agree with her.
-
If you have a lot of conditionals in a
-
class, you have probably missed a concept
that should
-
be abstracted out of it.
-
So the initialize and option method from our
previous
-
implementation carry over unchanged. Except
that we're gonna store
-
the options in a hash instead of just the
-
type.
-
My valid? method now simply asks all the options
-
if they're valid, and the value method simply
looks
-
up the option hash and asks it for its
-
value. So, now we need to build the options.
-
We have to implement this. And this is where
-
we're gonna instantiate the objects that represent
the boolean,
-
string, and integer options.
-
So, now we have the CommandLineOption class,
we need
-
collaborators. In order to get anything done,
CommandLineOption needs
-
option classes to manage. It's gonna have
those objects.
-
So this is creating a dependency. And if we're
-
gonna create a dependency in our code, we
can
-
do it in a way that's amenable to change,
-
or we can do it in a way that's
-
gonna make it hurt in the future.
-
You don't want to depend, or, excuse me, you
-
want to depend on abstractions, ot concretions.
Depend on
-
the duck type, not the concrete type. In our
-
case, depend on the concept, the concept of
an
-
option. Not on the concrete types that implement
that
-
abstraction.
-
In our case, option is the duck type. This
-
is the abstraction that I missed earlier,
when I
-
just kept going down the conditional logic
path.
-
It's really simple. It has a valid? method
and
-
a value method. String option, integer option,
and boolean
-
option, those are the concrete implementation
of the option
-
abstraction. All they need is a valid? and
a
-
value method, and a consistent method of construction,
and
-
I can depend on the abstraction, not on the
-
concretions.
-
So, how do I do that? I could go
-
down the case statement road again and check
the
-
option type, instantiating the correct type
of the option
-
based upon the symbol. But I'm not gonna do
-
that, cause that would tie CommandLineClass
to those concrete
-
types, which is what we're trying to avoid.
-
That creates a hard dependency between CommandLineOptions
class and
-
those various classes. Instead, I'm gonna
use the dynamic
-
capabilities of Ruby to instantiate those
objects for us
-
using naming conventions. For string, we're
going to have
-
a string option. For booleans, boolean option.
Et cetera.
-
I could do this even in many static languages.
-
So this isn't something that's specific to
Ruby. And
-
this is a very. This very simple change takes
-
out CommandLineOption class from depending
on those concrete implementations
-
and flips it to depending on the abstraction.
-
This is dependency inversion from the Solid
Principles, in
-
practice. Alternately, some other people have
suggested, you could
-
use a hash and map from the string, boolean,
-
and integer symbols to the concrete classes,
kind of
-
like what Sandi did in her Gilded Rose Coda??
-
solution earlier.
-
That's OK. But, it is an additional thing
that
-
I have to maintain over time. It's a reason
-
to open the CommandLineOptions and change
it if I
-
have to add a new type of option. If
-
using the dynamic ability of Ruby bothers
you, then
-
make a hash. Personally, I'm fine with using
the
-
dynamic capabilities of my language.
-
So, in my case, I've inoculated CommandLineOptions
class from
-
needing to change to support new option types.
And
-
at this point, this class should be closed
for
-
modification, but open for extension.
-
So, now we need to move the logic for
-
the various option types to the appropriate
option classes.
-
I decided to make a base class of option
-
for my concrete types to inherit from, because
the
-
manner of initialization needs to be the same
for
-
all of them. No sense of repeating that code.
-
And the subtypes have a cohesion around the
flag
-
attribute, and the wrong, excuse me, the flag
and
-
the raw value properties that in the code.
-
Here's the boolean option. This one I just
wrote
-
because the requirements are so simple. Booleans
are always
-
valid, and they just return the raw_value
from the
-
command line. If it's present, it's truthy.
If it's
-
nil it's falsey. Very simple.
-
But now we need to implement string option
and
-
integer option. And the logic for their validation
and
-
value extraction is in the old CommandLineOptions
class. So,
-
on the left are the original CommandLineOptions'
valid? and
-
value methods. On the right are those new
string
-
option and integer option classes.
-
As you can see, the process of creating the
-
option class was simply picking apart and
disassembling the
-
old command line option class. Moving the
logic to
-
where it belongs, using a combination of extract
class
-
and move method refactorings, we've really
cleaned up the
-
command option, CommandLineOptions.
-
Frankly, there's not much code left there
anymore. So,
-
now we can replace that nasty, hard to understand
-
valid? method with this. And the large value
method
-
with this.
-
To create the specs for the various option
classes,
-
I moved the corresponding section from the
CommandLineOptions spec
-
to the corresponding area for the particular
type of
-
option, and then lightly reworked them and
then I
-
worked them from red to green, as I went
-
through the process of extracting those classes
and moving
-
the code to those methods.
-
We've isolated abstractions here. And how
do we do
-
that? We separate the what from the how, like
-
we've done in CommandLineOptions. We want
to move from
-
code that looks like this to code that looks
-
like this.
-
The original CommandLineOptions' valid? method
contained all of the
-
how. The refactored valid? method says what
we want
-
done for us. That's it. All of the how
-
has moved to the collaborators of our main
class,
-
in this case, StringOption, boolean option,
and IntegerOption.
-
We want to move from that looks like this
-
to code that looks like this. Move the nitty
-
gritty details of your code out to the leaves
-
of your system. And let the center be a
-
coordinator.
-
So, when we're done with this, this is what
-
our CommandLineOptions class looks like. These
are our public
-
methods. It provides a very small surface.
And it
-
fulfills the use case. And these are the private
-
implementation cruft. It's necessary, but
no one really needs
-
to go poking around in here, and I've made
-
it obvious by declaring these methods private.
-
They're for me. Not for you.
-
So in the end, the sum total of the
-
implementation of the public interface, and
it's all delegated.
-
All delegated.
-
So in the process of making the specs pass,
-
I commented out that dreamed up code as I
-
went through the process, and then one by
one
-
I wrote the examples and uncommented the code
and
-
made them pass, working from red to green.
-
Then, because nothing is ever really done,
my buddy
-
says, hey. Any chance you could add the ability
-
for me to pass an array of values for
-
an option?
-
So, to implement this new requirement, I only
need
-
the new array option class. So I write a
-
spec example. Make it fail. Then create the
ArrayOption
-
class, and I'm done. And this particular example,
my
-
OptionClass is inheriting from the OptionWithContent
superclass. And, cause
-
I actually went through this and realized
that strings,
-
integers, and arrays all have content, so
I abstracted
-
that superclass and, in this case, all I have
-
to do is write the value method of that
-
particular type and I'm done.
-
And it works.
-
So, we now have a CommandLineOption class
that's closed
-
for modification, but open for extension.
I could all
-
float types, decimal types, other types of
options, and
-
I don't have to go back and touch that
-
class again.
-
We have small, easy to understand option classes
that
-
have a single responsibility. Oops, excuse
me.
-
We can. So, we have a easy to understand
-
option classes that have a single responsibility,
and easy
-
to compose together with that CommandLineOption
class. And we
-
can simply create new option types and have
them
-
instantiated by convention.
-
My name is Mark Menard. My company's Enable
Labs.
-
We do full lifecycle business productivity
and sass app
-
development, from napkin to production, as
I say. And,
-
I'm gonna be around the conference, so let's
get
-
together and talk about some code.
-
And we can do some questions.