abstraction 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,142 @@
1
+ Abstract Classes. In Ruby.
2
+ ===========================
3
+
4
+ Let's say you've got a class called `Car`. There are two subclasses of `Car`: `Convertible` and `Sedan`. And it turns out that all cars are either convertibles or sedans. (Who knew?) So really, there's no reason that a car object wouldn't be an instance of `Convertible` or `Sedan`, and in fact an object that's a direct instance of `Car` itself won't even work correctly.
5
+
6
+ class Car
7
+ def go_forward
8
+ # ...
9
+ end
10
+ end
11
+
12
+ class Convertible < Car
13
+ def door_count
14
+ 2
15
+ end
16
+ end
17
+
18
+ class Sedan < Car
19
+ def door_count
20
+ 4
21
+ end
22
+ end
23
+
24
+ How would you write `Car#doors`? You wouldn't, because unlike moving forward, that behavior isn't shared across all cars. `Car` is an abstract class: a class that should never be instantiated directly.
25
+
26
+ But what's stopping us? Nothing. And that's a problem. So let's fix it:
27
+
28
+ require 'abstraction'
29
+
30
+ class Car
31
+ abstract
32
+
33
+ def go_forward
34
+ # ...
35
+ end
36
+ end
37
+
38
+ Car.new
39
+ #> AbstractClassError: Car is an abstract class and cannot be instantiated
40
+
41
+ But:
42
+
43
+ Convertible.new # => #<Convertible:0x8fdf4>
44
+
45
+ Awesome.
46
+
47
+
48
+ Awesome? Why so awesome?
49
+ -------------------------
50
+
51
+ Ok, let's take it back a step. You've got a `Car` class with no subclasses. They haven't been necessary, and you don't want to add complexity you don't need. Good for you.
52
+
53
+ class Car
54
+ def go_forward
55
+ # ...
56
+ end
57
+ end
58
+
59
+ Cars go forward, and that's about it. Except, now some parts of the code want to know how many doors a car has. Remember, that depends on the kind of car it is, so we'll need `Car` to know about its type. And since convertibles are pretty rare in your code, you have cars be sedans by default.
60
+
61
+ class Car
62
+ attr_reader :type
63
+
64
+ def initialize(type=:sedan)
65
+ @type = type
66
+ end
67
+
68
+ def door_count
69
+ case type
70
+ when :convertible
71
+ 2
72
+ when :sedan
73
+ 4
74
+ end
75
+ end
76
+
77
+ def go_forward
78
+ # ...
79
+ end
80
+ end
81
+
82
+ Pretty soon you realize its time to refactor this puppy. You want a refactoring called [Replace Type Code with Subclasses](http://www.refactoring.com/catalog/replaceTypeCodeWithSubclasses.html "Refactoring: Replace Type Code with Subclasses"). What you end up with is the set of classes we saw at the beginning:
83
+
84
+ class Car
85
+ def go_forward
86
+ # ...
87
+ end
88
+ end
89
+
90
+ class Convertible < Car
91
+ def door_count
92
+ 2
93
+ end
94
+ end
95
+
96
+ class Sedan < Car
97
+ def door_count
98
+ 4
99
+ end
100
+ end
101
+
102
+ **The problem** is that all of your tests are passing, but none of your code is using the subclasses yet. You could probably grep or ack through your source to find all of the times you use `Car.new`; in fact, you should. But you should still be *testing* that you've done it right. Also, if `Car` is backed by an ORM, *it* might be creating `Car` objects for you.
103
+
104
+ But as we've seen, Abstraction clears that all up. Just make the class abstract...
105
+
106
+ class Car
107
+ abstract
108
+
109
+ def go_forward
110
+ # ...
111
+ end
112
+ end
113
+
114
+ ...and watch your tests fail. When they pass again, you've completed the refactoring.
115
+
116
+
117
+ Abstract Methods
118
+ ================
119
+
120
+ (Warning: this section is a bit of a tease.)
121
+
122
+ Traditionally, abstract classes are found in strongly typed languages, where the compiler makes sure they're never created by type checking. In the Ruby world, the test suite is essentially our type checker. No complier can statically prove that an abstract Ruby class will never be instantiated, but we can exercise the test suite and see if it ever happens.
123
+
124
+ Abstract classes usually have a way to notate abstract methods. These are methods which are declared in the superclass, but don't have an implementation there. A concrete subclass has to implement all of the abstract methods. Again, this is checked by the type checker.
125
+
126
+ We have an example of an abstract method above, it's just not denoted in any way: `#door_count`. Similar to the case of abstract classes, we can't prove statically that abstract methods are implemented in the concrete subclasses. We have to run the tests and see if they're defined when they're called.
127
+
128
+ **But**: if they're called and there's no implementation, we'll get a `NoMethodError` anyway. The declaration of an abstract method in the superclass is really only useful to the type checker, to tell it that, for instance, any `Car` object has a `#door_count` method. We don't have a type checker. So we don't need to declare abstract methods.
129
+
130
+
131
+ But wouldn't it be useful?
132
+ --------------------------
133
+
134
+ It would be, if it were meaningful. The problem is: what does it mean to implement a method? In Ruby you really can't know whether a method is implemented until you send it the message and see if it raises a `NoMethodError`. There's no way to determine whether a class "implements" all of its superclasses abstract methods without making assumptions like that the class doesn't use `method_missing` or that instances of the class won't get their own singleton implementations. And the nail in the coffin: there's no time when a Ruby class is done being implemented, so there's no time to check.
135
+
136
+ On the other hand, maybe there's a way to make it useful. If there is, it certainly belongs here, so drop me a line or just fork away.
137
+
138
+
139
+ Credits
140
+ =======
141
+ Written by Peter Jaros at drop.io.
142
+ Copyright 2009 drop.io, Inc.
@@ -0,0 +1,4 @@
1
+ ---
2
+ :minor: 0
3
+ :patch: 1
4
+ :major: 0
@@ -0,0 +1,37 @@
1
+ class AbstractClassError < StandardError; end
2
+
3
+ # metaid
4
+ class Object
5
+ # The hidden singleton lurks behind everyone
6
+ def metaclass; class << self; self; end; end
7
+ def meta_eval &blk; metaclass.instance_eval &blk; end
8
+
9
+ # Adds methods to a metaclass
10
+ def meta_def name, &blk
11
+ meta_eval { define_method name, &blk }
12
+ end
13
+
14
+ # Defines an instance method within a class
15
+ def class_def name, &blk
16
+ class_eval { define_method name, &blk }
17
+ end
18
+ end
19
+
20
+ class Class
21
+ def abstract
22
+ abstract_class = self
23
+
24
+ raise_if_abstract = lambda do
25
+ if self == abstract_class
26
+ raise AbstractClassError, "#{self} is an abstract class and cannot be instantiated"
27
+ else
28
+ super
29
+ end
30
+ end
31
+
32
+ meta_def :new, &raise_if_abstract
33
+ meta_def :allocate, &raise_if_abstract
34
+
35
+ nil
36
+ end
37
+ end
@@ -0,0 +1,37 @@
1
+ require 'rubygems'
2
+ require 'spec'
3
+
4
+ $: << File.join(File.dirname(__FILE__), '..', 'lib')
5
+ require 'abstraction'
6
+
7
+ describe "#abstract" do
8
+ class Animal
9
+ abstract
10
+ end
11
+
12
+ class Cat < Animal; end
13
+
14
+ it "makes a class non-instantiable via new" do
15
+ lambda {
16
+ Animal.new
17
+ }.should raise_error(AbstractClassError, "Animal is an abstract class and cannot be instantiated")
18
+ end
19
+
20
+ it "makes a class non-instantiable via allocate" do
21
+ lambda {
22
+ Animal.allocate
23
+ }.should raise_error(AbstractClassError, "Animal is an abstract class and cannot be instantiated")
24
+ end
25
+
26
+ it "lets subclasses be instantiated via new" do
27
+ lambda {
28
+ Cat.new
29
+ }.should_not raise_error(AbstractClassError)
30
+ end
31
+
32
+ it "lets subclasses be instantiated via allocate" do
33
+ lambda {
34
+ Cat.new
35
+ }.should_not raise_error(AbstractClassError)
36
+ end
37
+ end
@@ -0,0 +1 @@
1
+ --color
metadata ADDED
@@ -0,0 +1,58 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: abstraction
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Peter Jaros
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-03-19 00:00:00 -04:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description: Abstract classes for Ruby
17
+ email: peter.a.jaros@gmail.com
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files: []
23
+
24
+ files:
25
+ - README.markdown
26
+ - VERSION.yml
27
+ - lib/abstraction.rb
28
+ - spec/abstraction_spec.rb
29
+ - spec/spec.opts
30
+ has_rdoc: true
31
+ homepage: http://github.com/Peeja/abstraction
32
+ post_install_message:
33
+ rdoc_options:
34
+ - --inline-source
35
+ - --charset=UTF-8
36
+ require_paths:
37
+ - lib
38
+ required_ruby_version: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: "0"
43
+ version:
44
+ required_rubygems_version: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: "0"
49
+ version:
50
+ requirements: []
51
+
52
+ rubyforge_project:
53
+ rubygems_version: 1.3.1
54
+ signing_key:
55
+ specification_version: 2
56
+ summary: Abstract classes for Ruby
57
+ test_files: []
58
+