feedjira 2.1.0 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.rubocop.yml +9 -2
  4. data/CHANGELOG.md +4 -0
  5. data/LICENSE +1 -1
  6. data/README.md +210 -7
  7. data/Rakefile +5 -0
  8. data/feedjira.gemspec +2 -1
  9. data/lib/feedjira.rb +7 -1
  10. data/lib/feedjira/configuration.rb +76 -0
  11. data/lib/feedjira/core_ext/date.rb +1 -0
  12. data/lib/feedjira/core_ext/string.rb +1 -0
  13. data/lib/feedjira/core_ext/time.rb +5 -1
  14. data/lib/feedjira/date_time_utilities.rb +11 -3
  15. data/lib/feedjira/date_time_utilities/date_time_epoch_parser.rb +13 -0
  16. data/lib/feedjira/date_time_utilities/date_time_language_parser.rb +2 -0
  17. data/lib/feedjira/date_time_utilities/date_time_pattern_parser.rb +6 -1
  18. data/lib/feedjira/feed.rb +87 -69
  19. data/lib/feedjira/feed_entry_utilities.rb +5 -2
  20. data/lib/feedjira/feed_utilities.rb +11 -1
  21. data/lib/feedjira/parser.rb +1 -1
  22. data/lib/feedjira/parser/atom.rb +1 -0
  23. data/lib/feedjira/parser/atom_entry.rb +1 -0
  24. data/lib/feedjira/parser/atom_feed_burner.rb +19 -2
  25. data/lib/feedjira/parser/atom_feed_burner_entry.rb +1 -0
  26. data/lib/feedjira/parser/atom_youtube.rb +1 -0
  27. data/lib/feedjira/parser/atom_youtube_entry.rb +1 -0
  28. data/lib/feedjira/parser/google_docs_atom.rb +2 -1
  29. data/lib/feedjira/parser/google_docs_atom_entry.rb +2 -0
  30. data/lib/feedjira/parser/itunes_rss.rb +1 -0
  31. data/lib/feedjira/parser/itunes_rss_category.rb +1 -0
  32. data/lib/feedjira/parser/itunes_rss_owner.rb +1 -0
  33. data/lib/feedjira/parser/podlove_chapter.rb +2 -0
  34. data/lib/feedjira/parser/rss.rb +1 -0
  35. data/lib/feedjira/parser/rss_feed_burner.rb +1 -0
  36. data/lib/feedjira/parser/rss_feed_burner_entry.rb +1 -0
  37. data/lib/feedjira/preprocessor.rb +2 -0
  38. data/lib/feedjira/version.rb +1 -1
  39. data/spec/feedjira/configuration_spec.rb +25 -0
  40. data/spec/feedjira/date_time_utilities_spec.rb +6 -0
  41. data/spec/feedjira/feed_spec.rb +20 -2
  42. data/spec/feedjira/feed_utilities_spec.rb +18 -0
  43. data/spec/feedjira/parser/atom_feed_burner_spec.rb +32 -1
  44. data/spec/sample_feeds.rb +1 -0
  45. data/spec/sample_feeds/GiantRobotsSmashingIntoOtherGiantRobots.xml +682 -0
  46. metadata +49 -29
@@ -1,3 +1,5 @@
1
+ # rubocop:disable Style/Documentation
2
+ # rubocop:disable Style/DocumentationMethod
1
3
  module Feedjira
2
4
  class Preprocessor
3
5
  def initialize(xml)
@@ -1,3 +1,3 @@
1
1
  module Feedjira
2
- VERSION = '2.1.0'.freeze
2
+ VERSION = '2.1.1'.freeze
3
3
  end
@@ -0,0 +1,25 @@
1
+ require 'spec_helper'
2
+
3
+ describe Feedjira::Configuration do
4
+ describe '.configure' do
5
+ it 'sets follow_redirect_limit config' do
6
+ Feedjira.configure { |config| config.follow_redirect_limit = 10 }
7
+ expect(Feedjira.follow_redirect_limit).to eq(10)
8
+ end
9
+
10
+ it 'sets request_timeout config' do
11
+ Feedjira.configure { |config| config.request_timeout = 45 }
12
+ expect(Feedjira.request_timeout).to eq(45)
13
+ end
14
+
15
+ it 'sets strip_whitespace config' do
16
+ Feedjira.configure { |config| config.strip_whitespace = true }
17
+ expect(Feedjira.strip_whitespace).to be true
18
+ end
19
+
20
+ it 'sets user_agent config' do
21
+ Feedjira.configure { |config| config.user_agent = 'Test User Agent' }
22
+ expect(Feedjira.user_agent).to eq('Test User Agent')
23
+ end
24
+ end
25
+ end
@@ -37,5 +37,11 @@ describe Feedjira::FeedUtilities do
37
37
  expect(time.class).to eq Time
38
38
  expect(time).to eq Time.parse_safely('Wed Aug 31 14:37:00 UTC 2016')
39
39
  end
40
+
41
+ it 'should parse epoch into Time' do
42
+ time = @klass.new.parse_datetime('1472654220')
43
+ expect(time.class).to eq Time
44
+ expect(time).to eq Time.parse_safely('Wed Aug 31 14:37:00 UTC 2016')
45
+ end
40
46
  end
41
47
  end
@@ -224,8 +224,26 @@ describe Feedjira::Feed do
224
224
  parser = Feedjira::Feed.determine_feed_parser_for_xml xml
225
225
  expect(parser).to eq new_parser
226
226
 
227
- # this is a hack so that this doesn't break the rest of the tests
228
- Feedjira::Feed.feed_classes.reject! { |o| o == new_parser }
227
+ Feedjira::Feed.reset_parsers!
228
+ end
229
+ end
230
+
231
+ describe 'when parsers are configured' do
232
+ it 'does not use default parsers' do
233
+ xml = 'Atom asdf'
234
+ new_parser = Class.new do
235
+ def self.able_to_parse?(_)
236
+ true
237
+ end
238
+ end
239
+
240
+ Feedjira.configure { |config| config.parsers = [new_parser] }
241
+
242
+ parser = Feedjira::Feed.determine_feed_parser_for_xml(xml)
243
+ expect(parser).to eq(new_parser)
244
+
245
+ Feedjira.reset_configuration!
246
+ Feedjira::Feed.reset_parsers!
229
247
  end
230
248
  end
231
249
  end
@@ -26,6 +26,24 @@ describe Feedjira::FeedUtilities do
26
26
  end
27
27
  end
28
28
 
29
+ describe 'strip whitespace' do
30
+ context 'strip_whitespace config is true' do
31
+ it 'strips all XML whitespace' do
32
+ Feedjira.configure { |config| config.strip_whitespace = true }
33
+
34
+ expect(@klass.strip_whitespace("\nfoobar\n")).to eq('foobar')
35
+
36
+ Feedjira.configure { |config| config.strip_whitespace = false }
37
+ end
38
+ end
39
+
40
+ context 'strip_whitespace config is false' do
41
+ it 'lstrips XML whitespace' do
42
+ expect(@klass.strip_whitespace("\nfoobar\n")).to eq("foobar\n")
43
+ end
44
+ end
45
+ end
46
+
29
47
  describe 'instance methods' do
