Skip to content

Latest commit

ย 

History

History
1408 lines (1102 loc) ยท 46.3 KB

File metadata and controls

1408 lines (1102 loc) ยท 46.3 KB

Guide: Writing Testable Code

์›๋ฌธ

Flaw #1: Constructor does Real Work

์›๋ฌธ

  • constructor์—์„œ ์•„๋ž˜์™€ ๊ฐ™์€ ์ž‘์—…์€ ํ…Œ์ŠคํŠธ์— ํ•„์š”ํ•œ seam์„ ์ œ๊ฑฐํ•˜๊ณ , ์„œ๋ธŒ๋ธ”๋ž˜์Šค์™€ mock์ด ์›์น˜ ์•Š๋Š” ํ–‰์œ„๋ฅผ ์ƒ์†๋ฐ›๊ฒŒ ๋งŒ๋“ ๋‹ค.
    • collaborator๋ฅผ ์ƒ์„ฑ/์ดˆ๊ธฐํ™”
    • ๋‹ค๋ฅธ service์™€ ์ƒํ˜ธ์ž‘์šฉ
    • ์ž๊ธฐ ์ž์‹ ์˜ ์ƒํƒœ ์„ค์ •
  • ๋‹ค์‹œ ๋งํ•ด์„œ constructor์—์„œ ๋งŽ์€ ์ผ์„ ํ•˜๋ฉด ํ…Œ์ŠคํŠธ์—์„œ ์ƒ์„ฑ, collaborator ๋ณ€๊ฒฝ์„ ์–ด๋ ต๊ฒŒ ๋งŒ๋“ ๋‹ค.

Warning Signs:

  • constructor๋‚˜ ํ•„๋“œ ์ •์˜์—์„œ new ํ‚ค์›Œ๋“œ๊ฐ€ ์‚ฌ์šฉ๋œ ๊ฒฝ์šฐ
  • constructor๋‚˜ ํ•„๋“œ ์ •์˜์—์„œ static method๋ฅผ ํ˜ธ์ถœํ•œ ๊ฒฝ์šฐ
  • constructor์—์„œ ํ•„๋“œ ํ• ๋‹น๋ฌธ ์ด์™ธ์˜ ๋‹ค๋ฅธ ๋ฌธ์žฅ์ด ์‚ฌ์šฉ๋œ ๊ฒฝ์šฐ
  • constructor ์ข…๋ฃŒ ํ›„์— ๊ฐ์ฒด์˜ ์™„์ „ํžˆ ์ดˆ๊ธฐํ™” ๋˜์ง€ ์•Š์€ ๊ฒฝ์šฐ(initialize ๋ฉ”์†Œ๋“œ ์ฃผ์˜)
  • constructor์— ์กฐ๊ฑด๋ฌธ/๋ฃจํ”„๋ฌธ ๊ฐ™์€ ์ œ์–ด ๋กœ์ง์ด ์žˆ๋Š” ๊ฒฝ์šฐ
  • factory๋‚˜ builder ์‚ฌ์šฉ ์—†์ด constructor์— ๋ณต์žกํ•œ ๊ฐ์ฒด ๊ทธ๋ž˜ํ”„ ์ƒ์„ฑ ๋กœ์ง์ด ์žˆ๋Š” ๊ฒฝ์šฐ
  • initialization ๋ธ”๋ก์„ ์ถ”๊ฐ€ํ•˜๊ฑฐ๋‚˜ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ

Why this is a Flaw

  • constructor์—์„œ collaborator๋ฅผ ์ƒ์„ฑ/์ดˆ๊ธฐํ™”ํ•˜๋ฉด ์œ ์—ฐ์„ฑ์ด ์—†์–ด์ง€๊ณ  ์™„์„ฑ๋„๊ฐ€ ๋–จ์–ด์ง„ ๊ฒนํ•ฉ๋„(coupling)์ด ๋†’์€ ์„ค๊ณ„๊ฐ€ ๋œ๋‹ค.
  • ์ด๋Ÿฐ ๊ฒฝ์šฐ ํ…Œ์ŠคํŠธ์‹œ ํ…Œ์ŠคํŠธ collaborator ์ฃผ์ž…์ด ๋ถˆ๊ฐ€๋Šฅํ•˜๋‹ค.
    • SRP(Single Responsibility Principle) ์œ„๋ฐ˜
      • ๊ฐ์ฒด ๊ทธ๋ž˜ํ”„ ์ƒ์„ฑ์€ ์—„์—ฐํžˆ ๋…๋ฆฝ๋œ ์ฑ…์ž„์ด๊ณ  ์ด ์ฑ…์ž„์€ constructor์—์„œ ์ˆ˜ํ–‰ํ•˜๋Š” ๊ฒƒ์€ SRP ์œ„๋ฐ˜์ด๋‹ค.
    • ์ง์ ‘ ํ…Œ์ŠคํŠธํ•˜๋Š” ๊ฒƒ์ด ์–ด๋ ค์›Œ์ง
      • ์น˜ํ™˜ ๋ถˆ๊ฐ€ํ•œ collaborator์˜ ๋ฏธ๋ฌ˜ํ•œ ๋ณ€๊ฒฝ์€ constructor์— ๋ฐ˜์˜๋˜์–ด์•ผ ํ•˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋‹ค. ์ด๋Ÿฐ ๊ฒฝ์šฐ ํ…Œ์ŠคํŠธ๊ฐ€ ์–ด๋ ค์›Œ์ง„๋‹ค.
    • ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•œ ์„œ๋ธŒํด๋ž˜์‹ฑ/Overriding์ด ์—ฌ์ „ํžˆ ๊ฒฐํ•จ์œผ๋กœ ์กด์žฌํ•œ๋‹ค.
    • ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•œ Collaborator๋กœ ์น˜ํ™˜์ด ๋ถˆ๊ฐ€ํ•˜๋‹ค.
    • โ€œSeamโ€์„ ์—†์•ค๋‹ค.
    • Multiple Constructor๋ฅผ ๊ฐ–๋Š”๋‹ค ํ•ด๋„ ์—ฌ์ „ํžˆ ๋ฌธ์ œ
    • Bottom Line
      • ๊ณ ๋ฆฝ๋œ ์ƒํƒœ(isolation)๋‚˜ ํ…Œ์ŠคํŠธ ๋”๋ธ” collaborator๋กœ ์–ผ๋งˆ๋‚˜ ์‰ฝ๊ฒŒ ํด๋ž˜์Šค๋ฅผ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ๋Š”๊ฐ€๊ฐ€ ๊ด€๊ฑด
      • ์ƒ์„ฑํ•˜๊ธฐ ์–ด๋ ต๋‹ค๋ฉด constructor์—์„œ ๋„ˆ๋ฌด ๋งŽ์€ ์ผ์„ ํ•˜๋Š” ๊ฒƒ์ž„
      • ํ…Œ์ŠคํŠธ์—์„œ ์ƒ์„ฑํ•˜๊ธฐ ์–ด๋ ต๋‹ค๋ฉด ํ•ด๋‹น ํด๋ž˜์Šค๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๋‹ค๋ฅธ ์ฝ”๋“œ์—์„œ๋„ ์‚ฌ์šฉํ•˜๊ธฐ ์–ด๋ ต๋‹ค.

Recognizing the Flaw

  • ์•„๋ž˜์™€ ๊ฐ™์€ ์ฆ์ƒ์„ ์‚ดํŽด๋ณด๋ผ.
    • ํ…Œ์ŠคํŠธ์—์„œ ํ…Œ์ŠคํŠธ ๋”๋ธ”๋กœ ์น˜ํ™˜ํ•˜๊ณ  ์‹ถ์€ ๊ฐ์ฒด๋ฅผ new ํ‚ค์›Œ๋“œ๋กœ ์ƒ์„ฑํ•˜๊ณ  ์žˆ์ง€ ์•Š๋Š”๊ฐ€ ?
    • mocking, injection์ด ๋ถˆ๊ฐ€ํ•œ static method ํ˜ธ์ถœ์ด ์žˆ๋Š”๊ฐ€ ?
    • conditional/loop logic์ด ์กด์žฌํ•˜๋Š”๊ฐ€ ?

Fixing the Flaw

  • โ€œDo not create collaborators in your contructor, but pass them inโ€
  • โ€œDon't look for things !! Ask for things !!
  • ๊ฐ์ฒด ๊ทธ๋ž˜ํ”„ ์ƒ์„ฑ/์ดˆ๊ธฐํ™” ์ฑ…์ž„์„ ๋‹ค๋ฅธ ๊ฐ์ฒด๋กœ ์ด๋™์‹œ์ผœ๋ผ(builder, factory ๋“ฑ์„ ์ถ”์ถœํ•˜๊ณ  ์ด๋Ÿฐ collaborator๋“ค์„ constructor์— ์ „๋‹ฌํ•˜๋ผ).

Examples

Service Object Digging Around in Value Object
  • Before: Hard to Test
    • SUT
// Basic new operators called directly in
//   the class' constructor. (Forever
//   preventing a seam to create different
//   kitchen and bedroom collaborators).
class House {
    Kitchen kitchen = new Kitchen();
    Bedroom bedroom;
 
    House() {
        bedroom = new Bedroom();
    }
 
    // ...
}
- Test
// An attempted test that becomes pretty hard
 
class HouseTest extends TestCase {
    public void testThisIsReallyHard() {
        House house = new House();
        // Darn! I'm stuck with those Kitchen and
        //   Bedroom objects created in the
        //   constructor. 
 
        // ...
    }
}
  • After: Testable and Flexible Design
    • SUT
class House {
    Kitchen kitchen;
    Bedroom bedroom;
 
    // Have Guice create the objects
    //   and pass them in
    @Inject
    House(Kitchen k, Bedroom b) {
        kitchen = k;
        bedroom = b;
    }
    // ...
}
- Test
// New and Improved is trivially testable, with any
//   test-double objects as collaborators.
 
