-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathindex.html
More file actions
883 lines (783 loc) · 35.7 KB
/
Copy pathindex.html
File metadata and controls
883 lines (783 loc) · 35.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Static or Instance? Ten Decisions</title>
<style>
:root {
--bg: #0f1116;
--panel: #171a21;
--panel-hi: #1e222b;
--border: #2b313c;
--text: #e7eaee;
--muted: #98a2ae;
--good: #6fd3a1;
--good-bg: #14291f;
--good-border: #2e6b4d;
--bad: #f0908c;
--bad-bg: #2c1a1a;
--bad-border: #6b3535;
--focus: #7aa2f7;
--radius: 10px;
}
* { box-sizing: border-box; }
html, body {
margin: 0;
padding: 0;
background: var(--bg);
color: var(--text);
}
body {
font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
font-size: 16px;
line-height: 1.55;
-webkit-font-smoothing: antialiased;
}
code {
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 0.9em;
background: #0b0d11;
border: 1px solid var(--border);
border-radius: 4px;
padding: 0.1em 0.35em;
}
pre {
background: #0b0d11;
border: 1px solid var(--border);
border-radius: 8px;
padding: 0.85rem 1rem;
overflow-x: auto;
}
pre code { background: none; border: none; padding: 0; }
.wrap {
max-width: 760px;
margin: 0 auto;
padding: 2rem 1.25rem 5rem;
}
header { margin-bottom: 1.75rem; }
h1 {
font-size: 1.5rem;
line-height: 1.25;
margin: 0 0 0.5rem;
letter-spacing: -0.01em;
}
.intro {
color: var(--muted);
margin: 0;
font-size: 0.95rem;
}
.meter {
margin-top: 1.5rem;
display: flex;
align-items: center;
gap: 0.85rem;
}
.meter-track {
flex: 1;
height: 5px;
background: var(--panel-hi);
border-radius: 999px;
overflow: hidden;
}
.meter-fill {
height: 100%;
background: var(--good);
border-radius: 999px;
transition: width 220ms ease;
}
.meter-label {
color: var(--muted);
font-size: 0.8rem;
font-variant-numeric: tabular-nums;
white-space: nowrap;
}
.qhead {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 1rem;
margin-bottom: 0.75rem;
}
.qnum {
color: var(--muted);
font-size: 0.78rem;
text-transform: uppercase;
letter-spacing: 0.07em;
}
.chillies {
font-size: 0.8rem;
letter-spacing: 0.05em;
white-space: nowrap;
}
.stem {
font-size: 1.12rem;
line-height: 1.5;
margin: 0 0 1.35rem;
}
.stem > :first-child { margin-top: 0; }
.stem > :last-child { margin-bottom: 0; }
.stem pre { margin: 0.9rem 0; font-size: 0.88rem; }
.stem p { margin: 0 0 0.9rem; }
.options {
list-style: none;
margin: 0 0 1.25rem;
padding: 0;
display: flex;
flex-direction: column;
gap: 0.6rem;
}
.opt {
width: 100%;
display: flex;
gap: 0.8rem;
align-items: flex-start;
text-align: left;
font: inherit;
color: var(--text);
background: var(--panel);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 0.8rem 0.95rem;
cursor: pointer;
transition: background 120ms ease, border-color 120ms ease;
}
.opt:hover:not(:disabled) {
background: var(--panel-hi);
border-color: #46505f;
}
.opt:focus-visible {
outline: 2px solid var(--focus);
outline-offset: 2px;
}
.opt:disabled { cursor: default; }
.opt-key {
flex: 0 0 1.5rem;
height: 1.5rem;
display: grid;
place-items: center;
border: 1px solid var(--border);
border-radius: 6px;
font-size: 0.78rem;
color: var(--muted);
font-variant-numeric: tabular-nums;
}
.opt-body { flex: 1; }
.opt.is-wrong {
background: var(--bad-bg);
border-color: var(--bad-border);
color: #d8bcbc;
}
.opt.is-wrong .opt-key {
border-color: var(--bad-border);
color: var(--bad);
}
.opt.is-correct {
background: var(--good-bg);
border-color: var(--good-border);
}
.opt.is-correct .opt-key {
border-color: var(--good-border);
color: var(--good);
}
.opt.is-dimmed { opacity: 0.42; }
.fb {
border-radius: var(--radius);
padding: 0.95rem 1.1rem;
margin-bottom: 1.35rem;
border: 1px solid var(--border);
background: var(--panel);
}
.fb:empty { display: none; }
.fb.bad { background: var(--bad-bg); border-color: var(--bad-border); }
.fb.good { background: var(--good-bg); border-color: var(--good-border); }
.fb-head {
font-size: 0.78rem;
text-transform: uppercase;
letter-spacing: 0.07em;
margin: 0 0 0.4rem;
font-weight: 600;
}
.fb.bad .fb-head { color: var(--bad); }
.fb.good .fb-head { color: var(--good); }
.fb-body { margin: 0; }
.fb-body p { margin: 0 0 0.65rem; }
.fb-body p:last-child { margin-bottom: 0; }
.misc {
margin: 0.6rem 0 0;
font-size: 0.88rem;
color: var(--muted);
font-style: italic;
}
.actions {
display: flex;
align-items: center;
gap: 1rem;
}
.btn {
font: inherit;
font-weight: 500;
color: #0f1116;
background: var(--good);
border: 1px solid var(--good);
border-radius: 8px;
padding: 0.55rem 1.1rem;
cursor: pointer;
}
.btn:hover { filter: brightness(1.08); }
.btn:focus-visible {
outline: 2px solid var(--focus);
outline-offset: 2px;
}
.btn.ghost {
background: none;
color: var(--text);
border-color: var(--border);
}
.btn.ghost:hover { background: var(--panel-hi); }
.hint {
color: var(--muted);
font-size: 0.8rem;
margin: 2.5rem 0 0;
}
.score {
font-size: 2.6rem;
font-weight: 600;
letter-spacing: -0.02em;
margin: 0;
font-variant-numeric: tabular-nums;
}
.score-note { color: var(--muted); margin: 0.25rem 0 2rem; }
.review-h {
font-size: 1.05rem;
margin: 0 0 0.3rem;
}
.review-note {
color: var(--muted);
font-size: 0.9rem;
margin: 0 0 1.5rem;
}
.recap {
display: grid;
gap: 1rem;
grid-template-columns: repeat(auto-fit, minmax(255px, 1fr));
margin-bottom: 2.25rem;
}
.recap-col {
border: 1px solid var(--border);
border-radius: var(--radius);
background: var(--panel);
padding: 1rem 1.15rem;
}
.recap-h {
font-size: 0.78rem;
text-transform: uppercase;
letter-spacing: 0.07em;
color: var(--good);
font-weight: 600;
margin: 0 0 0.8rem;
}
.recap ul { margin: 0; padding-left: 1.15rem; }
.recap li { margin: 0 0 0.6rem; font-size: 0.93rem; }
.recap li:last-child { margin-bottom: 0; }
.recap li::marker { color: var(--muted); }
.qref { color: var(--muted); font-size: 0.78rem; white-space: nowrap; }
.missed { color: var(--bad); font-size: 0.78rem; white-space: nowrap; }
.card {
border: 1px solid var(--border);
border-radius: var(--radius);
background: var(--panel);
padding: 1.1rem 1.2rem;
margin-bottom: 1rem;
}
.card-stem {
font-weight: 600;
margin: 0 0 0.9rem;
}
.card-stem pre { margin: 0.7rem 0; font-size: 0.85rem; font-weight: 400; }
.card-stem p { margin: 0 0 0.6rem; }
.card-stem > :last-child { margin-bottom: 0; }
.card-num {
display: block;
font-size: 0.72rem;
text-transform: uppercase;
letter-spacing: 0.07em;
color: var(--muted);
font-weight: 600;
margin-bottom: 0.35rem;
}
.row {
border-left: 2px solid var(--border);
padding-left: 0.9rem;
margin: 0 0 0.9rem;
}
.row:last-child { margin-bottom: 0; }
.row.bad { border-left-color: var(--bad-border); }
.row.good { border-left-color: var(--good-border); }
.row-tag {
font-size: 0.72rem;
text-transform: uppercase;
letter-spacing: 0.07em;
font-weight: 600;
display: block;
margin-bottom: 0.2rem;
}
.row.bad .row-tag { color: var(--bad); }
.row.good .row-tag { color: var(--good); }
.row-text { margin: 0 0 0.45rem; }
.row-fb { margin: 0; color: #c8cfd8; font-size: 0.94rem; }
@media (max-width: 520px) {
.wrap { padding: 1.5rem 1rem 4rem; }
h1 { font-size: 1.3rem; }
.stem { font-size: 1.05rem; }
.opt-key { display: none; }
}
</style>
</head>
<body>
<div class="wrap">
<header>
<h1 id="quiz-title"></h1>
<p class="intro" id="quiz-intro"></p>
<div class="meter" id="meter" hidden>
<div class="meter-track"><div class="meter-fill" id="meter-fill" style="width:0%"></div></div>
<span class="meter-label" id="meter-label"></span>
</div>
</header>
<main id="view" aria-live="polite"></main>
</div>
<script>
/* ------------------------------------------------------------------
QUIZ DATA. Replace this whole object. Everything below it is the
engine and does not need changing.
Fields:
title string
intro string, one or two sentences
shuffleOptions optional, defaults true, set false if order matters
recapTitle optional, heading for the end recap, defaults "Recap"
recapNote optional, one line under that heading
questions[]
difficulty optional 1 to 3, rendered as chillies
stem the question, may contain block HTML such as <pre>
group optional, recap column this question belongs under
takeaway optional, one line summarising the lesson, shown in the recap
options[]
text may contain inline HTML
correct true on exactly one option
misconception short diagnosis, wrong options only
feedback required on every option
------------------------------------------------------------------ */
const QUIZ = {
title: "Static or Instance? Ten Decisions",
intro: "Ten scenarios. Each one describes something you need in order to do something else. Decide whether it wants a static method or an instance method. Most of them read as the wrong answer on first pass, so slow down and find the deciding detail.",
shuffleOptions: true,
recapTitle: "What decided each one",
recapNote: "The two columns are the whole lesson. Anything marked in red took you more than one attempt.",
questions: [
{
difficulty: 1,
group: "An instance method, because",
takeaway: "The answer depends on the state of one particular object.",
stem: "You are building a <code>ShoppingBasket</code>. You need to work out the total cost of everything currently in the basket, in order to show a figure on the checkout page.",
options: [
{
text: "An instance method",
correct: true,
feedback: "Right, and the reason is that the phrase \"currently in the basket\" is doing all the work. The total is a fact about one specific basket, so the method has to be able to reach that basket's items, which means it needs a <code>this</code>. Notice that the answer would flip if the requirement were reworded to hand the items in as an argument. That is the whole test in miniature: not what the method is about, but where it gets its data from."
},
{
text: "A static method",
misconception: "Treats arithmetic as inherently stateless",
feedback: "The pull here is that totalling is a calculation, and the calculations you meet first, <code>Math.abs</code> and friends, are static. So the shape feels familiar. Where it breaks is what the calculation needs as input. Summing prices requires knowing which items are in play, and that information lives inside one particular basket, not in the class. A static method has no basket to consult. Ask yourself what a static <code>total()</code> would even add up if two baskets existed and it was called on neither of them."
}
]
},
{
difficulty: 1,
group: "A static method, because",
takeaway: "It is a pure function of its arguments, wanted from unrelated places.",
stem: "You need to turn an amount held as an integer number of pence into a string like <code>£12.34</code>, in order to display money in the basket, in the order confirmation email and on the admin dashboard.",
options: [
{
text: "A static method",
correct: true,
feedback: "Correct. Everything the method needs arrives as its single argument, and nothing about the result depends on which basket, email or dashboard is asking. The three call sites are a useful signal in themselves: when a piece of behaviour is wanted by several unrelated classes and belongs to none of them, requiring callers to produce an object first is asking for something nobody has a use for. Making it static also documents that fact in the signature, which is information the next reader wants."
},
{
text: "An instance method",
misconception: "Assigns the method to whatever object the data usually comes from",
feedback: "This is domain reasoning rather than data reasoning, and it is a very common way to go wrong. Money appears on baskets, so formatting money feels like basket behaviour. But work out which class you would attach it to and the argument falls apart, because the email and the dashboard would then need a basket they do not have and do not want, purely to reach a formatting function. Look at the method's parameters and ask which of them could not be supplied without an object."
}
]
},
{
difficulty: 2,
group: "A static method, because",
takeaway: "It builds the first instance, so it cannot require one.",
stem: "You need to create a <code>Money</code> object from a string such as <code>\"12.34\"</code>, rejecting anything malformed, in order to parse the values coming out of a CSV import.",
options: [
{
text: "A static method",
correct: true,
feedback: "Yes, and this one is forced rather than chosen. At the moment of the call the importer holds a <code>String</code> and nothing else. There is no <code>Money</code> in existence for the method to be called on, so an instance method is not an option the language offers you. This is the static factory pattern, and it is worth recognising by name because the standard library is built on it: <code>Integer.parseInt</code>, <code>Duration.ofSeconds</code>, <code>List.of</code>. Contrast it with an instance method on the same class, such as <code>plus()</code>, which does have a <code>Money</code> to work from."
},
{
text: "An instance method",
misconception: "Thinks the object can populate its own fields from the input",
feedback: "The intuition that parsing fills in a <code>Money</code>'s fields is correct, and it is why this feels like <code>Money</code>'s job. It is <code>Money</code>'s job. The problem is purely one of sequence. To call an instance method you must already hold the instance, and the entire purpose of this method is to produce the thing you would need to hold. Walk through the importer line by line and ask what variables are actually in scope at the moment the call happens."
}
]
},
{
difficulty: 2,
group: "An instance method, because",
takeaway: "The collaborator carries configuration and must be swappable in tests.",
stem: "Your application talks to exactly one payment provider. You need a method that charges a card, in order to take payment at checkout. There will never be a second provider in production.",
options: [
{
text: "An instance method",
correct: true,
feedback: "Correct, and the sentence that looks like the decisive fact is the trap. \"There will never be a second provider\" is a statement about production, and production is not the only place this code runs. Two things push this to an instance. The charging code needs an API key, a base URL and a timeout, and that configuration is state, which differs between staging and production. And every test that touches checkout needs a substitute that records the charge instead of making it, which requires something you can pass in. Uniqueness in production tells you how many you will construct, not whether it should be an object."
},
{
text: "A static method",
misconception: "Uses static to express that there is only one of something",
feedback: "The reasoning is that one provider means one implementation means no need for objects, and it is a genuinely tempting chain because static members really do give you exactly one copy. But that is a property of the mechanism, not a reason to reach for it. Push on the claim by asking two questions. Where do the API key and base URL live, given that they differ by environment? And how does a test exercise checkout without charging a real card? Both questions want the same thing, and static cannot supply it."
}
]
},
{
difficulty: 2,
group: "A static method, because",
takeaway: "It validates input before the object it belongs to has been built.",
stem: "You need to check whether a UK postcode is well formed, in order to reject bad input on the address form and show an error, before any <code>Address</code> object has been constructed.",
options: [
{
text: "A static method",
correct: true,
feedback: "Right, and the clause \"before any <code>Address</code> object has been constructed\" is the deciding detail. The check runs on a raw string at the boundary of the system, so there is nothing to call an instance method on. This is the standard shape for input validation: a static predicate that the form handler can call, and which the <code>Address</code> constructor can also call to enforce its own invariant. Both callers need it, and only one of them has an <code>Address</code>."
},
{
text: "An instance method",
misconception: "Places validation on the domain class that the data will eventually belong to",
feedback: "There is a real principle underneath this, that a class should protect its own invariants, and the postcode rule does belong conceptually to <code>Address</code>. The mistake is going from \"belongs to <code>Address</code> conceptually\" to \"must be an instance method of <code>Address</code>\". A class can own a rule without needing an object to apply it. Reread the scenario and note exactly when the check has to happen relative to construction. That ordering rules one of these two out entirely."
}
]
},
{
difficulty: 3,
group: "An instance method, because",
takeaway: "Behaviour must vary by subtype, which needs real overriding.",
stem: "You have <code>PaymentMethod</code> with subtypes <code>Card</code>, <code>BankTransfer</code> and <code>Voucher</code>. You need each subtype to report its own processing fee percentage, in order to show a fee breakdown. The percentage is a fixed constant per subtype and never changes.",
options: [
{
text: "An instance method",
correct: true,
feedback: "Correct, and the constancy of the percentages is a deliberate red herring. The requirement is that the answer varies by subtype and that you get the right one when holding a <code>PaymentMethod</code> reference. That is polymorphism, and static methods cannot do it. A static method in a subclass <em>hides</em> the superclass one rather than overriding it, and the call is resolved at compile time from the declared type of the reference, so <code>method.feePercent()</code> on a <code>PaymentMethod</code> variable would return the base figure for every subtype regardless of the real object. The values being constant is irrelevant. What matters is that the selection between them has to happen at runtime, which requires reaching the object."
},
{
text: "A static method",
misconception: "Reads a hardcoded constant as proof that no instance is needed",
feedback: "The premise looks solid: nothing is read from any field, the numbers are literals, so what could an instance possibly contribute? Here is the missing piece. Something still has to choose <em>which</em> of the three literals you get, and that choice depends on which subtype the object really is. Static calls never look at the object, so they cannot make that choice. Try writing the static version out and calling it through a <code>PaymentMethod</code> variable holding a <code>Voucher</code>, and follow what the compiler does with the call."
}
]
},
{
difficulty: 3,
group: "A static method, because",
takeaway: "The algorithm is fixed and deterministic, with no configuration and nothing to substitute.",
stem: "You need to compute the SHA-256 hash of a file's bytes, in order to detect duplicate uploads. The algorithm is fixed by your file format specification and you will never want a different one.",
options: [
{
text: "A static method",
correct: true,
feedback: "Correct, and this one is worth holding next to the payment provider scenario, because they look like the same situation and resolve in opposite directions. Both are described as having exactly one implementation. The difference is what \"one implementation\" means in each case. Hashing takes bytes in and gives the same digest out every time, forever, with no endpoint, no credential and no environment to vary by. There is no configuration to hold, so no state, and nothing a test would want to fake, because a test can just check the digest. The injection rule earns its cost when a dependency has more than one legitimate implementation, and here a second one would not be an alternative, it would be a bug."
},
{
text: "An instance method",
misconception: "Applies the injection rule to something with nothing to vary",
feedback: "This is a good rule being applied slightly too widely, which is a much better error than not knowing the rule. Test the rule by asking what the second implementation would be. For a payment provider, it is a fake that records charges instead of making them, and that is worth the wiring. For SHA-256, an implementation that returns anything other than the SHA-256 digest is simply wrong, and there is nothing for a test to fake because the test can assert on the digest directly. Ask what an instance of a hasher would hold in its fields."
}
]
},
{
difficulty: 2,
group: "An instance method, because",
takeaway: "The value belongs to one object, not to the class as a whole.",
stem: "You need to count how many times a particular <code>Report</code> has been rendered, in order to display \"viewed 47 times\" on each report's page.",
options: [
{
text: "An instance method",
correct: true,
feedback: "Right, and the word carrying the weight is \"particular\". Each report needs its own tally, so the count is instance state and the method that reads or increments it is an instance method. The reason this scenario is worth sitting with is that counters are the canonical textbook example of a static field, which makes static feel like the idiomatic answer. It is idiomatic for counting things at the class level, such as how many reports have ever been created. It is exactly wrong for counting something about one object."
},
{
text: "A static method",
misconception: "Recalls the static counter idiom without checking what is being counted",
feedback: "The pattern being recalled is real, and a static counter incremented in a constructor is genuinely how you track instances created. So the shape is familiar for good reason. Now trace what happens here. One static field means one number for the whole program, so rendering report A would bump the figure shown on report B, and every page would display the same total. The idiom is right about counting and wrong about the scope. Reread the requirement and pick out the word that says whose count it is."
}
]
},
{
difficulty: 3,
group: "A static method, because",
takeaway: "It decides which instance the caller gets, including reusing a cached one.",
stem: "You need a way to obtain a <code>Currency</code> for a code such as <code>GBP</code>, handing back the very same object every time that code is requested, in order to avoid creating thousands of duplicates across a long import run.",
options: [
{
text: "A static method",
correct: true,
feedback: "Correct, and there are two independent reasons, which is what makes this a good closing case for static. First, the sequence problem again: the caller holds a <code>String</code> and wants a <code>Currency</code>, so there is nothing to call an instance method on. Second, and more interestingly, the requirement is to sometimes return an object that already exists. A constructor cannot do that, because <code>new</code> always produces a fresh object by definition. Only a static factory can look in a cache, decide nothing needs creating, and hand back what it found. Note that the cache itself is static state, which is normally a warning sign, and is defensible here only because the map is immutable once populated and the objects in it are immutable too."
},
{
text: "An instance method",
misconception: "Reasons from the presence of state to the need for an instance",
feedback: "The observation is sharp and it is half right: there genuinely is state here, because a cache is state, and \"state means instance\" is usually a reliable rule. What it misses is whose state it is. The cache belongs to the class, as a single shared registry of every currency, not to any one <code>Currency</code> object. And the sequence problem still bites regardless: work out what the caller is holding immediately before the call, and whether it is enough to invoke an instance method on."
}
]
},
{
difficulty: 3,
group: "An instance method, because",
takeaway: "Environment specific configuration is state, and state lives on an object.",
stem: "You need to send an SMS, in order to notify a customer that their order has shipped. The sending code needs an API key and a base URL, and both differ between your staging and production environments.",
options: [
{
text: "An instance method",
correct: true,
feedback: "Correct. The key and the URL are state, they vary by environment, and something has to hold them, which is a description of an object. Once you have that object, everything else you want follows for free: production constructs one pointing at the live endpoint, staging constructs one pointing at the sandbox, and a test passes one that records messages without sending anything. The alternative, a static <code>send</code> reading from static config, works right up to the first time you need two different configurations in one process, at which point there is no way out without changing every call site."
},
{
text: "A static method",
misconception: "Separates the action from its configuration and treats the action as a utility",
feedback: "The reasoning is that sending is a verb rather than data, so it feels like a utility function, and the config can be looked up from somewhere global. It is worth being precise about why that decomposition fails. The method cannot do its job without the key and the URL, so they are not incidental to it, they are inputs it depends on. Putting them in a static config does not remove the dependency, it just hides it and makes it unswappable. Ask what has to change for staging and production to differ, and where that difference could live."
}
]
}
]
};
/* ------------------------------------------------------------------
Engine
------------------------------------------------------------------ */
const view = document.getElementById("view");
const meter = document.getElementById("meter");
const meterFill = document.getElementById("meter-fill");
const meterLabel = document.getElementById("meter-label");
const state = { index: 0, done: false, progress: [] };
function shuffle(arr) {
for (let i = arr.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[arr[i], arr[j]] = [arr[j], arr[i]];
}
return arr;
}
function prepare() {
QUIZ.questions.forEach((q, qi) => {
q._opts = q.options.map((o, oi) => Object.assign({}, o, { key: qi + ":" + oi }));
if (QUIZ.shuffleOptions !== false) shuffle(q._opts);
});
document.title = QUIZ.title;
document.getElementById("quiz-title").textContent = QUIZ.title;
const intro = document.getElementById("quiz-intro");
if (QUIZ.intro) intro.textContent = QUIZ.intro;
else intro.hidden = true;
}
function reset() {
state.index = 0;
state.done = false;
state.progress = QUIZ.questions.map(() => ({ picks: [], solved: false, last: null }));
}
const q = () => QUIZ.questions[state.index];
const p = () => state.progress[state.index];
const firstTryCount = () =>
state.progress.filter(x => x.solved && x.picks.length === 1).length;
function updateMeter() {
if (state.done) { meter.hidden = true; return; }
meter.hidden = false;
const total = QUIZ.questions.length;
meterFill.style.width = (state.index / total) * 100 + "%";
meterLabel.textContent = (state.index + 1) + " of " + total;
}
function chillies(n) {
if (!n) return "";
const c = "\u{1F336}".repeat(Math.max(1, Math.min(3, n)));
return '<span class="chillies" title="Difficulty ' + n + ' of 3">' + c + "</span>";
}
function paragraphs(text) {
return String(text)
.split(/\n\s*\n/)
.map(t => "<p>" + t.trim() + "</p>")
.join("");
}
function renderQuestion() {
const cur = q();
const prog = p();
const opts = cur._opts.map((o, i) => {
const picked = prog.picks.indexOf(o.key) !== -1;
const classes = ["opt"];
let disabled = false;
if (picked && !o.correct) { classes.push("is-wrong"); disabled = true; }
if (prog.solved) {
disabled = true;
if (o.correct) classes.push("is-correct");
else if (!picked) classes.push("is-dimmed");
}
return (
'<li><button class="' + classes.join(" ") + '" data-key="' + o.key + '"' +
(disabled ? " disabled" : "") + ">" +
'<span class="opt-key" aria-hidden="true">' + (i + 1) + "</span>" +
'<span class="opt-body">' + o.text + "</span>" +
"</button></li>"
);
}).join("");
let fb = "";
if (prog.last) {
const o = cur._opts.find(x => x.key === prog.last);
const tone = o.correct ? "good" : "bad";
const head = o.correct ? "Correct" : "Not this one";
fb =
'<div class="fb ' + tone + '" role="status">' +
'<p class="fb-head">' + head + "</p>" +
'<div class="fb-body">' + paragraphs(o.feedback) + "</div>" +
(o.misconception ? '<p class="misc">Likely thinking: ' + o.misconception + "</p>" : "") +
"</div>";
}
const last = state.index === QUIZ.questions.length - 1;
const action = prog.solved
? '<button class="btn" id="next">' + (last ? "See results" : "Next question") + "</button>"
: "";
const nudge = prog.solved ? "" : '<span class="hint" style="margin:0">Pick an answer. Wrong answers are worth reading.</span>';
view.innerHTML =
'<div class="qhead">' +
'<span class="qnum">Question ' + (state.index + 1) + "</span>" +
chillies(cur.difficulty) +
"</div>" +
'<div class="stem">' + cur.stem + "</div>" +
'<ul class="options">' + opts + "</ul>" +
fb +
'<div class="actions">' + action + nudge + "</div>" +
'<p class="hint">Keys 1 to ' + cur._opts.length + " select an answer. Enter moves on.</p>";
view.querySelectorAll(".opt:not(:disabled)").forEach(btn => {
btn.addEventListener("click", () => pick(btn.dataset.key));
});
const next = document.getElementById("next");
if (next) {
next.addEventListener("click", advance);
if (prog.picks.length) next.focus();
}
updateMeter();
}
function pick(key) {
const prog = p();
if (prog.solved || prog.picks.indexOf(key) !== -1) return;
prog.picks.push(key);
prog.last = key;
const o = q()._opts.find(x => x.key === key);
if (o.correct) prog.solved = true;
renderQuestion();
}
function advance() {
if (state.index === QUIZ.questions.length - 1) {
state.done = true;
renderResults();
} else {
state.index++;
renderQuestion();
window.scrollTo({ top: 0, behavior: "smooth" });
}
}
function buildRecap() {
const tagged = QUIZ.questions
.map((q, i) => ({ q: q, n: i + 1, prog: state.progress[i] }))
.filter(x => x.q.group && x.q.takeaway);
if (!tagged.length) return "";
const order = [];
const groups = {};
tagged.forEach(x => {
if (!groups[x.q.group]) { groups[x.q.group] = []; order.push(x.q.group); }
groups[x.q.group].push(x);
});
const cols = order.map(g =>
'<div class="recap-col"><p class="recap-h">' + g + "</p><ul>" +
groups[g].map(x => {
const tag = x.prog.picks.length > 1
? '<span class="missed">Q' + x.n + ", reread</span>"
: '<span class="qref">Q' + x.n + "</span>";
return "<li>" + x.q.takeaway + " " + tag + "</li>";
}).join("") +
"</ul></div>"
).join("");
return (
'<h2 class="review-h">' + (QUIZ.recapTitle || "Recap") + "</h2>" +
(QUIZ.recapNote ? '<p class="review-note">' + QUIZ.recapNote + "</p>" : "") +
'<div class="recap">' + cols + "</div>"
);
}
function renderResults() {
const total = QUIZ.questions.length;
const clean = firstTryCount();
const missed = QUIZ.questions
.map((cur, i) => ({ cur, prog: state.progress[i], n: i + 1 }))
.filter(x => x.prog.picks.length > 1);
let note;
if (missed.length === 0) note = "Every question first time. Nothing to review.";
else if (clean === 0) note = "Every question took more than one attempt, which means there is a lot below worth rereading.";
else note = "First attempt only. The " + missed.length + " question" + (missed.length === 1 ? "" : "s") + " you worked through are below.";
const cards = missed.map(({ cur, prog, n }) => {
const wrong = prog.picks
.map(k => cur._opts.find(o => o.key === k))
.filter(o => !o.correct)
.map(o =>
'<div class="row bad">' +
'<span class="row-tag">You picked</span>' +
'<div class="row-text">' + o.text + "</div>" +
(o.misconception ? '<p class="row-fb"><em>' + o.misconception + "</em></p>" : "") +
'<div class="row-fb">' + paragraphs(o.feedback) + "</div>" +
"</div>"
).join("");
const right = cur._opts.find(o => o.correct);
return (
'<div class="card">' +
'<div class="card-stem"><span class="card-num">Question ' + n + '</span>' + cur.stem + "</div>" +
wrong +
'<div class="row good">' +
'<span class="row-tag">Correct answer</span>' +
'<div class="row-text">' + right.text + "</div>" +
'<div class="row-fb">' + paragraphs(right.feedback) + "</div>" +
"</div></div>"
);
}).join("");
view.innerHTML =
'<p class="score">' + clean + " / " + total + "</p>" +
'<p class="score-note">' + note + "</p>" +
buildRecap() +
(missed.length
? '<h2 class="review-h">Review</h2><p class="review-note">The misconception behind each wrong pick, and why the correct answer holds.</p>' + cards
: "") +
'<div class="actions"><button class="btn ghost" id="again">Start again</button></div>';
document.getElementById("again").addEventListener("click", () => {
prepare();
reset();
renderQuestion();
window.scrollTo({ top: 0 });
});
updateMeter();
window.scrollTo({ top: 0, behavior: "smooth" });
}
document.addEventListener("keydown", e => {
if (e.metaKey || e.ctrlKey || e.altKey) return;
if (state.done) return;
const prog = p();
if (e.key === "Enter" && prog.solved) { e.preventDefault(); advance(); return; }
const n = parseInt(e.key, 10);
if (!isNaN(n) && n >= 1 && n <= q()._opts.length) {
const o = q()._opts[n - 1];
if (!prog.solved && prog.picks.indexOf(o.key) === -1) { e.preventDefault(); pick(o.key); }
}
});
prepare();
reset();
renderQuestion();
</script>
</body>
</html>