30
48
  it 'should provide an updated? accessor' do
31
49
  feed = @klass.new
@@ -19,7 +19,7 @@ module Feedjira::Parser
19
19
  end
20
20
  end
21
21
 
22
- describe 'parsing' do
22
+ describe 'parsing old style feeds' do
23
23
  before(:each) do
24
24
  @feed = AtomFeedBurner.parse(sample_feedburner_atom_feed)
25
25
  end
@@ -56,6 +56,37 @@ module Feedjira::Parser
56
56
  end
57
57
  end
58
58
 
59
+ describe 'parsing alternate style feeds' do
60
+ before(:each) do
61
+ @feed = AtomFeedBurner.parse(sample_feedburner_atom_feed_alternate)
62
+ end
63
+
64
+ it 'should parse the title' do
65
+ expect(@feed.title).to eq 'Giant Robots Smashing Into Other Giant Robots'
66
+ end
67
+
68
+ it 'should parse the description' do
69
+ description = 'Written by thoughtbot'
70
+ expect(@feed.description).to eq description
71
+ end
72
+
73
+ it 'should parse the url' do
74
+ expect(@feed.url).to eq 'https://robots.thoughtbot.com'
75
+ end
76
+
77
+ it 'should parse the feed_url' do
78
+ expect(@feed.feed_url).to eq 'http://feeds.feedburner.com/GiantRobotsSmashingIntoOtherGiantRobots'
79
+ end
80
+
81
+ it 'should parse hub urls' do
82
+ expect(@feed.hubs.count).to eq 1
83
+ end
84
+
85
+ it 'should parse entries' do
86
+ expect(@feed.entries.size).to eq 3
87
+ end
88
+ end
89
+
59
90
  describe 'preprocessing' do
60
91
  it 'retains markup in xhtml content' do
61
92
  AtomFeedBurner.preprocess_xml = true
data/spec/sample_feeds.rb CHANGED
@@ -19,6 +19,7 @@ module SampleFeeds
19
19
  sample_rss_feed: 'TenderLovemaking.xml',
20
20
  sample_rss_entry_content: 'TenderLovemakingFirstEntry.xml',
21
21
  sample_feedburner_atom_feed: 'PaulDixExplainsNothing.xml',
22
+ sample_feedburner_atom_feed_alternate: 'GiantRobotsSmashingIntoOtherGiantRobots.xml',
22
23
  sample_feedburner_atom_entry_content: 'PaulDixExplainsNothingFirstEntryContent.xml',
23
24
  sample_wfw_feed: 'PaulDixExplainsNothingWFW.xml',
24
25
  sample_google_docs_list_feed: 'GoogleDocsList.xml',