class HouseTest extends TestCase {
    public void testThisIsEasyAndFlexible() {
        Kitchen dummyKitchen = new DummyKitchen();
        Bedroom dummyBedroom = new DummyBedroom();
 
        House house = new House(dummyKitchen, dummyBedroom);
 
        // Awesome, I can use test doubles that
        //   are lighter weight.
 
        // ...
    }
}
  • ์œ„ ์˜ˆ์ œ๋Š” ๊ฐ์ฒด ๊ทธ๋ž˜ํ”„ ์ƒ์„ฑ๊ณผ ๋กœ์ง์ด ์„ž์—ฌ์žˆ๋‹ค. ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•  ๋•Œ ์šด์˜ํ™˜๊ฒฝ๊ณผ ๋‹ค๋ฅธ ๊ฐ์ฒด ๊ทธ๋ž˜ํ”„(๋Œ€๊ฐœ๋Š” ์ข€ ์ž‘์€ ๊ฐ์ฒด ๊ทธ๋ž˜ํ”„๋กœ, ์–ด๋–ค ๊ฐ์ฒด๋“ค์€ ํ…Œ์ŠคํŠธ ๋”๋ธ”๋กœ ์น˜ํ™˜๋œ)๋ฅผ ๋งŒ๋“ค๊ณ ์ž ํ•  ๋•Œ๊ฐ€ ๋งŽ๋‹ค.
  • new ํ‚ค์›Œ๋“œ๋ฅผ constructor์— ์œ ์ง€ํ•˜๊ณ ๋Š” ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•œ ๊ฐ์ฒด ๊ทธ๋ž˜ํ”„๋ฅผ ๋งŒ๋“ค์ˆ˜ ์—†๋‹ค.
  • Flaws:
    • ํ•„๋“œ ์ •์˜์— new ํ‚ค์›Œ๋“œ๋ฅผ ์‚ฌ์šฉํ–ˆ๋‹ค.
    • ๋งŒ์ผ Kitchen์ด ํŒŒ์ผ/๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์™€ ๊ฐ™์ด ์ƒ์„ฑ ๋น„์šฉ์ด ๋งŽ์ด ๋“œ๋Š” ๊ฒฝ์šฐ๋ผ๋ฉด House ๊ฐ์ฒด๋ฅผ ๋งŒ๋“ค๊ธฐ ์–ด๋ ค์›Œ์ง„๋‹ค.
    • kitchen์ด๋‚˜ bedroom์˜ ํ–‰์œ„๋ฅผ polymorphicalํ•˜๊ฒŒ ๋ณ€๊ฒฝํ•  ์ˆ˜ ์—†๊ธฐ ๋•Œ๋ฌธ์— ์„ค๊ณ„๊ฐ€ ๊นจ์ง€๊ธฐ ์‰ฝ๋‹ค.
  • Kitchen์ด value object(LinkdList, Map, User, Email Address ๋“ฑ๊ณผ ๊ฐ™์ด)๋ผ๋ฉด value object๊ฐ€ service ๊ฐ์ฒด๋ฅผ ์ฐธ์กฐํ•˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— inline์œผ๋กœ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค. Service ๊ฐ์ฒด๋Š” ํ…Œ์ŠคํŠธ ๋”๋ธ”๋กœ ์น˜ํ™˜๋  ํ•„์š”๊ฐ€ ์žˆ๋Š” ํƒ€์ž…์ด๋‹ค. ๊ทธ๋ž˜์„œ static method ํ˜ธ์ถœ๋กœ ์ง์ ‘ ์ƒ์„ฑํ•ด์„œ๋Š” ์•ˆ๋œ๋‹ค.
Constructor takes a partially initialized object and has to set it up
  • Before: Hard to Test
    • SUT
// SUT initializes collaborators. This prevents
//   tests and users of Garden from altering them.
 
class Garden {
    Garden(Gardener joe) {
        joe.setWorkday(new TwelveHourWorkday());
        joe.setBoots(new BootsWithMassiveStaticInitBlock());
        this.joe = joe;
    }
    // ...
}
- Test
// A test that is very slow, and forced
//   to run the static init block multiple times.
 
class GardenTest extends TestCase {
    public void testMustUseFullFledgedGardener() {
        Gardener gardener = new Gardener();
        Garden garden = new Garden(gardener);
        new AphidPlague(garden).infect();
        garden.notifyGardenerSickShrubbery();
        assertTrue(gardener.isWorking());
    }
}
  • After: Testable and Flexible Design
    • SUT
// Let Guice create the gardener, and have a
//   provider configure it.
 
class Garden {
    Gardener joe;
 
    @Inject
    Garden(Gardener joe) {
        this.joe = joe;
    }
 
    // ...
}
 
// In the Module configuring Guice.
 
@Provides
Gardener getGardenerJoe(Workday workday, BootsWithMassiveStaticInitBlock badBoots) {
    Gardener joe = new Gardener();
    joe.setWorkday(workday);
 
    // Ideally, you'll refactor the static init.
    joe.setBoots(badBoots);
    return joe;
}
- Test
// The new tests run quickly and are not
//   dependent on the slow
//   BootsWithMassiveStaticInitBlock
 
class GardenTest extends TestCase {
 
    public void testUsesGardenerWithDummies() {
        Gardener gardener = new Gardener();
        gardener.setWorkday(new OneMinuteWorkday());
        // Okay to pass in null, b/c not relevant
        //   in this test.
        gardener.setBoots(null);
 
        Garden garden = new Garden(gardener);
 
        new AphidPlague(garden).infect();
        garden.notifyGardenerSickShrubbery();
 
        assertTrue(gardener.isWorking());
    }
}
  • ๊ฐ์ฒด ๊ทธ๋ž˜ํ”„ ์ƒ์„ฑ(Garden์˜ collaborator Gardener๋ฅผ ์ƒ์„ฑํ•˜๊ณ  ์„ค์ •ํ•˜๋Š” ๊ฒƒ)์€ Garden์ด ์ˆ˜ํ–‰ํ•ด์•ผ ํ•˜๋Š” ์ฑ…์ž„๊ณผ ๋‹ค๋ฅธ ์ฑ…์ž„์ด๋‹ค.
  • ์ด ์ฒ˜๋Ÿผ constructor์— ์„ค์ •๊ณผ ์ƒ์„ฑ์ด ์„ž์—ฌ์žˆ์œผ๋ฉด, ๊ฐ์ฒด๋Š” ๊นจ์ง€๊ธฐ ์‰ฝ๊ณ  ๊ตฌ์ฒด์  ๊ฐ์ฒด ๊ทธ๋ž˜ํ”„ ๊ตฌ์กฐ์— ์–ฝ๋ฉ”์ด๊ฒŒ ๋œ๋‹ค. ์ด๋กœ ์ธํ•ด ์ฝ”๋“œ๋ฅผ ๋ณ€๊ฒฝํ•˜๊ธฐ ์–ด๋ ต๊ณ , ํ…Œ์ŠคํŠธํ•˜๊ธฐ ๊ฑฐ์˜ ๋ถˆ๊ฐ€๋Šฅํ•ด ์ง„๋‹ค.
  • Flaws:
    • Garden์€ Gardener๋ฅผ ํ•„์š”๋กœ ํ•˜์ง€๋งŒ Gardener๋ฅผ ์„ค์ •ํ•˜๋Š” ๊ฒƒ์€ Garden์˜ ์ฑ…์ž„์ด ์•„๋‹ˆ๋‹ค.
    • Garden์˜ ์œ ๋‹› ํ…Œ์ŠคํŠธ์—์„œ warkday๋Š” constructor์—์„œ ์„ค์ •๋œ๋‹ค. ์ด๋กœ ์ธํ•ด Joe๊ฐ€ ํ•˜๋ฃจ์— 12์‹œ๊ฐ„ ์ผํ•˜๊ฒŒ ๋œ๋‹ค. ์ด๋Ÿฌํ•œ ์˜์กด์„ฑ ๊ฑธ์ •์€ ํ…Œ์ŠคํŠธ๊ฐ€ ๋А๋ฆฌ๊ฒŒ ๋™์ž‘ํ•˜๊ฒŒ ๋งŒ๋“ ๋‹ค. ์œ ๋‹› ํ…Œ์ŠคํŠธ์—์„œ๋Š” ์งง์€ ์‹œ๊ฐ„๋งŒ ์ผํ•˜๋„๋ก ์„ค์ •ํ•˜๊ธฐ๋ฅผ ์›ํ•  ๊ฒƒ์ด๋‹ค.
    • boots๋ฅผ ๋ณ€๊ฒฝํ•  ์ˆ˜ ์—†๋‹ค. BootsWithMassiveStaticInitBlock์„ ์‚ฌ์šฉํ•˜๊ณ  ๋กœ๋”ฉํ•˜๋Š” ๋ฌธ์ œ๋ฅผ ํšŒํ”ผํ•˜๊ธฐ ์œ„ํ•ด boots์— ๋Œ€ํ•ด ํ…Œ์ŠคํŠธ ๋”๋ธ”์„ ์‚ฌ์šฉํ•˜๊ณ  ์‹ถ์„ ๊ฒƒ์ด๋‹ค(Static initialization block์€ ์œ„ํ—˜ํ•˜๊ณ  ๋ฌธ์ œ๋ฅผ ์•ผ๊ธฐํ•  ์†Œ์ง€๊ฐ€ ๋งŽ๋‹ค. ํŠนํžˆ ์ „์—ญ์ƒํƒœ์™€ ์ƒํ˜ธ์ž‘์šฉํ•  ๊ฒฝ์šฐ๋Š” ๋” ์œ„ํ—˜ํ•˜๋‹ค).
  • ์ดˆ๊ธฐํ™”๋˜์–ด์•ผ ํ•˜๋Š” collaborator๊ฐ€ ํ•„์š”ํ•œ ๊ฒฝ์šฐ 2๊ฐœ์˜ ๊ฐ์ฒด๋ฅผ ๊ฐ–๋„๋ก ํ•˜๋ผ. 2๊ฐœ์˜ ๊ฐ์ฒด๋ฅผ ์ดˆ๊ธฐํ™”ํ•˜๊ณ  ์™„์ „ํžˆ ์ดˆ๊ธฐํ™”๋œ ์ƒํƒœ๋กœ ํด๋ž˜์Šค์˜ constructor์— ์ „๋‹ฌํ•˜๋ผ.
Violating the Law of Demeter in Constructor
  • Before: Hard to Test
    • SUT
// Violates the Law of Demeter
// Brittle because of excessive dependencies
// Mixes object lookup with assignment
 
class AccountView {
    User user;
    AccountView() {
        user = RPCClient.getInstance().getUser();
    }
}
- Test
// Hard to test because needs real RPCClient
class ACcountViewTest extends TestCase {
 
    public void testUnfortunatelyWithRealRPC() {
        AccountView view = new AccountView();
        // Shucks! We just had to connect to a real
        //   RPCClient. This test is now slow.
 
        // ...
    }
}
  • After: Testable and Flexible Design
    • SUT
class AccountView {
    User user;
 
    @Inject
    AccountView(User user) {
        this.user = user;
    }
}
 
// The User is provided by a GUICE provider
@Provides
User getUser(RPCClient rpcClient) {
    return rpcClient.getUser();
}
 
// RPCClient is also provided, and it is no longer
//   a JVM Singleton.
@Provides @Singleton
RPCClient getRPCClient() {
    // we removed the JVM Singleton
    //   and have GUICE manage the scope
    return new RPCClient();
}
- Test
// Easy to test with Dependency Injection
class AccountViewTest extends TestCase {
 
    public void testLightweightAndFlexible() {
        User user = new DummyUser();
        AccountView view = new AccountView(user);
        // Easy to test and fast with test-double
        //   user.
 
        // ...
    }
}
  • ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ์ „์—ญ ์ƒํƒœ์— ์ ‘๊ทผํ•˜๊ณ  RPCClient singleton์˜ holder๋ฅผ ์–ป์—ˆ๋‹ค. singleton์€ ํ•„์š” ์—†๋Š” ๊ฒƒ์ด๊ณ  User๋งŒ์ด ํ•„์š”ํ•œ ๊ฒƒ์ธ๋ฐ ๋ง์ด๋‹ค. ์ฒซ๋ฒˆ์งธ ์ž˜๋ชป์€ seam์„ ์ œ๊ณตํ•˜์ง€ ์•Š๋Š” static method๋ฅผ ์‚ฌ์šฉํ•œ ๊ฒƒ์ด๊ณ , ๋‘๋ฒˆ์งธ ์ž˜๋ชป์€ โ€œLaw of Demeterโ€๋ฅผ ์œ„๋ฐฐํ•œ ๊ฒƒ์ด๋‹ค.
  • Flaws:
    • mock ๊ฐ์ฒด๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด RPCClient.getInstance() ๋ฉ”์†Œ๋“œ๋ฅผ ๊ฐ€๋กœ์ฑŒ ์ˆ˜ ์—†๋‹ค(static method๋Š” non-interceptable & non-mockable).
    • SUT๊ฐ€ RPCClient๋ฅผ ํ•„์š”๋กœ ํ•˜์ง€ ์•Š๋Š”๋ฐ ์™œ RPCClient๋ฅผ mock์œผ๋กœ ์น˜ํ™˜ํ•ด์•ผ ํ•˜๋Š”๊ฐ€?(AccountView๋Š” rpc instance๋ฅผ ํ•„๋“œ์— ์ €์žฅํ•˜์ง€ ์•Š๋Š”๋‹ค). User๋งŒ ์ €์žฅ/์ ‘๊ทผํ•  ์ˆ˜ ์žˆ์œผ๋ฉด ๋œ๋‹ค.
    • AccountView๋ฅผ ์ƒ์„ฑํ•˜๋ ค๋Š” ๋ชจ๋“  ํ…Œ์ŠคํŠธ๋Š” ์œ„์˜ ๋ฌธ์ œ๋ฅผ ๊ฐ–๋Š”๋‹ค. ํ•˜๋‚˜์˜ ํ…Œ์ŠคํŠธ์—์„œ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ–ˆ๋‹ค๊ณ  ํ•˜๋”๋ผ๋„ ๋‹ค๋ฅธ ํ…Œ์ŠคํŠธ์—์„œ๋Š” ๋ฌธ์ œ๊ฐ€ ํ•ด๊ฒฐ๋œ ๊ฒƒ์ด ์•„๋‹ˆ๋‹ค.
  • ๊ฐœ์„ ๋œ ์ฝ”๋“œ์—์„œ๋Š” ์ง์ ‘์ ์œผ๋กœ ํ•„์š”ํ•œ ๊ฐ์ฒด๋งŒ ์ „๋‹ฌ๋˜์—ˆ๋‹ค: User collaborator. ํ…Œ์ŠคํŠธ์‹œ ์ƒ์„ฑํ•ด์•ผ ํ•˜๋Š” ๊ฒƒ์€ (real or test double) User ๊ฐ์ฒด๋ฟ์ด๋‹ค. ์ด๋กœ ์ธํ•ด ์„ค๊ณ„๊ฐ€ ๋ณด๋‹ค ์œ ์—ฐํ•ด์ง€๊ณ  ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅ์„ฑ์ด ๋ณด๋‹ค ๋†’์•„์ง„๋‹ค.
Creating Unneeded Third Party Objects in Constructor
  • Before: Hard to Test
    • SUT
// Creating unneeded third party objects,
//   Mixing object construction with logic, &
//   "new" keyword removes a seam for other
//   EngineFactory's to be used in tests.
//   Also ties you to the (slow) file system.
 
class Car {
    Engine engine;
    Car(File file) {
        String model = readEngineModel(file);
        engine = new EngineFactory().create(model);
    }
 
    // ...
}
- Test
// The test exposes the brittleness of the Car
class AccountViewTest extends TestCase {
 
    public void testNoSeamForFakeEngine() {
        // Aggh! I hate using files in unit tests
        File file = new File("engine.config");
        Car car = new Car(file);
 
        // I want to test with a fake engine
        //   but I can't since the EngineFactory
        //   only knows how to make real engines.
    }
}
  • After: Testable and Flexible Design
    • SUT
// Asks for precisely what it needs
 
class Car {
    Engine engine;
 
    @Inject
    Car(Engine engine) {
        this.engine = engine;
    }
 
    // ...
}
 
// Have a provider in the Module
//   to give you the Engine
@Provides
Engine getEngine(EngineFactory engineFactory, @EngineModel String model) {
    //
    return engineFactory.create(model);
}
 
// Elsewhere there is a provider to
//   get the factory and model
- Test
// Now we can see a flexible, injectible design
class AccountViewTest extends TestCase {
 
    public void testShowsWeHaveCleanDesign() {
        Engine fakeEngine = new FakeEngine();
        Car car = new Car(fakeEngine);
 
        // Now testing is easy, with the car taking
        //   exactly what it needs.
    }
}
  • Car๊ฐ€ ์ž์‹ ์˜ ์—”์ง„์„ ๋งŒ๋“ค๊ธฐ ์œ„ํ•ด EngineFactory๋ฅผ ํ•„์š”๋กœ ํ•˜๋Š” ๊ฒƒ์€ ์˜๋ฏธ์— ๋งž์ง€ ์•Š๋Š”๋‹ค. Car๋Š” ์—”์ง„์„ ์–ด๋–ป๊ฒŒ ๋งŒ๋“ค๊ฒƒ์ธ๊ฐ€๋ฅผ ์ƒ๊ด€ํ•˜์ง€ ๋ง๊ณ  ์ด๋ฏธ ๋งŒ๋“ค์–ด์ง„ ์—”์ง„์„ ๊ณต๊ธ‰ ๋ฐ›์•„์•ผ ํ•œ๋‹ค. ์ฃผํ–‰ํ•˜๋Š” ๊ฒƒ์ด ๋ชฉ์ ์ธ Car๋Š” ๊ณต์žฅ์— ๋Œ€ํ•œ ๋ ˆํผ๋Ÿฐ์Šค๋ฅผ ๊ฐ–์ง€ ๋ง์•„์•ผ ํ•œ๋‹ค. ๊ฐ™์€ ๋งฅ๋ฝ์œผ๋กœ constructor์—์„œ๋Š” ์ง์ ‘์ ์œผ๋กœ ํ•„์š”ํ•˜์ง€ ์•Š์€ 3rd party ๊ฐ์ฒด๊ฐ€ ์•„๋‹‰๋ผ ๊ทธ ๊ฐ์ฒด๊ฐ€ ์ƒ์„ฑํ•˜๋Š” ๊ฐ์ฒด๋งŒ ์‚ฌ์šฉํ•ด์•ผ ํ•œ๋‹ค.
  • Flaws:
    • ์‹ค์ œ๋กœ ํ•„์š”ํ•œ ๊ฒƒ์€ Engine์ธ๋ฐ File์„ ๋„˜๊ธฐ๊ณ  ์žˆ๋‹ค.
    • 3rd party ๊ฐ์ฒด(EngineFactory)๋ฅผ ์ƒ์„ฑํ•˜๊ณ  ์žˆ๋‹ค. 3rd party ๊ฐ์ฒด ์ƒ์„ฑ์€ inject/override ๋ถˆ๊ฐ€ํ•˜๋ฏ€๋กœ ๋ถˆํ•„์š”ํ•œ ์ž‘์—…์ด๋‹ค.
    • Car๊ฐ€ ์–ด๋–ป๊ฒŒ EngineFactory๋ฅผ ๋งŒ๋“œ๋Š”์ง€ ๋˜ ์–ด๋–ป๊ฒŒ ์—”์ง„์„ ๋งŒ๋“œ๋Š”์ง€ ์•„๋Š” ๊ฒƒ์€ ์–ด๋ฆฌ์„์ธ ๊ฒƒ์ด๋‹ค.
    • ์ด ํ…Œ์ŠคํŠธ์˜ ๋ฌธ์ œ๋ฅผ ํ•ด์†Œํ•œ๋‹ค๊ณ  ํ•ด๋„ AccountView๋ฅผ ์ƒ์„ฑํ•ด์•ผ ํ•˜๋Š” ๋ชจ๋“  ํ…Œ์ŠคํŠธ๋Š” ์œ„์™€ ๊ฐ™์€ ๋ถˆํ•ฉ๋ฆฌํ•œ ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•ด์•ผ ํ•œ๋‹ค.
    • Car constructor๊ฐ€ ํ˜ธ์ถœ๋˜๋Š” ๋ชจ๋“  ํ…Œ์ŠคํŠธ๋Š” file์„ ์ ‘๊ทผํ•ด์•ผ ํ•œ๋‹ค. ์ด ์ž‘์—…์€ ๋งค์šฐ ๋А๋ฆฌ๊ณ , ํ…Œ์ŠคํŠธ๊ฐ€ ์ง„์ •ํ•œ ์œ ๋‹› ํ…Œ์ŠคํŠธ๊ฐ€ ๋  ์ˆ˜ ์—†๊ฒŒ ํ•œ๋‹ค.
  • ์ด๋Ÿฌํ•œ 3rd party ๊ฐ์ฒด๋ฅผ ์ œ๊ฑฐํ•˜๊ณ  constructor์—์„œ์˜ ์ž‘์—…์„ ๋‹จ์ˆœํ•œ ๋ณ€์ˆ˜ ํ• ๋‹น๋ฌธ์œผ๋กœ ์น˜ํ™˜ํ•˜๋ผ. ์‚ฌ์ „์— ์„ค์ •๋œ ๋ณ€์ˆ˜๋“ค์„ constructor์˜ ํ•„๋“œ๋กœ ํ• ๋‹นํ•˜๋ผ. ๋‹ค๋ฅธ ๊ฐ์ฒด(factory, builder, DI container)๊ฐ€ constructor์˜ parameter๋ฅผ ์ƒ์„ฑํ•˜๋Š” ์ž‘์—…์„ ๋‹ด๋‹นํ•˜๋„๋ก ํ•˜๋ผ. ๊ฐ์ฒด์˜ ์ฃผ์š” ์ฑ…์ž„๊ณผ ๊ฐ์ฒด ๊ทธ๋ž˜ํ”„ ์ƒ์„ฑ์„ ๋ถ„๋ฆฌํ•˜์—ฌ ๋ณด๋‹ค ์œ ์—ฐํ•˜๊ณ  ์œ ์ง€๋ณด์ˆ˜ ๊ฐ€๋Šฅํ•œ ์„ค๊ณ„๋ฅผ ์œ ์ง€ํ•˜๋ผ.
Directly Reading Flag Values in Constructor
  • Before: Hard to Test
    • SUT
// Reading flag values to create collaborators
 
class PingServer {
    Socket socket;
    PingServer() {
        socket = new Socket(FLAG_PORT.get());
    }
 
    // ...
}
- Test
// The test is brittle and tied directly to a
//   Flag's static method (global state).
 
class PingServerTest extends TestCase {
    public void testWithDefaultPort() {
        PingServer server = new PingServer();
        // This looks innocent enough, but really
        //   it forces you to mutate global state
        //   (the flag) to run on another port.
    }
}
  • After: Testable and Flexible Design
    • SUT
// Best solution (although you also could pass
//   in an int of the Socket's port to use)
 
class PingServer {
    Socket socket;
 
    @Inject
    PingServer(Socket socket) {
        this.socket = socket;
    }
}
 