@@ -0,0 +1,682 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <?xml-stylesheet type="text/xsl" media="screen" href="/~d/styles/atom10full.xsl"?><?xml-stylesheet type="text/css" media="screen" href="http://feeds.feedburner.com/~d/styles/itemcontent.css"?><feed xmlns="http://www.w3.org/2005/Atom" xmlns:feedburner="http://rssnamespace.org/feedburner/ext/1.0">
3
+ <title>Giant Robots Smashing Into Other Giant Robots</title>
4
+ <subtitle>Written by thoughtbot</subtitle>
5
+ <id>https://robots.thoughtbot.com/</id>
6
+ <link href="https://robots.thoughtbot.com" />
7
+
8
+ <updated>2016-12-21T00:00:00+00:00</updated>
9
+ <author>
10
+ <name>thoughtbot</name>
11
+ </author>
12
+ <atom10:link xmlns:atom10="http://www.w3.org/2005/Atom" rel="self" type="application/atom+xml" href="http://feeds.feedburner.com/GiantRobotsSmashingIntoOtherGiantRobots" /><feedburner:info uri="giantrobotssmashingintoothergiantrobots" /><atom10:link xmlns:atom10="http://www.w3.org/2005/Atom" rel="hub" href="http://pubsubhubbub.appspot.com/" /><feedburner:emailServiceId>GiantRobotsSmashingIntoOtherGiantRobots</feedburner:emailServiceId><feedburner:feedburnerHostname>https://feedburner.google.com</feedburner:feedburnerHostname><feedburner:browserFriendly>This is an XML content feed. It is intended to be viewed in a newsreader or syndicated to another site, subject to copyright and fair use.</feedburner:browserFriendly><entry>
13
+ <title>"Tell, Don't Ask" in Elixir: A Story of Pattern-Matching
14
+ </title>
15
+ <link rel="alternate" href="http://feedproxy.google.com/~r/GiantRobotsSmashingIntoOtherGiantRobots/~3/DeWX2QCae6A/tell-don-t-ask-in-elixir" />
16
+ <author>
17
+ <name>Josh Clayton</name>
18
+ </author>
19
+ <id>https://robots.thoughtbot.com/tell-don-t-ask-in-elixir</id>
20
+ <published>2016-12-21T00:00:00+00:00</published>
21
+ <updated>2016-12-20T22:08:11Z</updated>
22
+ <content type="html">&lt;p&gt;&lt;a href="https://robots.thoughtbot.com/tell-dont-ask"&gt;&amp;ldquo;Tell, Don&amp;rsquo;t Ask&amp;rdquo;&lt;/a&gt; is a &lt;a href="http://martinfowler.com/bliki/TellDontAsk.html"&gt;well-covered&lt;/a&gt; &lt;a href="http://c2.com/cgi/wiki?TellDontAsk"&gt;topic&lt;/a&gt; within
23
+ object-oriented programming communities. Its goal? Encourage encapsulation by
24
+ having the caller &lt;strong&gt;tell&lt;/strong&gt; an object to do something instead of checking on
25
+ state and acting upon it. Almost at odds with the &lt;a href="https://github.com/troessner/reek/blob/master/docs/Control-Couple.md"&gt;control couple&lt;/a&gt; code smell,
26
+ our goal is to have the caller issue explicit commands without concerning
27
+ itself with object state.&lt;/p&gt;
28
+ &lt;h2 id="quottell-don39t-askquot-in-elixir"&gt;
29
+ &lt;a href="#quottell-don39t-askquot-in-elixir"&gt;
30
+ &amp;ldquo;Tell, Don&amp;rsquo;t Ask&amp;rdquo; in Elixir
31
+ &lt;/a&gt;
32
+ &lt;/h2&gt;
33
+
34
+ &lt;p&gt;&lt;a href="http://tech.noredink.com/post/142689001488/the-most-object-oriented-language"&gt;Is Elixir object-oriented?&lt;/a&gt; From a paradigm perspective, Elixir is a
35
+ functional language when looking at aspects like immutability,
36
+ pattern-matching, and functions with inputs and outputs, focused on the sending
37
+ of messages to &amp;ldquo;objects&amp;rdquo; directly. How does &amp;ldquo;Tell, Don&amp;rsquo;t Ask&amp;rdquo; translate?&lt;/p&gt;
38
+
39
+ &lt;p&gt;Thinking about the goal, let&amp;rsquo;s do some mental mapping. In OOP, objects are a
40
+ blueprint with information containing behavior (methods) and data (state).
41
+ In FP, we have functions organized within modules, with state being captured in
42
+ various values (e.g. Elixir&amp;rsquo;s Maps or Structs). We want to avoid having the
43
+ caller (a function) dictate paths based on information present in our data.&lt;/p&gt;
44
+
45
+ &lt;p&gt;Let&amp;rsquo;s write out some non-idiomatic Elixir and see what we can improve.&lt;/p&gt;
46
+
47
+ &lt;pre&gt;&lt;code class="elixir"&gt;defmodule Game.Lobby do
48
+ def add_player(%{game: game} = lobby, player) do
49
+ new_player = cond do
50
+ is_binary(player) -&amp;gt;
51
+ %Game.Player{name: player, id: Game.Player.generate_id}
52
+ is_map(player) -&amp;gt;
53
+ %Game.Player{} |&amp;gt; Map.merge(player)
54
+ true -&amp;gt;
55
+ %Game.Player{}
56
+ end
57
+
58
+ %{lobby |
59
+ game: %{game | players: game.players ++ [new_player]}}
60
+ end
61
+ end
62
+
63
+ defmodule Game.Player do
64
+ defstruct id: 0, name: &amp;quot;New player&amp;quot;, active: true
65
+
66
+ def generate_id do
67
+ UUID.uuid4()
68
+ end
69
+ end
70
+ &lt;/code&gt;&lt;/pre&gt;
71
+
72
+ &lt;p&gt;&lt;code&gt;Game.Lobby.add_player/2&lt;/code&gt; doesn&amp;rsquo;t feel right. There&amp;rsquo;s a significant amount of
73
+ &lt;a href="http://c2.com/cgi/wiki?FeatureEnvySmell"&gt;feature envy&lt;/a&gt; as it cares about the various shapes of &lt;code&gt;player&lt;/code&gt; and how to
74
+ construct a &lt;code&gt;%Game.Player{}&lt;/code&gt;. Also, why is &lt;code&gt;Game.Player.generate_id/0&lt;/code&gt; public?
75
+ It seems all &lt;code&gt;Game.Lobby.add_player/2&lt;/code&gt; should care about is managing its own
76
+ structure (the final two lines of the function).&lt;/p&gt;
77
+
78
+ &lt;p&gt;Instead of having &lt;code&gt;Game.Lobby.add_player/2&lt;/code&gt; care about constructing players,
79
+ generating ids, and so on, let&amp;rsquo;s &lt;strong&gt;tell&lt;/strong&gt; &lt;code&gt;Game.Player&lt;/code&gt; to handle that instead:&lt;/p&gt;
80
+
81
+ &lt;pre&gt;&lt;code class="elixir"&gt;defmodule Game.Lobby do
82
+ def add_player(%{game: game} = lobby, player) do
83
+ %{lobby |
84
+ game: %{game | players: game.players ++ [Game.Player.new(player)]}}
85
+ end
86
+ end
87
+
88
+ defmodule Game.Player do
89
+ defstruct id: 0, name: &amp;quot;New player&amp;quot;, active: true
90
+
91
+ def new(name) when is_binary(name), do: new(%{name: name, id: generate_id})
92
+ def new(a) when is_map(a), do: %__MODULE__{} |&amp;gt; Map.merge(a)
93
+ def new(_), do: %__MODULE__{}
94
+
95
+ defp generate_id, do: UUID.uuid4()
96
+ end
97
+ &lt;/code&gt;&lt;/pre&gt;
98
+
99
+ &lt;p&gt;Here, we move player generation to the &lt;code&gt;Game.Player&lt;/code&gt; module, where it can
100
+ determine how best to generate the struct instead of &lt;code&gt;Game.Lobby.add_player/2&lt;/code&gt;.&lt;/p&gt;
101
+ &lt;h2 id="write-declarative-not-imperative-code"&gt;
102
+ &lt;a href="#write-declarative-not-imperative-code"&gt;
103
+ Write declarative (not imperative) code
104
+ &lt;/a&gt;
105
+ &lt;/h2&gt;
106
+
107
+ &lt;p&gt;By moving player creation logic from &lt;code&gt;Game.Lobby.add_player/2&lt;/code&gt; to
108
+ &lt;code&gt;Game.Player.new/1&lt;/code&gt;, we were able to call a single function to take the
109
+ appropriate action based on data. It is important to note that the data it&amp;rsquo;s
110
+ acting upon specifically is behavior to construct a &lt;code&gt;%Game.Player{}&lt;/code&gt;.&lt;/p&gt;
111
+
112
+ &lt;p&gt;This becomes more important when using the &lt;a href="http://elixir-lang.org/docs/stable/elixir/Kernel.html#%7C%3E/2"&gt;pipe operator&lt;/a&gt;, which shines as a
113
+ way to transform data.&lt;/p&gt;
114
+
115
+ &lt;p&gt;&amp;ldquo;Tell, don&amp;rsquo;t ask&amp;rdquo; is a way to encourage developers to &lt;a href="https://medium.freecodecamp.com/imperative-vs-declarative-programming-283e96bf8aea"&gt;write declarative code
116
+ instead of imperative code&lt;/a&gt;. Imperative code asks questions before making
117
+ decisions; declarative code issues a command and expects it to be done
118
+ correctly.&lt;/p&gt;
119
+ </content>
120
+ <summary>Write declarative Elixir code by applying &amp;ldquo;Tell, Don&amp;rsquo;t Ask&amp;rdquo;.</summary>
121
+ <feedburner:origLink>https://robots.thoughtbot.com/tell-don-t-ask-in-elixir</feedburner:origLink></entry>
122
+ <entry>
123
+ <title>Avoiding Out of Memory Crashes on Mobile</title>
124
+ <link rel="alternate" href="http://feedproxy.google.com/~r/GiantRobotsSmashingIntoOtherGiantRobots/~3/Px1_JC1CMVg/avoiding-out-of-memory-crashes-on-mobile" />
125
+ <author>
126
+ <name>Steff Kelsey</name>
127
+ </author>
128
+ <id>https://robots.thoughtbot.com/avoiding-out-of-memory-crashes-on-mobile</id>
129
+ <published>2016-12-20T00:00:00+00:00</published>
130
+ <updated>2016-12-19T19:30:45Z</updated>
131
+ <content type="html">&lt;p&gt;One reason why it is difficult to develop software for mobile devices is that
132
+ the hardware is not the best compared to deploying to a console or a &amp;ldquo;real&amp;rdquo;
133
+ computer. Resources are limited. One particularly sparse resource is RAM. Out
134
+ of memory exceptions are common on both Android and iOS if you&amp;rsquo;re dealing with
135
+ large files. Recently, when building a Google VR 360 video player, we went over
136
+ the 1GB of RAM available on older iOS devices pretty quickly.&lt;/p&gt;
137
+ &lt;h2 id="what-not-to-do"&gt;
138
+ &lt;a href="#what-not-to-do"&gt;
139
+ What Not to Do
140
+ &lt;/a&gt;
141
+ &lt;/h2&gt;
142
+
143
+ &lt;p&gt;One of my big complaints about the Unity manual and many tutorials is they
144
+ usually just show you how to do something really quickly and don&amp;rsquo;t always tell
145
+ you the exact use case or how it can just flat out fail. For example, using the
146
+ relatively new &lt;code&gt;UnityWebRequest&lt;/code&gt;, you can download a file over HTTP like this:&lt;/p&gt;
147
+
148
+ &lt;pre&gt;&lt;code class="csharp"&gt;private IEnumerator loadAsset(string path)
149
+ {
150
+ using (UnityWebRequest webRequest = new UnityWebRequest(path))
151
+ {
152
+ webRequest.downloadHandler = new DownloadHandlerBuffer();
153
+ webRequest.Send();
154
+ while (!webRequest.isDone)
155
+ {
156
+ yield return null;
157
+ }
158
+ if (string.IsNullOrEmpty(webRequest.error))
159
+ {
160
+ FileComplete(this, new FileLoaderCompleteEventArgs(
161
+ webRequest.downloadHandler.data));
162
+ }
163
+ else
164
+ {
165
+ Debug.Log(&amp;quot;error! message: &amp;quot; + webRequest.error);
166
+ }
167
+ }
168
+ }
169
+ &lt;/code&gt;&lt;/pre&gt;
170
+
171
+ &lt;p&gt;These are all off the shelf parts from Unity, with the exception of the
172
+ &lt;code&gt;FileLoaderCompleteEventArg&lt;/code&gt; but just assume that we use that to pass off the
173
+ downloaded bytes as an array eg: &lt;code&gt;byte[]&lt;/code&gt;. Notice this returns an &lt;code&gt;IEnumerator&lt;/code&gt;
174
+ and utilizes &lt;code&gt;yield&lt;/code&gt; statements so it should be run in a &lt;code&gt;Coroutine&lt;/code&gt;. What
175
+ happens here is that the &lt;code&gt;UnityWebRequest&lt;/code&gt; will open up a connection to the
176
+ given path, download everything into a byte array contained within the
177
+ &lt;code&gt;DownloadHandlerBuffer&lt;/code&gt;. The &lt;code&gt;FileComplete&lt;/code&gt; event will fire if there are no
178
+ errors, sending the entire byte array to the subscribing class. Easy, right? For
179
+ small files, sure. But we were making a 360 Video player. Our max resolution was
180
+ 1440p. The first sample files we got for testing were bigger than 400MB. The
181
+ iPhone 7, with 2GB of RAM, took it like a champ. The iPhone 6, with 1GB of RAM,
182
+ crashed like a piano dropped from a helicopter.&lt;/p&gt;
183
+ &lt;h2 id="why-did-my-app-just-crash"&gt;
184
+ &lt;a href="#why-did-my-app-just-crash"&gt;
185
+ Why Did my App Just Crash?
186
+ &lt;/a&gt;
187
+ &lt;/h2&gt;
188
+
189
+ &lt;p&gt;Let&amp;rsquo;s look at the guts of these components. The main focus is on the
190
+ &lt;code&gt;DownloadHandlerBuffer&lt;/code&gt; object. When it is first created, it will start by
191
+ preallocating memory for a small byte array where it will store all the
192
+ downloaded bytes. As the bytes come in, it will periodically expand the size of
193
+ the array. In our test case, it was expanding the array until it could hold
194
+ 400MB. And because each additional allocation is a guess, it will most likely
195
+ overshot that amount. Note, I am speculating here because I have not looked at
196
+ the source code for the &lt;code&gt;DownloadBufferHandler&lt;/code&gt;. There is a chance it allocates
197
+ space based on the Content-Length header returned with the HTTP Response. But,
198
+ the result is the same; it will use up at least 400MB of RAM. That&amp;rsquo;s 40% of the
199
+ 1GB that the iPhone 6 has! We&amp;rsquo;re already in dangerous territory. I know what
200
+ you&amp;rsquo;re saying, &amp;ldquo;Steff, why did it crash if we only used 40% of the RAM?&amp;rdquo; There
201
+ are two ways to find the answer. One (and give Unity credit here) is in the
202
+ documentation for &lt;a href="https://docs.unity3d.com/ScriptReference/Networking.DownloadHandlerBuffer.html"&gt;&lt;code&gt;DownloadHandlerBuffer&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
203
+
204
+ &lt;blockquote&gt;
205
+ &lt;p&gt;Note: When accessing DownloadHandler.data or DownloadHandler.text on this
206
+ subclass, a new byte array or string will be allocated each time the property
207
+ is accessed.&lt;/p&gt;
208
+ &lt;/blockquote&gt;
209
+
210
+ &lt;p&gt;So, by accessing the data property, Unity allocates an &lt;em&gt;additional 400MB of
211
+ memory&lt;/em&gt; to pass off the byte array into the EventArg. Now we have used 800MB of
212
+ RAM just on handling this one file. The OS has other services running plus you
213
+ very likely have RAM allocated for bitmaps and UI and logic. You&amp;rsquo;re doomed!&lt;/p&gt;
214
+ &lt;h2 id="profiling-memory-allocations"&gt;
215
+ &lt;a href="#profiling-memory-allocations"&gt;
216
+ Profiling Memory Allocations
217
+ &lt;/a&gt;
218
+ &lt;/h2&gt;
219
+
220
+ &lt;p&gt;If you didn&amp;rsquo;t read the docs, and they&amp;rsquo;re long: I get it, you could have found
221
+ this memory leak by running the application in Unity while using the Profiler
222
+ &lt;em&gt;AND&lt;/em&gt; by running the application on an iOS device while using a valuable free
223
+ tool from Apple: Instruments. The Allocations instrument captures information
224
+ about memory allocation for an application. I recommend using the Unity Profiler
225
+ heavily for testing in the Editor and then continuing performance testing on
226
+ device for each platform. They all act differently. Using the Profiler in the
227
+ Editor is only your first line of defense. In this case I only properly
228
+ understood what was happening when I watched it unfold in a recording using the
229
+ Allocations instrument.&lt;/p&gt;
230
+ &lt;h2 id="streams-to-the-rescue"&gt;
231
+ &lt;a href="#streams-to-the-rescue"&gt;
232
+ Streams to the Rescue
233
+ &lt;/a&gt;
234
+ &lt;/h2&gt;
235
+
236
+ &lt;p&gt;There is a way to download large files and save them without using unnecessary
237
+ RAM. &lt;em&gt;Streams!&lt;/em&gt; Since we plan on immediately saving these large video files
238
+ in local storage on device to be ready for offline viewing, we need to send the
239
+ downloaded bytes right into a File as they are received. When doing that, we can
240
+ reuse the same byte array and never have to allocate more space. Unity outlines
241
+ how to do that &lt;a href="https://docs.unity3d.com/Manual/UnityWebRequest-CreatingDownloadHandlers.html"&gt;here&lt;/a&gt;, but below is an expanded example that includes a &lt;code&gt;FileStream&lt;/code&gt;:&lt;/p&gt;
242
+
243
+ &lt;pre&gt;&lt;code class="csharp"&gt;public class ToFileDownloadHandler : DownloadHandlerScript
244
+ {
245
+ private int expected = -1;
246
+ private int received = 0;
247
+ private string filepath;
248
+ private FileStream fileStream;
249
+ private bool canceled = false;
250
+
251
+ public ToFileDownloadHandler(byte[] buffer, string filepath)
252
+ : base(buffer)
253
+ {
254
+ this.filepath = filepath;
255
+ fileStream = new FileStream(filepath, FileMode.Create, FileAccess.Write);
256
+ }
257
+
258
+ protected override byte[] GetData() { return null; }
259
+
260
+ protected override bool ReceiveData(byte[] data, int dataLength)
261
+ {
262
+ if (data == null || data.Length &amp;lt; 1)
263
+ {
264
+ return false;
265
+ }
266
+ received += dataLength;
267
+ if (!canceled) fileStream.Write(data, 0, dataLength);
268
+ return true;
269
+ }
270
+
271
+ protected override float GetProgress()
272
+ {
273
+ if (expected &amp;lt; 0) return 0;
274
+ return (float)received / expected;
275
+ }
276
+
277
+ protected override void CompleteContent()
278
+ {
279
+ fileStream.Close();
280
+ }
281
+
282
+ protected override void ReceiveContentLength(int contentLength)
283
+ {
284
+ expected = contentLength;
285
+ }
286
+
287
+ public void Cancel()
288
+ {
289
+ canceled = true;
290
+ fileStream.Close();
291
+ File.Delete(filepath);
292
+ }
293
+ }
294
+ &lt;/code&gt;&lt;/pre&gt;
295
+
296
+ &lt;p&gt;And to use the above in our coroutine:&lt;/p&gt;
297
+
298
+ &lt;pre&gt;&lt;code class="csharp"&gt;private IEnumerator loadAsset(string path, string savePath)
299
+ {
300
+ using (UnityWebRequest webRequest = new UnityWebRequest(path))
301
+ {
302
+ webRequest.downloadHandler = new ToFileDownloadHandler(new byte[64 * 1024],
303
+ savePath);
304
+ webRequest.Send();
305
+ ...
306
+ ...
307
+ }
308
+ }
309
+ &lt;/code&gt;&lt;/pre&gt;
310
+
311
+ &lt;p&gt;Looking first at our new &lt;code&gt;ToFileDownloadHandler&lt;/code&gt;, we extended Unity&amp;rsquo;s
312
+ &lt;code&gt;DownloadHandlerScript&lt;/code&gt; and have overridden the required methods. The magic
313
+ happens in two places. First, we pass in a byte array to the base class via the
314
+ constructor. This let&amp;rsquo;s Unity know that we want to re-use that byte array on
315
+ each &lt;code&gt;ReceiveData&lt;/code&gt; callback where we only allocate a small amount of RAM once.
316
+ Second, we use a &lt;code&gt;FileStream&lt;/code&gt; object to write the bytes directly to our desired
317
+ file. The rest of the code is there to handle canceling the request. Whenever
318
+ you deal with &lt;code&gt;FileStream&lt;/code&gt; objects, you &lt;em&gt;must&lt;/em&gt; remember to close them out when
319
+ you&amp;rsquo;re done.&lt;/p&gt;
320
+
321
+ &lt;p&gt;Looking at the &lt;code&gt;loadAsset&lt;/code&gt; method, we added a parameter for the path to where
322
+ the file will be saved locally and we defined the size of the buffer at 64MB.
323
+ This size is dependent on your network speeds. We were focussed on WiFi
324
+ connections, so a larger buffer made sense. Too small and you will make the
325
+ download take longer than necessary to complete.&lt;/p&gt;
326
+ &lt;h2 id="where-to-go-from-here"&gt;
327
+ &lt;a href="#where-to-go-from-here"&gt;
328
+ Where to Go from Here
329
+ &lt;/a&gt;
330
+ &lt;/h2&gt;
331
+
332
+ &lt;p&gt;Now you have an understanding of one way that your application can eat up RAM.
333
+ If you only take away one thing from reading this post it&amp;rsquo;s this: for managing
334
+ memory allocations, streams are your friends. And you should be constantly
335
+ performance testing as you develop your application, unless you&amp;rsquo;re trying to
336
+ maximize one-star reviews in the App Store.&lt;/p&gt;
337
+ &lt;h2 id="gotchyas"&gt;
338
+ &lt;a href="#gotchyas"&gt;
339
+ Gotchyas
340
+ &lt;/a&gt;
341
+ &lt;/h2&gt;
342
+
343
+ &lt;p&gt;One final note on the code above: we did not end up going to production using
344
+ &lt;code&gt;UnityWebRequest&lt;/code&gt; on iOS. When we tried using a similar streaming solution
345
+ as above, we found that the request was not clearing from memory if it was
346
+ canceled due to the user sending the application to the background. Using the
347
+ Time Profiler Instrument showed that &lt;code&gt;NSURLSession&lt;/code&gt; objects were not being
348
+ cleaned up when the application paused and resumed, so eventually the CPU would
349
+ max out and crash. We had to seek an alternative solution for iOS using a native
350
+ plugin. However, in the final code we still used HTTP streaming directly into a
351
+ file via &lt;code&gt;FileStream&lt;/code&gt;. Just not wrapped up in &lt;code&gt;UnityWebRequest&lt;/code&gt; objects.&lt;/p&gt;
352
+ </content>
353
+ <summary>How to use Streams to more efficiently manage memory when downloading large files using Unity Engine.</summary>
354
+ <feedburner:origLink>https://robots.thoughtbot.com/avoiding-out-of-memory-crashes-on-mobile</feedburner:origLink></entry>
355
+ <entry>
356
+ <title>Solve Problems, not Strawmen</title>
357
+ <link rel="alternate" href="http://feedproxy.google.com/~r/GiantRobotsSmashingIntoOtherGiantRobots/~3/E6Xo6tl2AKk/you-aint-gonna-need-process" />
358
+ <author>
359
+ <name>Mike Burns</name>
360
+ </author>
361
+ <id>https://robots.thoughtbot.com/you-aint-gonna-need-process</id>
362
+ <published>2016-12-19T00:00:00+00:00</published>
363
+ <updated>2016-12-19T20:00:59Z</updated>
364
+ <content type="html">&lt;p&gt;It&amp;rsquo;s important to remember that solutions make little sense without problems,
365
+ and to &lt;strong&gt;avoid applying solutions in a vacuum&lt;/strong&gt;.&lt;/p&gt;
366
+
367
+ &lt;p&gt;Applying solutions within a vacuum &amp;ndash; for example, reading about VPNs and then
368
+ deciding to implement one for your team, without having a strong driving
369
+ impetus &amp;ndash; has a few downsides. Skipping the problem statement makes it
370
+ difficult to measure whether the solution works or how to iterate on it. It
371
+ also runs the risk of decreasing team happiness when solutions are not tethered
372
+ to reality.&lt;/p&gt;
373
+
374
+ &lt;p&gt;We avoid applying them unless needed in an effort to reduce bureaucracy and
375
+ process, so that developers can concentrate on developing, designers can
376
+ concentrate on designing, product owners can concentrate on prioritization, and
377
+ so on.&lt;/p&gt;
378
+
379
+ &lt;p&gt;To round this out we&amp;rsquo;ll present &lt;a href="https://thoughtbot.com/playbook"&gt;some solutions that we apply to
380
+ problems&lt;/a&gt;, along with some of the problems that they attack. Our
381
+ choice of the word &amp;ldquo;attack&amp;rdquo; here is meaningful: these solutions, despite their
382
+ name, do not &lt;em&gt;solve&lt;/em&gt; the problems; rather, they are part of a long-running
383
+ process of dealing with the problems. They might solve them entirely; they
384
+ might need refinement, or they might not work at all.&lt;/p&gt;
385
+ &lt;h2 id="acceptance-criteria"&gt;
386
+ &lt;a href="#acceptance-criteria"&gt;
387
+ Acceptance Criteria
388
+ &lt;/a&gt;
389
+ &lt;/h2&gt;
390
+
391
+ &lt;p&gt;Acceptance criteria is one way to attack the following problems:&lt;/p&gt;
392
+
393
+ &lt;ul&gt;
394
+ &lt;li&gt;Unrelated result: the ticket says one thing, the dev does another. If you
395
+ have another person accepting the stories, this will lead to lost time as the
396
+ dev and QA go back and forth on solutions. Without QA, this leads to an app
397
+ that you don&amp;rsquo;t recognize.&lt;/li&gt;
398
+ &lt;li&gt;The banana ticket: the developer knows how to implement the solution, but
399
+ doesn&amp;rsquo;t know &lt;a href="http://blog.teamtreehouse.com/when-is-a-user-story-done-acceptance-criteria-definition-done"&gt;when to stop&lt;/a&gt;, leading to an infinite refactoring of the entire
400
+ codebase under the guise of finishing this one ticket.&lt;/li&gt;
401
+ &lt;/ul&gt;
402
+
403
+ &lt;p&gt;To implement &lt;a href="http://nomad8.com/acceptance_criteria/"&gt;acceptance criteria&lt;/a&gt;, when writing tickets and stories for the
404
+ team, provide a detailed description of what the solution might look like &amp;ndash; a
405
+ description of when the story is finished and the ticket can be accepted by the
406
+ quality assurance team. &amp;ldquo;As an unconfirmed user, I cannot message anyone,&amp;rdquo; is a
407
+ quick example, but they can get bigger and more descriptive.&lt;/p&gt;
408
+ &lt;h2 id="retrospectives"&gt;
409
+ &lt;a href="#retrospectives"&gt;
410
+ Retrospectives
411
+ &lt;/a&gt;
412
+ &lt;/h2&gt;
413
+
414
+ &lt;p&gt;Retrospectives is one way to attack the following problems:&lt;/p&gt;
415
+
416
+ &lt;ul&gt;
417
+ &lt;li&gt;Unresolved conflict: the team is unable to communicate effectively during
418
+ conflict. Anger and resentment grow instead, buried and festering inside,
419
+ manifesting as passive-aggressive code review comments, poor collaboration,
420
+ low morale, people quitting, or an explosion of anger.&lt;/li&gt;
421
+ &lt;li&gt;Fights: instead of passive-aggressive, low-level frustration, the team could
422
+ express constant anger. Code review ends in tears.&lt;/li&gt;
423
+ &lt;/ul&gt;
424
+
425
+ &lt;p&gt;Scheduled, frequent, recurring retrospectives are a time for the team to
426
+ reflect on their happiness and internal relationship. These happen rain or
427
+ shine: it helps to practice communication in good times so that it becomes a
428
+ normal reflex in times of need. Some teams pair it with drinks at the end of a
429
+ workday; others do it mid-day as a normal part of the workday.&lt;/p&gt;
430
+ &lt;h2 id="standups"&gt;
431
+ &lt;a href="#standups"&gt;
432
+ Standups
433
+ &lt;/a&gt;
434
+ &lt;/h2&gt;
435
+
436
+ &lt;p&gt;Standup is one way to attack the following problems:&lt;/p&gt;
437
+
438
+ &lt;ul&gt;
439
+ &lt;li&gt;Isolation: people feel lonely working on remote teams or siloed projects.&lt;/li&gt;
440
+ &lt;li&gt;Othering: people on different teams have inhuman expectations of anothers&amp;rsquo;
441
+ team. This is expressed as increasing demands via faceless platforms like
442
+ &lt;a href="https://medium.com/@chrisjbatts/actually-slack-really-sucks-625802f1420a#.qf3n7xjdn"&gt;chat&lt;/a&gt; and ticket trackers, constant rejection of solutions, or warring
443
+ teams.&lt;/li&gt;
444
+ &lt;/ul&gt;
445
+
446
+ &lt;p&gt;A quick check-in once a day to catch everyone up on small details: what issue
447
+ or project people are working on, whether they are blocked, perhaps something
448
+ interesting that they learned, and general announcements. They are done
449
+ standing in an effort to &lt;a href="https://www.researchgate.net/publication/232529574_The_effects_of_stand-up_and_sit-down_meeting_formats_on_meeting_outcomes"&gt;keep them short&lt;/a&gt; (more fit teams may want to do them
450
+ standing on one leg instead).&lt;/p&gt;
451
+ &lt;h2 id="ticket-tracker"&gt;
452
+ &lt;a href="#ticket-tracker"&gt;
453
+ Ticket Tracker
454
+ &lt;/a&gt;
455
+ &lt;/h2&gt;
456
+
457
+ &lt;p&gt;A ticket tracker is one way to attack the following problems:&lt;/p&gt;
458
+
459
+ &lt;ul&gt;
460
+ &lt;li&gt;Duplicated work: multiple people open code reviews, only to find that another
461
+ review exists with that exact feature implemented. This kind of simple
462
+ miscommunication can be exacerbated by large teams, &lt;a href="http://basho.com/posts/technical/microservices-please-dont/"&gt;microservices&lt;/a&gt;,
463
+ distributed teams, and other communication challenges.&lt;/li&gt;
464
+ &lt;li&gt;Hurry up and wait: the marketing department waits until the feature is
465
+ shipped, and then hurries to advertise it (meanwhile they sat around
466
+ waiting).&lt;/li&gt;
467
+ &lt;li&gt;Surprise changes: the support team first learns that the UI has changed when
468
+ complaints roll in about the redesign; the CEO hears about the removal of her
469
+ favorite feature during a Q&amp;amp;A session at a conference.&lt;/li&gt;
470
+ &lt;/ul&gt;
471
+
472
+ &lt;p&gt;Ticket trackers are common, though often using different vocabulary. Trello
473
+ uses cards; Trajectory uses stories; Pivotal Tracker uses bugs, chores, and
474
+ stories. Jira does all of that plus provides visibility into metrics &amp;ndash; some
475
+ of which are projections, and others that are correct.&lt;/p&gt;
476
+ &lt;h2 id="story-or-jobs-to-be-done-format"&gt;
477
+ &lt;a href="#story-or-jobs-to-be-done-format"&gt;
478
+ Story or Jobs-To-Be-Done Format
479
+ &lt;/a&gt;
480
+ &lt;/h2&gt;
481
+
482
+ &lt;p&gt;Story format is one way to attack the following problems:&lt;/p&gt;
483
+
484
+ &lt;ul&gt;
485
+ &lt;li&gt;Mysterious business: the developer will happily implement a feature, lacking
486
+ the understanding of how it fits into the product or how it might be used.
487
+ Long term this leads to a disconnect between the code and the product &amp;ndash; the
488
+ domain-specific wording and language used throughout the app bears no
489
+ relation to the reality that it must model &amp;ndash; causing frustration among the
490
+ users and confusion when onboarding new developers.&lt;/li&gt;
491
+ &lt;li&gt;Unfollow-through: the developer implements the letter of the ticket, but not
492
+ the spirit, leading to situations where the feature is done but nothing
493
+ links to it; the JSON API functions but sends useless data; the user can
494
+ receive the password reset email but Gmail marks it as spam.&lt;/li&gt;
495
+ &lt;li&gt;Inflexibility: when the dev runs into complications, she pushes through
496
+ instead of re-evaluating for time sensitivity. This one solution is the only
497
+ one, and no compromises are entertained, regardless of how long this takes
498
+ and how it affects the user or the company.&lt;/li&gt;
499
+ &lt;/ul&gt;
500
+
501
+ &lt;p&gt;The story format phrases tasks in terms that the end user cares about, with an
502
+ explanation for &lt;a href="http://www.toyota-global.com/company/toyota_traditions/quality/mar_apr_2006.html"&gt;why&lt;/a&gt; the user might want the task done; similarly, the &lt;a href="https://robots.thoughtbot.com/converting-to-jobs-stories"&gt;jobs
503
+ story format&lt;/a&gt; puts the user&amp;rsquo;s context first and the &lt;a href="http://www.kitchensoap.com/2014/11/14/the-infinite-hows-or-the-dangers-of-the-five-whys/"&gt;motivation&lt;/a&gt; right in the
504
+ middle. This is all in contrast to the traditional task format, that focuses
505
+ only on what the developer must change in the code, with no explanation or
506
+ motivation.&lt;/p&gt;
507
+ &lt;h2 id="test-driven-design"&gt;
508
+ &lt;a href="#test-driven-design"&gt;
509
+ Test-Driven Design
510
+ &lt;/a&gt;
511
+ &lt;/h2&gt;
512
+
513
+ &lt;p&gt;TDD is one way to attack the following problems:&lt;/p&gt;
514
+
515
+ &lt;ul&gt;
516
+ &lt;li&gt;Blind fixes: the bug is fixed &amp;hellip; or is it? No one is quite sure, but
517
+ the new commit sure has a great description of how it could have fixed the
518
+ bug.&lt;/li&gt;
519
+ &lt;li&gt;Runtime whimsy: return values go unchecked, from &lt;a href="https://robots.thoughtbot.com/why-do-rubyists-test-so-completely"&gt;&lt;code&gt;nil&lt;/code&gt;&lt;/a&gt; to missing
520
+ files to failed credit card payments, leading to errors at runtime.&lt;/li&gt;
521
+ &lt;li&gt;Fear of deployment: the development workflow is running efficiently until it
522
+ comes time to actually merge the branch. Hesitation, followed by asking for
523
+ review after review, followed by begging others for reviews because no one
524
+ wants the responsibility of saying that it will work in production.&lt;/li&gt;
525
+ &lt;/ul&gt;
526
+
527
+ &lt;p&gt;Test-driven design (TDD) uses programmatic tests to drive the design and
528
+ architecture of the codebase. An incidental side effect is that the major
529
+ codepaths are tested, including error flows. Running the test suite exercises
530
+ all parts of the application, finding regressions in paths where bugs have been
531
+ found and fixed.&lt;/p&gt;
532
+ &lt;h2 id="refactoring"&gt;
533
+ &lt;a href="#refactoring"&gt;
534
+ Refactoring
535
+ &lt;/a&gt;
536
+ &lt;/h2&gt;
537
+
538
+ &lt;p&gt;Refactoring is one way to attack the following problems:&lt;/p&gt;
539
+
540
+ &lt;ul&gt;
541
+ &lt;li&gt;Unworkable feature: adding the new feature requires its own separate app or a
542
+ fragile connection through the database. What could be a simple button on a
543
+ Web page that performs a common activity takes days, weeks, or months to
544
+ implement.&lt;/li&gt;
545
+ &lt;li&gt;Hidden bug: you&amp;rsquo;ve traced the crash to one method, but that method was
546
+ written by a developer from two generations of coworkers ago, is 127
547
+ lines long, and the commit message was &amp;ldquo;it works&amp;rdquo;. The twenty code paths,
548
+ including liberal use of &lt;a href="https://docs.racket-lang.org/reference/cont.html#%28def._%28%28quote._%7E23%7E25kernel%29._call-with-escape-continuation%29%29"&gt;&lt;code&gt;return&lt;/code&gt;&lt;/a&gt;, obscure the source of the error.&lt;/li&gt;
549
+ &lt;/ul&gt;
550
+
551
+ &lt;p&gt;Refactoring is the process of shuffling code around without adding any features
552
+ or fixing any bugs. It is often the first step to implementing a new feature or
553
+ bug fix, carving a more clear path through the system, as part of the
554
+ &lt;a href="http://www.virtuouscode.com/2012/06/25/every-day-in-every-way/"&gt;red-green-refactor&lt;/a&gt; workflow.&lt;/p&gt;
555
+ &lt;h2 id="feature-flags"&gt;
556
+ &lt;a href="#feature-flags"&gt;
557
+ Feature Flags
558
+ &lt;/a&gt;
559
+ &lt;/h2&gt;
560
+
561
+ &lt;p&gt;Feature flags is one way to attack the following problems:&lt;/p&gt;
562
+
563
+ &lt;ul&gt;
564
+ &lt;li&gt;Market timing: the feature is implemented but the rest of the company still
565
+ isn&amp;rsquo;t ready for it. The support team needs to be trained on it, the
566
+ promotional announcement needs to be sent out, or the CEO needs to be
567
+ convinced that it&amp;rsquo;s a good idea.&lt;/li&gt;
568
+ &lt;li&gt;Questionable code: an isolated chunk of code &amp;ndash; a new file system, for
569
+ example &amp;ndash; is ready to be evaluated by willing and able participants, but
570
+ is not ready for public consumption until all the initial bugs have been
571
+ shaken out.&lt;/li&gt;
572
+ &lt;/ul&gt;
573
+
574
+ &lt;p&gt;Feature flags refers to hiding parts of the app behind a toggle, only shown to
575
+ some specific users or only enabled by an admin. These feature flags typically
576
+ differ from A/B testing in that they&amp;rsquo;re less about measurement and more about
577
+ hiding.&lt;/p&gt;
578
+ &lt;h2 id="code-review"&gt;
579
+ &lt;a href="#code-review"&gt;
580
+ Code Review
581
+ &lt;/a&gt;
582
+ &lt;/h2&gt;
583
+
584
+ &lt;p&gt;Code review is one way to attack the following problems:&lt;/p&gt;
585
+
586
+ &lt;ul&gt;
587
+ &lt;li&gt;Siloed development: developers work in isolation on specific categories of
588
+ projects &amp;ndash; one person on payment, another on API, a third on advertisement
589
+ targeting &amp;ndash; but on the same codebase. Features change around them and cruft
590
+ grows without any clear communication channel between devs.&lt;/li&gt;
591
+ &lt;li&gt;Poor code quality: the developers learn from blog posts and Web search
592
+ results instead of from each other, furthering their isolation. Coding styles
593
+ vary, and the same solution exists &lt;a href="http://lists.llvm.org/pipermail/llvm-dev/2016-October/106079.html"&gt;multiple times&lt;/a&gt; in the same repo.&lt;/li&gt;
594
+ &lt;/ul&gt;
595
+
596
+ &lt;p&gt;Reviewing code is the &lt;a href="http://www.geraldmweinberg.com/Site/Programming_Psychology.html"&gt;process&lt;/a&gt; of reading a diff: comparing a new commit with
597
+ what exists in the system. Often there is a focus on maintainability,
598
+ consistency, or knowledge transfer. Since it typically works on a diff, there
599
+ are fewer considerations for big picture harmony.&lt;/p&gt;
600
+ &lt;h2 id="git-workflow"&gt;
601
+ &lt;a href="#git-workflow"&gt;
602
+ Git Workflow
603
+ &lt;/a&gt;
604
+ &lt;/h2&gt;
605
+
606
+ &lt;p&gt;A consistent git workflow is one way to attack the following problems:&lt;/p&gt;
607
+
608
+ &lt;ul&gt;
609
+ &lt;li&gt;Unexplainable code: you find a strange line of code, the test explains
610
+ nothing, the commit message only says &amp;ldquo;initial commit&amp;rdquo;, the ticket tracker
611
+ was replaced twice since the project started, as was the project manager. Why
612
+ is this solution the right one, and why does the mysterious test enforce it?&lt;/li&gt;
613
+ &lt;li&gt;Bus factor one: the developer works alone, deploys a JAR, and leaves no
614
+ comments. Then she quits and the new dev is onboarded. I hope you enjoyed
615
+ this short horror story.&lt;/li&gt;
616
+ &lt;/ul&gt;
617
+
618
+ &lt;p&gt;Git provides enough plumbing to hang yourself. Maybe that&amp;rsquo;s not the expression.
619
+ Regardless, there&amp;rsquo;s no one way to use git, and multiple right ways. From branch
620
+ names to merge strategies to &lt;a href="https://robots.thoughtbot.com/5-useful-tips-for-a-better-commit-message"&gt;commit message&lt;/a&gt; content, it&amp;rsquo;s possible to have
621
+ &lt;a href="https://github.com/thoughtbot/guides/tree/master/protocol/git"&gt;a unified version control process&lt;/a&gt;.&lt;/p&gt;
622
+ &lt;h2 id="deployment-process"&gt;
623
+ &lt;a href="#deployment-process"&gt;
624
+ Deployment Process
625
+ &lt;/a&gt;
626
+ &lt;/h2&gt;
627
+
628
+ &lt;p&gt;A deploy process is one way to attack the following problems:&lt;/p&gt;
629
+
630
+ &lt;ul&gt;
631
+ &lt;li&gt;Expensive downtime: each second of downtime costs more in lost sales than
632
+ each second of development time costs in salary.&lt;/li&gt;
633
+ &lt;li&gt;External failures: the deployment depends on a set of external services
634
+ providing DNS, caching and content delivery, uptime monitoring, error
635
+ reporting, and so on, each of which has their own failure modes.&lt;/li&gt;
636
+ &lt;li&gt;Regulations: strict compliance with the law requires that very few people
637
+ have access to the database or the production servers.&lt;/li&gt;
638
+ &lt;/ul&gt;
639
+
640
+ &lt;p&gt;Going from development to production takes a few steps, which means it can be
641
+ scripted. The runnable script can live near a human-readable script, separately
642
+ describing who can deploy, what steps to take when it fails, what to do about
643
+ downtime, and how to announce it. Combined, the program and documentation
644
+ around it make up the deployment process.&lt;/p&gt;
645
+ &lt;h2 id="horizontal-scaling"&gt;
646
+ &lt;a href="#horizontal-scaling"&gt;
647
+ Horizontal Scaling
648
+ &lt;/a&gt;
649
+ &lt;/h2&gt;
650
+
651
+ &lt;p&gt;Scaling is one way to attack the following problems:&lt;/p&gt;
652
+
653
+ &lt;ul&gt;
654
+ &lt;li&gt;Concurrent users: the service is immediately more popular and needs to handle
655
+ twice as many users as before. They don&amp;rsquo;t necessarily need to use the full
656
+ service, but at least need to have enough working to get their job done.&lt;/li&gt;
657
+ &lt;li&gt;Concurrent processing: the algorithm can be subdivided into smaller,
658
+ independent problems, but each problem would take its own computer to solve.&lt;/li&gt;
659
+ &lt;/ul&gt;
660
+
661
+ &lt;p&gt;Scaling horizontally presents as spinning up more servers to handle the load;
662
+ this is in comparison with vertical scaling, where the CPU is sped up or the
663
+ RAM increased. The new servers come up quickly (&amp;ldquo;instantly&amp;rdquo;), run the same
664
+ software, and can be reduced when needed.&lt;/p&gt;
665
+ &lt;h2 id="and-more"&gt;
666
+ &lt;a href="#and-more"&gt;
667
+ And more
668
+ &lt;/a&gt;
669
+ &lt;/h2&gt;
670
+
671
+ &lt;p&gt;This is just a tiny selection of the solutions that we have seen to real
672
+ problems over the years. Some are for fancy ideas &amp;ndash; feature flags and story
673
+ format are not common to every team &amp;ndash; and some are sacred cows, like TDD and
674
+ refactoring.&lt;/p&gt;
675
+
676
+ &lt;p&gt;Your homework, dear reader, is to think of what problems you are solving and
677
+ what solutions are not carrying their weight. Let us know what you learn while
678
+ reflecting; we are always looking for &lt;a href="https://thoughtbot.com/playbook"&gt;processes we can drop&lt;/a&gt;!&lt;/p&gt;
679
+ </content>
680
+ <summary>Avoid applying solutions in a vacuum.</summary>
681
+ <feedburner:origLink>https://robots.thoughtbot.com/you-aint-gonna-need-process</feedburner:origLink></entry>
682
+ </feed>