// This uses the FlagBinder to bind Flags to
// the @Named annotation values. Somewhere in
// a Module's configure method:
new FlagBinder(
    binder().bind(FlagsClassX.class));
 
// And the method provider for the Socket
@Provides
Socket getSocket(@Named("port") int port) {
    // The responsibility of this provider is
    //   to give a fully configured Socket
    //   which may involve more than just "new"
    return new Socket(port);
}
- Test
// The revised code is flexible, and easily
//   tested (without any global state). 
 
class PingServerTest extends TestCase {
  public void testWithNewPort() {
    int customPort = 1234;
    Socket socket = new Socket(customPort);
    PingServer server = new PingServer(socket);
 
    // ...
  }
}
  • ์ธ์ž๋ฅผ ๊ฐ–์ง€ ์•Š๋Š” constructor๊ฐ€ ๋งŽ์€ ์˜์กด์„ฑ์„ ๊ฐ€์ง€๊ณ  ์žˆ๋‹ค.API๊ฐ€ ๊ฑฐ์ง“์„ ๋งํ•˜๊ณ  ์žˆ๋Š” ๊ฒƒ์ด๋‹ค. API๋Š” ์ธ์ž๊ฐ€ ์—†์œผ๋ฏ€๋กœ ์‰ฝ๊ฒŒ ๋งŒ๋“ค์ˆ˜ ์žˆ๋‹ค๊ณ  ๋ง๊ณ ํ•˜๊ณ  ์žˆ์ง€๋งŒ PingServer๋Š” ๋ถˆ์•ˆ์ •ํ•˜๊ณ  ์ „์—ญ ์ƒํƒœ์— ์˜์กดํ•˜๊ณ  ์žˆ๋‹ค.
  • Flaws:
    • ํ…Œ์ŠคํŠธ๋Š” ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•˜๊ธฐ ์œ„ํ•ด ์ „์—ญ ๋ณ€์ˆ˜ FLAG_PORT์— ์˜์กดํ•˜๊ณ  ์žˆ๋‹ค. ํ…Œ์ŠคํŠธ ์ˆœ์„œ์— ์˜ํ•ด ํ…Œ์ŠคํŠธ๊ฐ€ ์˜ํ–ฅ์„ ๋ฐ›๊ฒŒ ๋œ๋‹ค.
    • statiticํ•˜๊ฒŒ ์ ‘๊ทผ ๊ฐ€๋Šฅํ•œ ์ „์—ญ ๋ณ€์ˆ˜ ํ”Œ๋ž˜๊ทธ๋กœ ์ธํ•ด ๋ณ‘๋ ฌ๋กœ ํ…Œ์ŠคํŠธ ์ˆ˜ํ–‰์ด ๋ถˆ๊ฐ€ํ•ด ์ง„๋‹ค.
    • ๊ฐ์ฒด ์ƒ์„ฑ์ด ์ž˜๋ชป๋œ ๊ณณ์—์„œ ์ˆ˜ํ–‰๋˜๊ณ  ์žˆ์–ด์„œ Socket์— ์ถ”๊ฐ€์ ์ธ ์„ค์ •(setSoTimeout ํ˜ธ์ถœ ๋“ฑ)์ด ๋ถˆ๊ฐ€ํ•˜๋‹ค.
  • PingServer๋Š” port ๋ฒˆํ˜ธ๊ฐ€ ์•„๋‹ˆ๋ผ ์†Œ์ผ“์„ ํ•„์š”๋กœ ํ•œ๋‹ค. port ๋ฒˆํ˜ธ๋ฅผ ์ „๋‹ฌํ•จ์œผ๋กœ์จ ํ…Œ์ŠคํŠธ์‹œ ์‹ค์ œ ์†Œ์ผ“/์“ฐ๋ ˆ๋“œ๋ฅผ ์‚ฌ์šฉํ•ด์•ผ๋งŒ ํ•œ๋‹ค. ํฌํŠธ ๋ฒˆํ˜ธ๊ฐ€ ์•„๋‹ˆ๋ผ ์†Œ์ผ“์„ ์ „๋‹ฌํ•˜๋„๋ก ์ˆ˜์ •ํ•˜๋ฉด ํ…Œ์ŠคํŠธ์‹œ mock ์†Œ์ผ“์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.
  • ๋ช…์‹œ์ ์œผ๋กœ port ๋ฒˆํ˜ธ๋ฅผ ์ „๋‹ฌํ•จ์œผ๋กœ์จ ์ „์—ญ ์ƒํƒœ์— ๋Œ€ํ•œ ์˜์กด์„ฑ์„ ์ œ๊ฑฐํ•˜์—ฌ ํ…Œ์ŠคํŠธ๋ฅผ ๋‹จ์ˆœํ™”ํ•  ์ˆ˜ ์žˆ๋‹ค. ๊ถ๊ทน์˜ ํ•ด๊ฒฐ์ฑ…์€ ์ง„์งœ๋กœ ํ•„์š”ํ•œ ์†Œ์ผ“์„ ์ „๋‹ฌํ•˜๋Š” ๊ฒƒ์ด๋‹ค.
Directly Reading Flags and Creating Objects in Constructor
  • Before: Hard to Test
    • SUT
// Branching on flag values to determine state.
 
class CurlingTeamMember {
  Jersey jersey;
 
  CurlingTeamMember() {
    if (FLAG_isSuedeJersey.get()) {
      jersey = new SuedeJersey();
    } else {
      jersey = new NylonJersey();
    }
  }
}
- Test
// Testing the CurlingTeamMember is difficult.
//   In fact you can't use any Jersey other
//   than the SuedeJersey or NylonJersey.
 
class CurlingTeamMemberTest extends TestCase {
  public void testImpossibleToChangeJersey() {
    //  You are forced to use global state.
    // ... Set the flag how you want it
    CurlingTeamMember russ =
        new CurlingTeamMember();
 
    // Tests are locked in to using one
    //   of the two jerseys above.
  }
}
  • After: Testable and Flexible Design
    • SUT
// We moved the responsibility of the selection
//   of Jerseys into a provider.
 
class CurlingTeamMember {
  Jersey jersey;
 
  // Recommended, because responsibilities of
  // Construction/Initialization and whatever
  // this object does outside it's constructor
  // have been separated.
  @Inject
  CurlingTeamMember(Jersey jersey) {
    this.jersey = jersey;
  }
}
 
// Then use the FlagBinder to bind flags to
//   injectable values. (Inside your Module's
//   configure method)
  new FlagBinder(
      binder().bind(FlagsClassX.class));
 
// By asking for Provider<SuedeJersey>
//   instead of calling new SuedeJersey
//   you leave the SuedeJersey to be free
//   to ask for its dependencies.
@Provides
Jersey getJersey(
     Provider<SuedeJersey> suedeJerseyProvider,
     Provider<NylonJersey> nylonJerseyProvider,
     @Named('isSuedeJersey') suede) {
  if (sued) {
    return suedeJerseyProvider.get();
  } else {
    return nylonJerseyProvider.get();
  }
}
- Test
// We moved the responsibility of the selection
//   of Jerseys into a provider.
 
class CurlingTeamMember {
  Jersey jersey;
 
  // Recommended, because responsibilities of
  // Construction/Initialization and whatever
  // this object does outside it's constructor
  // have been separated.
  @Inject
  CurlingTeamMember(Jersey jersey) {
    this.jersey = jersey;
  }
}
 
// Then use the FlagBinder to bind flags to
//   injectable values. (Inside your Module's
//   configure method)
  new FlagBinder(
      binder().bind(FlagsClassX.class));
 
// By asking for Provider<SuedeJersey>
//   instead of calling new SuedeJersey
//   you leave the SuedeJersey to be free
//   to ask for its dependencies.
@Provides
Jersey getJersey(
     Provider<SuedeJersey> suedeJerseyProvider,
     Provider<NylonJersey> nylonJerseyProvider,
     @Named('isSuedeJersey') suede) {
  if (sued) {
    return suedeJerseyProvider.get();
  } else {
    return nylonJerseyProvider.get();
  }
}
  • Flaws:
    • ํ”Œ๋ž˜๊ทธ๋ฅผ ์ง์ ‘ ์ฝ๋Š” ๊ฒƒ์€ ๊ฐ’์„ ์–ป๊ธฐ ์œ„ํ•ด ์ „์—ญ ์ƒํƒœ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด๋‹ค. ์ „์—ญ ์ƒํƒœ๊ฐ€ ๋ถ„๋ฆฌ(isolate)๋˜์ง€ ์•Š์•„์„œ ์•…์˜ํ–ฅ์ด ์ƒ๊ธด๋‹ค. ์ด์ „ ํ…Œ์ŠคํŠธ๋‚˜ ๋™์‹œ์— ์ˆ˜ํ–‰๋˜๋Š” ๋‹ค๋ฅธ ์“ฐ๋ ˆ๋“œ๊ฐ€ ์˜ˆ์ƒํ•˜์ง€ ์•Š์€ ์ƒํƒœ๋กœ ์„ค์ •ํ•  ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.
    • ํ”Œ๋ž˜๊ทธ ๊ฐ’์— ๋”ฐ๋ผ ๋‹ค๋ฅธ ํƒ€์ž…์˜ Jersey๋ฅผ ์ง์ ‘ ์ƒ์„ฑํ•˜๊ณ  ์žˆ๋‹ค. CurlingTeamMember๋ฅผ ์ƒ์„ฑํ•˜๋Š” ํ…Œ์ŠคํŠธ๋Š” ๋‹ค๋ฅธ Jersey collaborator๋ฅผ ์ฃผ์ž…ํ•  seam์„ ๊ฐ–์ง€ ๋ชปํ•œ๋‹ค.
    • CurlingTeamMember์˜ ์ฑ…์ž„์ด ๊ด‘๋ฒ”์œ„ํ•˜๋‹ค.
Moving the Constructor's "work" into an Initialize Method
  • Before: Hard to Test
    • SUT
// With statics, singletons, and a tricky
//   initialize method this class is brittle.
 
class VisualVoicemail {
  User user;
  List<Call> calls;
 
  @Inject
  VisualVoicemail(User user) {
    // Look at me, aren't you proud? I've got
    // an easy constructor, and I use Guice
    this.user = user;
  }
 
  initialize() {
     Server.readConfigFromFile();
     Server server = Server.getSingleton();
     calls = server.getCallsFor(user);
  }
 
  // This was tricky, but I think I figured
  // out how to make this testable!
  @VisibleForTesting
  void setCalls(List<Call> calls) {
    this.calls = calls;
  }
 
  // ...
}
- Test
// Brittle code exposed through the test
 
class VisualVoicemailTest extends TestCase {
 
  public void testExposesBrittleDesign() {
    User dummyUser = new DummyUser();
    VisualVoicemail voicemail =
        new VisualVoicemail(dummyUser);
    voicemail.setCalls(buildListOfTestCalls());
 
    // Technically this can be tested, as long
    //   as you don't need the Server to have
    //   read the config file. But testing
    //   without testing the initialize()
    //   excludes important behavior.
 
    // Also, the code is brittle and hard to
    //   later on add new functionalities.
  }
}
  • After: Testable and Flexible Design
    • SUT
// Using DI and Guice, this is a
//   superior design.
 
class VisualVoicemail {
  List<Call> calls;
 
  VisualVoicemail(List<Call> calls) {
    this.calls = calls;
  }
}
 
// You'll need a provider to get the calls
@Provides
List<Call> getCalls(Server server,
    @RequestScoped User user) {
  return server.getCallsFor(user);
}
 
// And a provider for the Server. Guice will
//  let you get rid of the JVM Singleton too.
@Provides @Singleton
Server getServer(ServerConfig config) {
  return new Server(config);
}
 
@Provides @Singleton
ServerConfig getServerConfig(
    @Named("serverConfigPath") path) {
  return new ServerConfig(new File(path));
}
 
// Somewhere, in your Module's configure()
//   use the FlagBinder.
  new FlagBinder(binder().bind(
      FlagClassX.class))
- Test
// Dependency Injection exposes your
//   dependencies and allows for seams to
//   inject different collaborators.
 
class VisualVoicemailTest extends TestCase {
 
  VisualVoicemail voicemail =
      new VisualVoicemail(
          buildListOfTestCalls());
 
  // ... now you can test this however you want.
}
  • โ€œworkโ€๋ฅผ initialize ๋ฉ”์†Œ๋“œ๋กœ ์ด๋™์‹œํ‚ค๋Š” ๊ฒƒ์€ ํ•ด๊ฒฐ์ฑ…์ด ์•„๋‹ˆ๋‹ค. ๊ฐ์ฒด๊ฐ€ ํ•˜๋‚˜์˜ ์ฑ…์ž„๋งŒ ๊ฐ–๋„๋ก decoupleํ•ด์•ผ ํ•œ๋‹ค(์ด๋•Œ ํ•˜๋‚˜์˜ ์ฑ…์ž„์€ ์™„์ „ํ•˜๊ฒŒ ์„ค์ •๋œ ๊ฐ์ฒด ๊ทธ๋ž˜ํ”„๋ฅผ ์ œ๊ณตํ•˜๋Š” ๊ฒƒ์ด๋‹ค).
  • Flaws:
    • ์ฝ”๋“œ์˜ ์•ˆ์ •์„ฑ์ด ์—†๊ณ  ๋ช‡๊ฐœ์˜ statitic initialization ํ˜ธ์ถœ์— ๊ฒฐ๋ถ€๋˜์–ด ์žˆ๋‹ค.
    • initialization ๋ฉ”์†Œ๋“œ๋Š” ๊ฐ์ฒด๊ฐ€ ๋„ˆ๋ฌด ๋งŽ์€ ์ฑ…์ž„์„ ๊ฐ–๋Š”๋‹ค๋Š” ๊ฒƒ์„ ๋‚˜ํƒ€๋‚ด๋Š” ํ˜„๊ฒฉํ•œ ์ฆ๊ฑฐ์ด๋‹ค: ์˜์กด์„ฑ initialization์€ ๋‹ค๋ฅธ ํด๋ž˜์Šค์—์„œ ์ˆ˜ํ–‰๋˜์–ด์•ผ ํ•˜๊ณ , ๋ฐ”๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ๊ฐ์ฒด๋“ค์ด constructor์— ์ „๋‹ฌ๋˜์–ด์•ผ ํ•œ๋‹ค.
    • ํ…Œ์ŠคํŠธ์‹œ initialize ๋ฉ”์†Œ๋“œ๋ฅผ ํ˜ธ์ถœํ•˜๊ณ ์ž ํ•œ๋‹ค๋ฉด Server.readConfigFromFile ๋ฉ”์†Œ๋“œ๋Š” intercept ๋ถˆ๊ฐ€ํ•˜๋‹ค.
    • ํ…Œ์ŠคํŠธ์‹œ Server๋Š” initialize ๋ถˆ๊ฐ€ํ•˜๋‹ค. Server๋ฅผ ์‚ฌ์šฉํ•˜๊ณ ์ž ํ•œ๋‹ค๋ฉด ์ „์—ญ singleton ์ƒํƒœ์—์„œ ์–ป์–ด์™€์•ผ ํ•œ๋‹ค. 2๊ฐœ์˜ ํ…Œ์ŠคํŠธ๊ฐ€ ๋™์‹œ์— ์ˆ˜ํ–‰๋˜๊ฑฐ๋‚˜ ์ด์ „ ํ…Œ์ŠคํŠธ๊ฐ€ Server๋ฅผ ์˜ˆ์ƒํ•˜์ง€ ์•Š์€ ์ƒํƒœ๋กœ ์ดˆ๊ธฐํ™”ํ–ˆ๋‹ค๋ฉด ์ „์—ญ ์ƒํƒœ๋กœ ์ธํ•ด ํ…Œ์ŠคํŠธ๊ฐ€ ์‹คํŒจํ•œ๋‹ค.
Having Multiple Constructors, where one is Just for Testing
  • Before: Hard to Test
    • SUT
// Half way easy to construct. The other half
//   expensive to construct. And for collaborators
//   that use the expensive constructor - they
//   become expensive as well.
 
class VideoPlaylistIndex {
  VideoRepository repo;
 
  @VisibleForTesting
  VideoPlaylistIndex(
      VideoRepository repo) {
    // Look at me, aren't you proud?
    // An easy constructor for testing!
    this.repo = repo;
  }
 
  VideoPlaylistIndex() {
    this.repo = new FullLibraryIndex();
  }
 
  // ...
 
}
 
// And a collaborator, that is expensive to build
//   because the hard coded index construction.
 
class PlaylistGenerator {
 
  VideoPlaylistIndex index =
      new VideoPlaylistIndex();
 
  Playlist buildPlaylist(Query q) {
    return index.search(q);
  }
}
- Test
// Testing the VideoPlaylistIndex is easy,
//  but testing the PlaylistGenerator is not!
 
class PlaylistGeneratorTest extends TestCase {
 
  public void testBadDesignHasNoSeams() {
    PlaylistGenerator generator =
        new PlaylistGenerator();
    // Doh! Now we're tied to the
    //   VideoPlaylistIndex with the bulky
    //   FullLibraryIndex that will make slow
    //   tests.
  }
}
  • After: Testable and Flexible Design
    • SUT
// Easy to construct, and no other objects are
//   harmed by using an expensive constructor.
 
class VideoPlaylistIndex {
  VideoRepository repo;
 
  VideoPlaylistIndex(
      VideoRepository repo) {
    // One constructor to rule them all
    this.repo = repo;
  }
}
 
// And a collaborator, that is now easy to
//   build.
 
class PlaylistGenerator {
  VideoPlaylistIndex index;
 
  // pass in with manual DI
  PlaylistGenerator(
      VideoPlaylistIndex index) {
    this.index = index;
  } 
 
  Playlist buildPlaylist(Query q) {
    return index.search(q);
  }
}
- Test
// Easy to test when Dependency Injection
//   is used everywhere. 
 
class PlaylistGeneratorTest extends TestCase {
 
  public void testFlexibleDesignWithDI() {
    VideoPlaylistIndex fakeIndex =
        new InMemoryVideoPlaylistIndex()
    PlaylistGenerator generator =
        new PlaylistGenerator(fakeIndex);
 
    // Success! The generator does not care
    //   about the index used during testing
    //   so a fakeIndex is passed in.
  }
}
  • ๋‹ค์ค‘ constructor(์ผ๋ถ€๋Š” ํ…Œ์ŠคํŠธ์—์„œ๋งŒ ์‚ฌ์šฉ๋˜๋Š”)๋Š” ์ฝ”๋“œ์˜ ์ผ๋ถ€๊ฐ€ ์—ฌ์ „ํžˆ ํ…Œ์ŠคํŠธํ•˜๊ธฐ ์–ด๋ ต๋‹ค๋Š” ๊ฒƒ์„ ๋‚˜ํƒ€๋‚ธ๋‹ค. VideoRepository๋ฅผ ์ธ์ž๋กœ ๊ฐ–๋Š” Constructor๋กœ ์ธํ•ด VideoPlaylistIndex๋Š” ํ…Œ์ŠคํŠธํ•˜๊ธฐ ์‰ฝ๋‹ค. ํ•˜์ง€๋งŒ ์ธ์ž๊ฐ€ ์—†๋Š” constructor๋กœ ์ธํ•ด ํ…Œ์ŠคํŠธ๊ฐ€ ์–ด๋ ค์›Œ์ง„๋‹ค.
  • Flaws:
    • PlaylistGenerator๋Š” ํ…Œ์ŠคํŠธํ•˜๊ธฐ ์–ด๋ ต๋‹ค. VideoPlaylistIndex์˜ ๋””ํดํŠธ constructor๋ฅผ ์‚ฌ์šฉํ•˜๋„๋ก ํ•˜๋“œ์ฝ”๋”ฉ๋˜์–ด ์žˆ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค(FullLibraryIndex๊ฐ€ ์‚ฌ์šฉ๋จ). PlaylistGenerator๋ฅผ ํ…Œ์ŠคํŠธํ•  ๋•Œ FullLibraryIndex์— ๋Œ€ํ•œ ํ…Œ์ŠคํŠธ๋Š” ๋ฐฐ์ œํ•˜๊ณ  ์‹ถ์–ด๋„ ๊ทธ๋Ÿด ์ˆ˜ ์—†๋‹ค.
    • ์ด์ƒ์ ์œผ๋กœ๋Š” PlaylistGenerator์˜ constructor๊ฐ€ VideoPlaylistIndex๋ฅผ ์ง์ ‘ ์ƒ์„ฑํ•˜๋Š” ๋Œ€์‹  VideoPlaylistIndex์— ์š”์ฒญํ•ด์•ผ ํ•œ๋‹ค. PlaylistGenerator์—์„œ VideoPlaylistIndex์˜ ๋””ํดํŠธ constructor ์‚ฌ์šฉ์„ ์—†์• ๋ฉด VideoPlaylistIndex์˜ ๋””ํดํŠธ constructor๋ฅผ ์ œ๊ฑฐํ•  ์ˆ˜ ์žˆ๋‹ค. ๋Œ€๊ฐœ์˜ ๊ฒฝ์šฐ ๋‹ค์ค‘ constructor๋Š” ๋ถˆํ•„์š”ํ•˜๋‹ค.

Flaw #2: Digging into Collaborators

์›๋ฌธ

  • โ€œholderโ€, โ€œcontextโ€, โ€œkitchen sinkโ€ ๊ฐ™์€ ๊ฐ์ฒด๋“ค(๋ชจ๋‘ ๋ฉ”์†Œ๋“œ์—์„œ ์ง์ ‘์ ์œผ๋กœ ํ•„์š”ํ•œ Specific Object์— ๋Œ€ํ•œ ์ ‘๊ทผ์„ ์ œ๊ณตํ•˜๋Š” grab bag ์—ญํ• )์˜ ์‚ฌ์šฉ์„ ํ”ผํ•˜๋ผ.
  • method์—์„œ ์ง์ ‘์ ์œผ๋กœ ์‚ฌ์šฉํ•  ํ•„์š”๊ฐ€ ์žˆ๋Š” ๊ฐ์ฒด๋Š” method๋‚˜ constructor์˜ parameter๋กœ ์ „๋‹ฌํ•˜๋ผ.
  • User ๊ฐ์ฒด(Holder)๊ฐ€ ์žˆ๊ณ , ๋ฉ”์†Œ๋“œ์—์„œ Address ๊ฐ์ฒด(Specific Object)๊ฐ€ ์ง์ ‘์ ์œผ๋กœ ํ•„์š”ํ•  ๋•Œ
    • Holder(User)๋ฅผ ์ „๋‹ฌํ•˜์—ฌ Specific Object๋ฅผ ์–ป์ง€ ๋ง๊ณ (User#getAddress() ํ˜ธ์ถœ์„ ํ†ตํ•ด), ์ง์ ‘์ ์œผ๋กœ ํ•„์š”ํ•œ Specific Object๋ฅผ method๋‚˜ constructor์˜ parameter๋กœ ์ „๋‹ฌํ•˜๋ผ.

Warning Signs

  • โ€œTrain Wreckโ€ or a โ€œLaw of Demeterโ€ violation
  • ์ „๋‹ฌ๋œ ๊ฐ์ฒด๊ฐ€ ์ง์ ‘์ ์œผ๋กœ ์‚ฌ์šฉ๋˜์ง€ ์•Š์€ ๊ฒฝ์šฐ(๋‹ค๋ฅธ ๊ฐ์ฒด๋ฅผ ์–ป๊ธฐ ์œ„ํ•ด์„œ๋งŒ ์‚ฌ์šฉ๋˜๋Š” ๊ฒฝ์šฐ) โ€“ Holder
  • ํ…Œ์ŠคํŠธ์—์„œ mock์„ ๋ฐ˜ํ™˜ํ•˜๋Š” mock์„ ์ƒ์„ฑํ•ด์•ผ ํ•˜๋Š” ๊ฒฝ์šฐ
  • Law of Demeter violation: ํ•˜๋‚˜ ์ด์ƒ์˜ ๋„ํŠธ๋ฅผ ์ด์šฉํ•˜์—ฌ ๊ฐ์ฒด ๊ทธ๋ž˜ํ”„๋ฅผ ํƒ์ƒ‰ํ•˜์—ฌ ๋ฉ”์†Œ๋“œ ํ˜ธ์ถœ ์ฒด์ธ์ด ๋ฐœ์ƒํ•˜๋Š” ๊ฒฝ์šฐ
  • ์˜์‹ฌ์Šค๋Ÿฌ์šด ์ด๋ฆ„๋“ค์ด ์‚ฌ์šฉ๋˜๋Š” ๊ฒฝ์šฐ. eg. context, environment, principal, container, or manager ๋“ฑ
  • fixture setup์ด ๋„ˆ๋ฌด ๋ณต์žกํ•ด์„œ ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•˜๊ธฐ ์–ด๋ ค์šด ๊ฒฝ์šฐ

Why this is a Flaw

  • Deceitful API
    • ์นด๋“œ๋ฅผ ํ†ตํ•œ ๊ฒฐ์žฌ์˜ ๊ฒฝ์šฐ๋ฅผ ์˜ˆ๋กœ ๋“ค์–ด๋ณด์ž.
    • ๋ฉ”์†Œ๋“œ ์‹œ๊ทธ๋‹ˆ์ฒ˜์—์„œ๋Š” ๋‹จ์ˆœํ•œ ๋ฌธ์ž์—ด์ด ์นด๋“œ๋ฒˆํ˜ธ๊ฐ€ ํ•„์š”ํ•˜๋‹ค๊ณ  ๋˜์–ด ์žˆ๋‹ค.
    • ๋ฉ”์†Œ๋“œ ๋ฐ”๋””์—์„œ๋Š” CardProcessor๋‚˜ PaymentGateway ๋“ฑ ์‹ค์ œ๋กœ ์‚ฌ์šฉํ•  ๊ฐ์ฒด๋ฅผ ๊ตฌํ•ด์„œ ์นด๋“œ๋ฒˆํ˜ธ๋ฅผ ์ด์šฉํ•œ ๊ฒฐ์žฌ๋ฅผ ํ•ด์•ผ ํ•œ๋‹ค.
    • ๋ฉ”์†Œ๋“œ ์‹œ๊ทธ๋‹ˆ์ฒ˜์— ์‹ค์ œ ์˜์กด์„ฑ(String์ด ์•„๋‹ˆ๋ผ CardProcessor๋‚˜ PaymentGateway)์˜ ํ‘œํ˜„๋˜์ง€ ์•Š๋Š”๋‹ค.
  • Makes for Brittle Code
    • Holder๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ ๋ณ€๊ฒฝ์ด ์š”๊ตฌ๋  ๋•Œ ์ƒˆ๋กœ์šด ์ƒํ˜ธ์ž‘์šฉ ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•ด ๋ชจ๋“  Holder๋ฅผ ์ˆ˜์ •ํ•ด์•ผ ํ•œ๋‹ค.
    • ๋˜ํ•œ ์ฝ”๋“œ๊ฐ€ ์ ์ง„์ ์œผ๋กœ Holder์™€ ๊ฐ™์€ Intermediary์— ์ข…์†์ ์œผ๋กœ ๋˜์–ด ๋ณด๋‹ค ๋ณต์žกํ•ด ์ง„๋‹ค.
    • Specific Object๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค๋ฉด ์ฝ”๋“œ์˜ ์•ˆ์ •์„ฑ์„ ๋†’์ผ ์ˆ˜ ์žˆ๋‹ค.
    • ์ด ๊ฒฝ์šฐ๋„ ํ•˜๋‚˜ ์ด์ƒ์˜ ์ฑ…์ž„์„ ๊ฐ–๋Š” class๋ฅผ ๋ถ„๋ฆฌํ•˜๋Š” ๊ฒฝ์šฐ๋Š” ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋‹ค.
    • ํ•˜์ง€๋งŒ ๊ฑฑ์ •ํ•˜์ง€ ๋ง๊ณ  ๋ฐ˜๋“œ์‹œ SRP(Single Reponsibility Principle) ๊ณ ์ˆ˜ํ•˜๋ผ.
  • Bloats your code and complicates what's really happening
    • ์‹ค์ œ๋กœ ์‚ฌ์šฉํ•  ๊ฐ์ฒด๋ฅผ Holder์™€ ๊ฐ™์€ intermediary๋ฅผ ํ†ตํ•ด ์–ป๋Š” ๊ณผ์ •์„ ๋ถˆํ•„์š”ํ•˜๊ฒŒ ์ฝ”๋“œ์— ์œ ์ง€ํ•จ์œผ๋กœ์จ ์ฝ”๋“œ์˜ ๊ธธ์ด/ํ˜ผ๋ž€์ด ์ฆ๊ฐ€ํ•œ๋‹ค.
  • Hard for Testing
    • Holder๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๋ฉ”์†Œ๋“œ๋ฅผ ํ…Œ์ŠคํŠธํ•  ๋•Œ ํ•ด๋‹น ๋ฉ”์†Œ๋“œ๊ฐ€ Holder์—์„œ ๋ฌด์—‡์„ ์š”๊ตฌํ•  ์ง€ ์ถ”์ธกํ•˜๊ธฐ ์–ด๋ ต๋‹ค(๋ฌด์—‡์€ ์ƒ๊ด€์—†์„์ง€๋„ ์ถ”์ธก์ด ์–ด๋ ต๋‹ค).
    • ๋‹ค์‹œ ๋งํ•ด empty holder๋ฅผ ๋„˜๊ธด ํ›„ NPE(NullPointerException)๋ฅผ ๋ฐœ์ƒ์‹œํ‚ค๊ณ , NPE๊ฐ€ ๋ฐœ์ƒํ•˜์ง€ ์•Š๋„๋ก Holder์˜ ์ƒํƒœ๋ฅผ ์ฒ˜๋ฆฌํ•˜๊ณ  ๋‹ค์‹œ ์‹œ๋„ํ•˜๋Š” ์ง€๋ฃจํ•œ ๋ฐ˜๋ณต ๋ฐฉ์‹์ด ๋œ๋‹ค.

Recognizing the Flaw

  • ์ด ๋ฌธ์ œ๋Š” โ€œTrain Wreckโ€, โ€œLaw of Demeterโ€ ์œ„๋ฐ˜์œผ๋กœ๋„ ์•Œ๋ ค์ ธ์žˆ๋‹ค.
  • ์•„๋ž˜์™€ ๊ฐ™์€ ์ฆ์ƒ์œผ๋กœ ์‹๋ณ„ํ•  ์ˆ˜ ์žˆ๋‹ค.
    • ํ…Œ์ŠคํŠธ ์ž‘์„ฑ์‹œ mock์„ ๋ฐ˜ํ™˜ํ•˜๋Š” mock์„ ์ƒ์„ฑํ•ด์•ผ ํ•˜๋Š” ๊ฒฝ์šฐ
    • โ€œcontextโ€๋ผ๋Š” ์ด๋ฆ„์„ ๊ฐ€์ง„ ๊ฐ์ฒด๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ
    • ๋‘˜ ์ด์ƒ์˜ ๋„ํŠธ๊ฐ€ method chaining์— ๋ฐœ์ƒํ•˜๊ณ  ํ•ด๋‹น method๊ฐ€ getters์ธ ๊ฒฝ์šฐ
    • ๋ณต์žกํ•œ fixture setup ๋•Œ๋ฌธ์— ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•˜๊ธฐ ์–ด๋ ค์šด ๊ฒฝ์šฐ

Fixing the Flaw

  • ํ•„์š”ํ•œ ๊ฐ์ฒด๋ฅผ ์–ป์œผ๋ ค ํ•˜์ง€ ๋ง๊ณ  ์‹ค์ œ๋กœ ํ•„์š”ํ•œ ๊ฐ์ฒด๋ฅผ method๋‚˜ constructor์˜ parameter๋กœ ์ „๋‹ฌํ•˜๋ผ.
  • ์•„๋ž˜ ์›์น™์„ ๊ณ ์ˆ˜ํ•˜๋ผ:
    • ๋ฐ˜๋“œ์‹œ 1์ดŒ ์นœ๊ตฌ(immediate friends)์™€๋งŒ ์ƒํ˜ธ์ž‘์šฉํ•˜๋ผ.
    • ์ง„์งœ๋กœ ํ•„์š”ํ•œ ๊ฐ์ฒด๋งŒ constructor์— injectํ•˜๊ฑฐ๋‚˜ method์— paramter๋กœ ์ „๋‹ฌํ•˜๋ผ.
    • ๊ฐ์ฒด๋ฅผ ์ฐพ๊ฑฐ๋‚˜ ์„ค์ •ํ•˜๋Š” ์ฑ…์ž„์€ factory๋‚˜ DI Container(spring)์— ์œ„์ž„ํ•˜๋ผ.

Examples by Diagrams

์ž˜๋ชป๋œ ์˜ˆ

์ž˜๋œ ์˜ˆ

Examples by Codes

Service Object Digging Around in Value Object
  • Before: Hard to Test
    • SUT
// This is a service object that works with a value
// object (the User and amount). 
 
class SalesTaxCalculator {
    TaxTable taxTable;
 
    SalesTaxCalculator(TaxTable taxTable) {
        this.taxTable = taxTable;
    }
 
    float computeSalesTax(User user, Invoice invoice) {
        // note that "user" is never used directly
        Address address = user.getAddress(); // holder
        float amount = invoice.getSubTotal(); // holder
        return amount * taxTable.getTaxRate(address);
    }
}
- Test
// Testing exposes the problem by the amount of work
//   necessary to build the object graph, and test the
//   small behavior you are interested in.
 
class SalesTaxCalculatorTest extends TestCase {
    SalesTaxCalculator calc = new SalesTaxCalculator(new TaxTable());
    // So much work wiring together all the objects needed
    Address address = new Address("1600 Amphitheatre Parkway...");
    User user = new User(address);
    Invoice invoice = new Invoice(1, new ProductX(95.00));
    // ...
    assertEquals(0.09, calc.computeSalesTax(user, invoice), 0.05);
}
  • After: Testable and Flexible Design
    • SUT
// Reworked, it only asks for the specific objects
// that it needs to collaborate with.
 
class SalesTaxCalculator {
    TaxTable taxTable;
    SalesTaxCalculator(TaxTable taxTable) {
        this.taxTable = taxTable;
    }
 
    // Note that we no longer use User, nor do we dig inside
    // the address. (Note: We would use a Money, BigDecimal,
    // etc. in reality).
    float computeSalesTax(Address address, float amount) {
        return amount * taxTable.getTaxRate(address);
    }
}
- Test
// The new API is clearer in what collaborators it needs.
 
class SalesTaxCalculatorTest extends TestCase {
    SalesTaxCalculator calc = new SalesTaxCalculator(new TaxTable());
    // Only wire together the objects that are needed
    Address address = new Address("1600 Amphitheatre Parkway...");
    // ...
    assertEquals(0.09, calc.computeSalesTax(address, 95.00), 0.05);
    }
}
  • ์œ„ ์˜ˆ์ œ๋Š” calcuation(business logic)๊ณผ object lookup์ด ํ˜ผํ•ฉ๋˜์–ด ์žˆ๋‹ค. ์‹ค์ œ ํด๋ž˜์Šค์˜ ์—ญํ• /์ฑ…์ž„์€ ์„ธ๊ธˆ์„ ๊ณ„์‚ฐํ•˜๋Š” ๊ฒƒ์ด๋‹ค.
  • Flaws:
    • ํ…Œ์ŠคํŠธ์‹œ User, Invoice ๊ฐ์ฒด ์ƒ์„ฑ์ด ๋ถˆํ•„์š”ํ•˜๊ฒŒ ์š”๊ตฌ๋œ๋‹ค.
    • ๋ฉ”์†Œ๋“œ ์‚ฌ์šฉ์ž์˜ ์ž…์žฅ์—์„œ ๋ฉ”์†Œ๋“œ ์‹œ๊ทธ๋‹ˆ์ฒ˜๋Š” ๊ฑฐ์ง“๋ง(deceitful)์„ ํ•˜๊ณ  ์žˆ๋‹ค. ์‹ค์ œ ํ•„์š”ํ•œ ๊ฒƒ์€ ์ฃผ์†Œ์™€ ๊ธˆ์•ก์ธ๋ฐ, API๋Š” User์™€ Invoice๋ผ๊ณ  ์•Œ๋ฆฌ๊ณ  ์žˆ๋‹ค.
    • ์žฌ์‚ฌ์šฉ์„ ํ•˜๊ฒŒ ๋œ๋‹ค๋ฉด Invoice, User์™€ ๊ฐ™์ด ๋ถˆํ•„์š”ํ•œ ํด๋ž˜์Šค๊ฐ€ ์‹ ๊ทœ ์†Œ์Šค์— ํ•„์š”ํ•˜๊ฒŒ ๋œ๋‹ค(์˜์กด์„ฑ(Dependency)์ด ์ฆ๊ฐ€ํ•˜๊ฒŒ ๋œ๋‹ค).
Service Object Directly Violating "Law of Demeter"
  • Before: Hard to Test
    • SUT
// This is a service object which violates the
//   Law of Demeter. 
 
class LoginPage {
    RPCClient client;
    HttpRequest request;
 
    LoginPage(RPCClient client, HttpServletRequest request) {
        this.client = client;
        this.request = request;
    }
 
    boolean login() {
        String cookie = request.getCookie();
        return client.getAuthenticator().authenticate(cookie);
    }
}
- Test
// The extensive and complicated easy mock usage is
//   a clue that the design is brittle.
 
class LoginPageTest extends TestCase {
    public void testTooComplicatedThanItNeedsToBe() {
        Authenticator authenticator = new FakeAuthenticator();
        IMocksControl control = EasyMock.createControl();
        RPCClient client = control.createMock(RPCClient.class);
        EasyMock.expect(client.getAuthenticator()).andReturn(authenticator);
        HttpServletRequest request = control.createMock(HttpServletRequest.class);
        Cookie[] cookies = new Cookie[]{new Cookie("g", "xyz123")};
        EasyMock.expect(request.getCookies()).andReturn(cookies);
        control.replay();
 
        LoginPage page = new LoginPage(client, request);
        // ...
        assertTrue(page.login());
        control.verify();
}
  • Ater: Testable and Flexible Design
    • SUT
// The specific object we need is passed in
//   directly.
 
class LoginPage {
    LoginPage(@Cookie String cookie, Authenticator authenticator) {
        this.cookie = cookie;
        this.authenticator = authenticator;
    }
 
    boolean login() {
        return authenticator.authenticate(cookie);
    }
}
- Test
// Things now have a looser coupling, and are more
//   maintainable, flexible, and testable.
 
class LoginPageTest extends TestCase {
    public void testMuchEasier() {
        Cookie cookie = new Cookie("g", "xyz123");
        Authenticator authenticator = new FakeAuthenticator();
        LoginPage page = new LoginPage(cookie, authenticator);
        // ...
        assertTrue(page.login());
    }
}
  • Flaws:
    • RPCClient๋Š” ์ง์ ‘ ์‚ฌ์šฉ๋˜์ง€ ์•Š๋Š”๋‹ค. ์™œ ์ „๋‹ฌ๋˜์—ˆ๋Š”๊ฐ€ ?
    • HttpRequest๋Š” ์ง์ ‘ ์‚ฌ์šฉ๋˜์ง€ ์•Š๋Š”๋‹ค. ์™œ ์ „๋‹ฌ๋˜์—ˆ๋Š”๊ฐ€ ?
    • Cookie๊ฐ€ ์ง์ ‘ ํ•„์š”ํ•œ ๊ฐ์ฒด์ธ๋ฐ, HttpRequest๋กœ ๋ถ€ํ„ฐ ์–ป์–ด์•ผ ํ•œ๋‹ค. HttpRequest๋ฅผ ํ…Œ์ŠคํŠธ์—์„œ ์„ค์ •ํ•˜๋Š” ๊ฒƒ์€ ์„ฑ๊ฐ€์‹  ์ผ์ด๋‹ค.
    • Authenticator๊ฐ€ ์ง์ ‘ ํ•„์š”ํ•œ ๊ฐ์ฒด์ธ๋ฐ RPCClient๋กœ ๋ถ€ํ„ฐ ์–ป์–ด์˜ค๊ณ  ์žˆ๋‹ค.
Law of Demeter Violated to Inappropriately make a Service Locator
  • Before: Hard to Test
    • SUT
// Database has an single responsibility identity
//   crisis.
 
class UpdateBug {
    Database db;
 
    UpdateBug(Database db) {
        this.db = db;
    }
 
    void execute(Bug bug) {
        // Digging around violating Law of Demeter
        db.getLock().acquire();
        try {
            db.save(bug);
        } finally {
            db.getLock().release();
        }
    }
}
- Test
// Testing even the happy path is complicated with all
// the mock objects that are needed. Especially
// mocks that take mocks (very bad).
 
class UpdateBugTest extends TestCase {
 
    public void testThisIsRidiculousHappyPath() {
        Bug bug = new Bug("description");
 
        // This both violates Law of Demeter and abuses
        //   mocks, where mocks aren't entirely needed.
        IMocksControl control = EasyMock.createControl();
        Database db = control.createMock(Database.class);
        Lock lock = control.createMock(Lock.class);
 
        // Yikes, this mock (db) returns another mock.
        EasyMock.expect(db.getLock()).andReturn(lock);
        lock.acquire();
        db.save(bug);
        EasyMock.expect(db.getLock()).andReturn(lock);
        lock.release();
        control.replay();
        // Now we're done setting up mocks, finally!
 
        UpdateBug updateBug = new UpdateBug(db);
        updateBug.execute(bug);
        // Verify it happened as expected
        control.verify();
        // Note: another test with multiple execute
        //   attempts would need to assert the specific
        //   locking behavior is as we expect.
    }
}
  • Ater: Testable and Flexible Design
    • SUT
// The revised Database has a Single Responsibility.
 
class UpdateBug {
    Database db;
    Lock lock;
 
    UpdateBug(Database db, Lock lock) {
        this.db = db;
    }
 
    void execute(Bug bug) {
        // the db no longer has a getLock method
        lock.acquire();
        try {
            db.save(bug);
        } finally {
            lock.release();
        }
    }
}
// Note: In Database, the getLock() method was removed
- Test
// Two improved solutions: State Based Testing
//   and Behavior Based (Mockist) Testing.
 
// First Sol'n, as State Based Testing.
class UpdateBugStateBasedTest extends TestCase {
    public void testThisIsMoreElegantStateBased() {
        Bug bug = new Bug("description");
 
        // Use our in memory version instead of a mock
        InMemoryDatabase db = new InMemoryDatabase();
        Lock lock = new Lock();
        UpdateBug updateBug = new UpdateBug(db, lock);
 
        // Utilize State testing on the in memory db.
        assertEquals(bug, db.getLastSaved());
    }
}
 
// Second Sol'n, as Behavior Based Testing.
//   (using mocks).
class UpdateBugMockistTest extends TestCase {
 
    public void testBehaviorBasedTestingMockStyle() {
        Bug bug = new Bug("description");
 
        IMocksControl control = EasyMock.createControl();
        Database db = control.createMock(Database.class);
        Lock lock = control.createMock(Lock.class);
        lock.acquire();
        db.save(bug);
        lock.release();
        control.replay();
        // Two lines less for setting up mocks.
 
        UpdateBug updateBug = new UpdateBug(db, lock);
        updateBug.execute(bug);
        // Verify it happened as expected
        control.verify();
    }
}
  • ํด๋ž˜์Šค๋Š” ๋‹ค๋ฅธ ๊ฐ์ฒด์— ๋Œ€ํ•œ ServiceLocator ์—ญํ• ์„ ํ•˜์ง€ ๋ง๊ณ , ํ•˜๋‚˜์˜ ์ฑ…์ž„์„ ๊ฐ€์ ธ์•ผ ํ•œ๋‹ค.
  • Flaws:
    • db.getLock()์€ Database ํด๋ž˜์Šค์˜ ์—ญํ• ์ด ์•„๋‹ˆ๋‹ค. ๋˜ db.getLock().acquire(), db.getLock().release()๋Š” โ€œLaw of Demeterโ€๋ฅผ ์œ„๋ฐ˜ํ•˜๊ณ  ์žˆ๋‹ค.
    • UpdateBag ํด๋ž˜์Šค๋ฅผ ํ…Œ์ŠคํŠธํ•  ๋•Œ Database#getLock ๋ฉ”์†Œ๋“ค๋ฅผ mock์œผ๋กœ ์ฒ˜๋ฆฌํ•ด์•ผ ํ•œ๋‹ค.
    • Database ํด๋ž˜์Šค๋Š” database๋กœ๋„ ๋™์ž‘ํ•˜๊ณ , service locator(lock์„ ์ œ๊ณต)๋กœ๋„ ๋™์ž‘ํ•œ๋‹ค. โ€œLaw of Demeterโ€๋„ ์œ„๋ฐ˜ํ•˜๊ณ , service locator๋กœ๋„ ๋™์ž‘ํ•˜๊ณ  ์žˆ์–ด์„œ ์‹ฌ๊ฐํ•œ ๋ฌธ์ œ๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ๋‹ค. Database ํด๋ž˜์Šค์˜ ์—ญํ• ์€ entity๋“ค์„ database์— ์ €์žฅํ•˜๋Š” ๊ฒƒ์ด๋‹ค.
  • Database ํด๋ž˜์Šค์˜ getLock ๋ฉ”์†Œ๋“œ๋Š” ์ œ๊ฑฐ๋˜์–ด์•ผ ํ•œ๋‹ค.
    • Database๊ฐ€ lock์— ๋Œ€ํ•œ ์ฐธ์กฐ๊ฐ€ ํ•„์š”ํ•˜๋”๋ผ๋„ ์ œ๊ฑฐ๋˜์–ด์•ผ ํ•œ๋‹ค.
    • You should never have to mock out a setter or getter
Object Called "Context" is a Great Big Hint to look for a Violation
  • Before: Hard to Test
    • SUT
// Context objects can be a java.util.Map or some
//   custom grab bag of stuff.
 
class MembershipPlan {
 
    void processOrder(UserContext userContext) {
        User user = userContext.getUser();
        PlanLevel level = userContext.getLevel();
        Order order = userContext.getOrder();
 
        // ... process
    }
}
- Test
// An example test method working against a
//   wretched context object.
 
    public void testWithContextMakesMeVomit() {
        MembershipPlan plan = new MembershipPlan();
        UserContext userContext = new UserContext();
        userContext.setUser(new User("Kim"));
        PlanLevel level = new PlanLevel(143, "yearly");
        userContext.setLevel(level);
        Order order = new Order("SuperDeluxe", 100, true);
        userContext.setOrder(order);
 
        plan.processOrder(userContext);
 
        // Then make assertions against the user, etc ...
}
  • Ater: Testable and Flexible Design
    • SUT
// Replace context with the specific parameters that
//   are needed within the method.
 
class MembershipPlan {
 
    void processOrder(User user, PlanLevel level, Order order) { 
        // ... process
    }
}
- Test
// The new design is simpler and will easily evolve.
 
    public void testWithHonestApiDeclaringWhatItNeeds() {
        MembershipPlan plan = new MembershipPlan();
        User user = new User("Kim");
        PlanLevel level = new PlanLevel(143, "yearly");
        Order order = new Order("SuperDeluxe", 100, true);
 
        plan.processOrder(user, level, order);
 
        // Then make assertions against the user, etc ...
    }
}
  • context ๊ฐ์ฒด๋Š” ์ด๋ก ์ƒ ๊ดœ์ฐฎ์•„ ๋ณด์ธ๋‹ค(context ๊ฐ์ฒด์— ์–ด๋–ค ์†์„ฑ์ด ์ถ”๊ฐ€๋˜์–ด๋„ context ๊ฐ์ฒด๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ํด๋ž˜์Šค์˜ ์‹œ๊ทธ๋‹ˆ์ฒ˜๊ฐ€ ๋ณ€๊ฒฝ๋  ํ•„์š”๊ฐ€ ์—†๋‹ค).
  • ํ•˜์ง€๋งŒ context ๊ฐ์ฒด๋Š” ํ…Œ์ŠคํŠธํ•˜๊ธฐ ๋งค์šฐ ์–ด๋ ต๋‹ค.
  • search์— ์‚ฌ์šฉ๋œ map์ด ์ด ๊ฒฝ์šฐ์ผ ๋“ฏโ€ฆ
  • Flaws:
    • API๋Š” ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•ด ํ•„์š”ํ•œ ๊ฒƒ์ธ userContext๊ฐ€ ์ „๋ถ€๋ผ๊ณ  ํ‘œํ˜„ํ•œ๋‹ค. ํ•˜์ง€๋งŒ ํ…Œ์ŠคํŠธ ์ž‘์„ฑ์ž๋Š” ์‹ค์ œ userContext์— ์–ด๋–ค ๊ฐ’๋“ค์ด ๋“ค์–ด ์žˆ์–ด์•ผ ํ•˜๋Š”์ง€ ์•Œ ์ˆ˜ ์—†๋‹ค. ์ด๋Ÿฐ ๊ฒฝ์šฐ ํ•„์š”ํ•  ๊ฒƒ ๊ฐ™์€ ๊ฐ’์„ ์ฑ„์›Œ๊ฐ€๋ฉฐ ์ •์ƒ์ ์ธ ๊ฒฐ๊ณผ๊ฐ€ ๋‚˜์˜ฌ ๋•Œ๊นŒ์ง€ ๋ฐ˜๋ณต ์ž‘์—…์„ ํ•ด์•ผ ํ•œ๋‹ค.
    • API๊ฐ€ flexible(๋ฉ”์†Œ๋“œ ์‹œ๊ทธ๋‹ˆ์ฒ˜ ๋ณ€๊ฒฝ ์—†์ด ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ๋‹ค)ํ•˜๋‹ค๊ณ  ํ•  ์ˆ˜๋„ ์žˆ๋‹ค. ํ•˜์ง€๋งŒ refactoring tool์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†์ด ์ฝ”๋“œ๊ฐ€ ๊นจ์ง€๊ธฐ ์‰ฝ๊ณ , ์‚ฌ์šฉ์ž๊ฐ€ ์–ด๋–ค ํŒŒ๋ผ๋ฏธํ„ฐ๊ฐ€ ํ•„์š”ํ•œ์ง€ ์•Œ์ˆ˜ ์—†๋‹ค๋Š” ๋ฌธ์ œ๋ฅผ ๊ฐ–๋Š”๋‹ค. API๋งŒ ๋ณด๊ณ ๋Š” ์–ด๋–ค Collaborator๊ฐ€ ํ•„์š”ํ•œ์ง€ ์•Œ ์ˆ˜ ์—†๋‹ค. ์ด๋Ÿฌํ•œ API๋Š” ํ”„๋กœ์ ํŠธ์˜ ์‹ ๊ทœ ๋ฉค๋ฒ„๊ฐ€ ํด๋ž˜์Šค์˜ ๋ชฉ์ /์—ญํ• /ํ–‰์œ„๋ฅผ ์ดํ•ดํ•˜๊ธฐ ์–ด๋ ต๊ฒŒ ๋งŒ๋“ ๋‹ค. ์ด๋Ÿด ๊ฒฝ์šฐ API๊ฐ€ ์˜์กด์„ฑ์— ๋Œ€ํ•ด ๊ฑฐ์ง“๋งํ•˜๊ณ  ์žˆ๋‹ค๊ณ  ํ•œ๋‹ค.
When this is not a Flaw
  • fluent style์„ ์‚ฌ์šฉํ•˜์—ฌ DSL(Domain Specific Language)์—์„œ ์„ค์ •์„ ํ•˜๋Š” ๊ฒฝ์šฐ
  • ์ด ๊ฒฝ์šฐ value object(ํ•ญ์ƒ ์ƒˆ๋กœ ๋งŒ๋“ค์–ด์ง€๋Š”)๋ฅผ ์ƒ์„ฑํ•˜๋Š” ๊ฒƒ์ด๊ธฐ ๋•Œ๋ฌธ์— ๋ฌธ์ œ๊ฐ€ ์•ˆ๋œ๋‹ค.
// A DSL may be an acceptable violation.
//   i.e. in a GUICE Module's configure method
    bind(Some.class)
        .annotatedWith(Annotation.class)
        .to(SomeImplementaion.class)
        .in(SomeScope.class);

Flaw #3: Brittle Global State & Singletons

์›๋ฌธ

Warning Signs

  • singleton์„ ์‚ฌ์šฉํ•˜๊ฑฐ๋‚˜ ์ถ”๊ฐ€ํ•˜๋Š” ๊ฒฝ์šฐ
  • static field๋‚˜ static method๋ฅผ ์‚ฌ์šฉํ•˜๊ฑฐ๋‚˜ ์ถ”๊ฐ€ํ•˜๋Š” ๊ฒฝ์šฐ
  • static initialization ๋ธ”๋ก์„ ์‚ฌ์šฉํ•˜๊ฑฐ๋‚˜ ์ถ”๊ฐ€ํ•˜๋Š” ๊ฒฝ์šฐ
  • registry๋ฅผ ์‚ฌ์šฉํ•˜๊ฑฐ๋‚˜ ์ถ”๊ฐ€ํ•˜๋Š” ๊ฒฝ์šฐ
  • service locator๋ฅผ ์‚ฌ์šฉํ•˜๊ฑฐ๋‚˜ ์ถ”๊ฐ€ํ•˜๋Š” ๊ฒฝ์šฐ

Flaw #4: Class Does Too Much

์›๋ฌธ

Warning Signs

  • ํด๋ž˜์Šค๊ฐ€ ๋ฌด์—‡์„ ํ•˜๋Š”์ง€๋ฅผ ์ข…ํ•ฉํ•ด์„œ ํ‘œํ˜„ํ•  ๋•Œ โ€œandโ€๊ฐ€ ํฌํ•จ๋˜๋Š” ๊ฒฝ์šฐ
  • ์ƒˆ๋กœ์šด ๋ฉค๋ฒ„๊ฐ€ ํด๋ž˜์Šค๋ฅผ ์ฝ๊ณ , ์ดํ•ดํ•˜๋Š” ๊ฒƒ์ด ๋„์ „์ ์ธ(์–ด๋ ค์šด) ์ผ์ธ ๊ฒฝ์šฐ
  • ํด๋ž˜์Šค๊ฐ€ ๋ช‡๋ช‡ ๋ฉ”์†Œ๋“œ์— ์˜ํ•ด์„œ๋งŒ ์‚ฌ์šฉ๋˜๋Š” ํ•„๋“œ๋ฅผ ๊ฐ–๋Š” ๊ฒฝ์šฐ
  • ํด๋ž˜์Šค์— ํŒŒ๋ผ๋ฏธํ„ฐ๋งŒ ์‚ฌ์šฉํ•˜๋Š” static method๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