해리의 데브로그

SOLID Principles in PHP 03 - Liskov substitution Principle (LSP)

|

Laracasts - SOLID Principles in PHP 강의를 듣고 정리한 포스팅 입니다.

1. GET STARTED

“컴퓨터 프로그램에서 자료형 S가 자료형 T의 하위형이라면 필요한 프로그램의 속성(정확성, 수행하는 업무 등)의 변경 없이 자료형 T의 객체를 자료형 S의 객체로 치환 할 수 있어야 한다”

  • Let q(x) be a property provable about objects x of type T.

  • Then q(y) should be provable for objects y of type S where S is a subtype of T

서브 타입(자식 클래스) 은 언제나 자신의 기반타입(부모 클래스) 으로 교체 할 수 있어야 한다. Derived(sub) classes must be substitutable for their base classes

  • dosomething 함수가 클래스 A를 인자로 받아 동작을 한다면 A의 서브 클래스인 B가 넣어도 문제 없이 그대로 동작해야만 함.
class A {
    public function fire() {}
}

class B extends A {
    public function fire() {}
}

function doSomething(A $obj)
{
    // do something with it
}

2. Pre-condition of the subclass is too great

비디오 플레이어에 대한 VideoPlayer 클래스가 있으며 플레이를 시키는 play 메소드가 있다고 가정하자. 그리고 .avi 파일을 플레이하는 경우를 고려했을 때, 가장 먼저 떠오르는 생각은 VideoPlayer 를 상속받아 서브클래스를 만드는 것일 것이다. 그 다음 코드 구현은 다음과 같다.

  • play 메소드를 오버라이드
  • 확장자를 체크하는 분기문 작성. 확장자가 avi가 아닐 경우 Exception을 던짐

그러나 이 코드는 LSP를 위반하는 코드이다. 이유는 서브 클래스의 전제조건이 더 클 수없기 때문이다.(pre-conditions of the subclass can’t be greater). 부모 클래스인 VideoPlayerAviVideoPlayer 로 교체한다고 했을 경우, 반환값이 달라 질 수 있기 때문에(확장자에 따라) LSP를 준수하지 못하게 된다

이러한 경우, 우리가 떠올릴 수 있는 방법은 contract(interface)를 활용하는 것이다. 그러나 이 방법은 input만을 검증하며 output은 검증하지 못한다.

class VideoPlayer {
    public function play($file)
    {
        // play the video
    }
}

class AviVideoPlayer extends VideoPlayer {
    public function play($file)
    {
        if(pathinfo($file, PATHINFO_EXTENSION) !== 'avi')
        {
            throw new Exception; // violates the LSP
        }
    }

LessonRepositoryInterface 에는 모든 데이터를 갖고오는 getAll 메소드가 있음. 이에 따라, 파일시스템/데이터베이스로부터 데이터를 갖고와 인터페이스를 구현하는 코드가 있다고 하자. 이 경우 각각의 클래스 내 getAll 메소드는 다음과 같이 다른값을 반환한다. 이 경우 역시 LSP를 준수하지 못하는 코드가 됨.

  • FileLessonRepository@getAll - 배열을 반환
  • DbLessonRepository@getAll - collection 을 반환
interface LessonRepositoryInterface {
    public function getAll();
}

class FileLessonRepository implements LessonRepositoryInterface {

    public function getAll()
    {
        // return through filesystem
        return [];
    }
}

class DbLessonRepository implements LessonRepositoryInterface {

    public function getAll()
    {
        // return via eloquent model
        return Lesson::all();
    }
}

위 코드를 LSP를 준수하는 방향으로 변경하고 싶을 경우 인터페이스의 메소드위의 힌트를 넣을 수 있음. PHP가 강제로 이 주석을 준수해라고 하진 않지만 일종의 약속으로, 반드시 array를 반환하게 코드를 바꿔야 함.

interface LessonRepositoryInterface {
    /**
     * Fetch all records
     *
     * @return array
     */
    public function getAll();
}

class DbLessonRepository implements LessonRepositoryInterface {

    public function getAll()
    {
        return Lesson::all()->toArray();
    }
}

인터페이스를 인자로 받아 동작하는 함수를 예시로 들어 보자. 만약에 인터페이스를 구현하는 클래스들의 반환값이 다르다면, 값의 데이터 타입을 체크하여 분기문을 작성할 것이다. 이는 반환값이 다를 것을 예상한다는 것으로 LSP를 지키지 않았다는 의미이다.

function foo(LessonRepositoryInterface $lesson)
{
    $lessons = $lesson->getAll();
}

3. Summary

  • Signature must match
  • Preconditions can’t be greater
  • Post conditions at least equal to
  • Exception types must match

SOLID Principles in PHP 02 - Open Closed Principle (OCP)

|

Laracasts - SOLID Principles in PHP 강의를 듣고 정리한 포스팅 입니다.

1. GET STARTED

Open-Closed: Entities(class, method, function etc..) should be open for extension, but closed for modification

확장에는 개방되어야하며 변경에는 폐쇄되어야한다. 기존의 코드를 변경하지 않으면서 기능을 추가할 수 있도록 설계되어야 함.

  • open for extension: it should be simple to change the behavior of a particular entity (class)
  • closed for modification: Goal (very difficult to follow perfectly). something you should strive for its goal. Change behavior without modifying original source code .

2. Modify behavior by doing it from extension

회사의 보스가 사각형을 준비하라고 지시를 내렸음

  • 이에 따라 squre 이라는 클래스 생성

이후, 보스가 사각형의 면적을 계산해야한다고 지시를 내림

  • Single Responsibility Principle에 따라, 면적만을 계산하는 클래스 AreaCalculator 를 따로 생성
  • 가장 심플한 형태로 사각형들을 배열로 받아 면적의 합을 누적으로 저장시킴
<?php namespace Acme;

class Square {
    public $width;
    public $height;

    function __construct($height, $width)
    {
        $this->height = $height;
        $this->width = $width;
    }
}
<?php namespace Acme;

class AreaCalculator {

    public function calculate($squares)
    {
        $area = 0;
        foreach ($squares as $square)
        {
            $area += $square->width * $square->height;
        }
      
      	return $area;
    }
}

이후, 보스가 원에 대해서도 준비해달라고 요청을 하였고, 원의 면적도 고려해서 계산할 수 있는 로직을 구현해달라고 추가 요청을 함. 그러나 현재 AreaCalculator 클래스는 사각형만을 고려해서 구현되어있으며 원에 대해서는 적용이 불가능한 상태임. 따라서 클래스를 전면적으로 수정해야함. 이는 Open-Closed Principle을 위반하는 것.

<?php namespace Acme;

class Circle {
    public $radius;

    function __construct($radius)
    {
        $this->radius = $radius;
    }
}

OCP를 준수하기 위해 먼저, AreaCalculator@calculator 내 반복문 변수명을 $squares$shapes 으로 변경시킬 수 있음. 그런데 도형에 따라 면적을 계산하는 계산식이 다른 상황임. 이때 우리가 우선적으로 생각할 수 있는 것은 분기문임.

<?php namespace Acme;

class AreaCalculator {

    public function calculate($shapes)
    {
        foreach ($shapes as $shape)
        {
            if (is_a($shape, 'square')) // if ($shape instanceof Square)
            {
                $area[] = $shapes->width * $shapes->height;
            }
            else
            {
                $area[] = $shapes->radius * $shapes->radius * pi();
            }
        }

        return array_sum($area);
    }
}

또 다시, 삼각형 클래스를 추가하여 면적 계산 클래스에 반영해야하는 경우라면 또다시 분기문을 통해 코드를 수정해야 하는 것일까? 이처럼 변화가 생길 때마다 코드를 매번 수정하는것이 맞는 것일까? This is sort of things that lead to code rot.

how can we extend this behavior while keeping the class closed from modification?

3. Seperate extensible behavior behind an interface, and flip the dependencies

Shape 인터페이스 생성후 인터페이스를 각각의 클래스에 implement 시킴.

//ShapeInterface.php
<?php namespace Acme;

interface Shape {
    public function area();
}

// Sqaure.php
<?php namespace Acme;

class Square implements Shape {
    public $width;
    public $height;

    function __construct($height, $width)
    {
        $this->height = $height;
        $this->width = $width;
    }

    public function area()
    {
        return $this->width * $this->height;
    }
}
// Circle.php
<?php namespace Acme;

class Circle implements Shape {
    public $radius;

    function __construct($radius)
    {
        $this->radius = $radius;
    }

    public function area()
    {
        return $this->radius * $this->radius * pi();
    }
}

AreaCalculator@calculate 는 이제 면적을 계산한 값을 파라미터로 받아 $area 에 저장만 하면 됨. 만약에 삼각형에 대하여 추가적인 계산이 필요하더라도 아래 코드는 수정할 필요가 없으며, 삼각형 클래스만 만들어 area 메소드를 implement하여 면적 계산만 하면 됨.

<?php namespace Acme;

class AreaCalculator {

    public function calculate($shapes)
    {
        foreach ($shapes as $shape)
        {
            $area[] = $shape->area();
        }

        return array_sum($area);
    }
}

4. Practices

제품을 사고 체크아웃 하는 프로세스 로직을 갖고 있는 Checkout 클래스를 정의해보자.

  • begin : 영수증을 받아 시작을 함. 그리고 payment를 accept 하는 acceptCash 를 호출함
  • acceptCash : 현금을 받음
class Checkout{
    public function begin(Receipt $receipt)
    {
        $this->acceptCash($receipt);
    }

    public function acceptcash($receipt)
    {
        // accept the cash
    }
}

만약에 현금이 아니라 신용카드, 카카오 페이로 계산을 하기 원하는 경우는 어떻게 해야할까? 기존의 코드는 현금을 받아 계산하는 경우만을 고려하여 디자인 되어있음. 이러한 경우는 Open Closed Principle을 고려하지 않은 경우임. Open Closed Principle을 적용할 경우 코드는 다음과 같음.

interface PaymentMethodInterface {
    public function acceptPayment($receipt);
}

class CashPaymentMethod implements PaymentMethodInterface {
    public function acceptPayment($receipt)
    {
    }
}

class Checkout{
    public function begin(Receipt $receipt, PaymentMethodInterface $payment)
    {
        $payment->acceptPayment();
    }
}

SOLID Principles in PHP 01 - Single Responsibility Principle (SRP)

|

Laracasts - SOLID Principles in PHP 강의를 듣고 정리한 포스팅 입니다.

1. GET STARTED

A Class Should have one, and only one, reason to change

현재 아래의 예시는 여러 방면으로 Single Responsibility Principle을 준수하고 있지 않다. 사용자 인증, DB 접근, 결과 반환 등 너무 많은 책임과 역할을 수행하고 있음.

// SalesReporter.php
class SalesReporter {
  
  public function between($startDate, $endDate)
  {
    //perform authentication
    if ( ! Auth::check()) throw new exception('Authentication reqiured for reporting');
    
    // get sales from db
    $sales = $this->queryDBForSalesBetween($startDate, $endDate)
      
    // return results
    return $this->format($slaes);
  }
  
  //quering database
  protected function queryDBForSalesBetween($startDate, $endDate)
  {
    return DB::table('sales')->whereBetween('created_at', [$startDate, $endDate])->sum('charged') / 100;
  }
}

	protected function format($sales)
  {
    return "<h1>Sales: $sales</h1>";
  }
}
// routes.php

Route::get('/', function()
{
	$reporter = new Acme\Reporting\SalesReporter();
  
  $begin = Carbon\Carbon::now()->subDays();
  $end = Carbon\Carbon::now();
  
  return $report->between($begin, $end);
})

2. SalesReporter과 user Authentication을 왜 신경써야하는가?

That’s application logic. It does not belong in here→ perform authentication 코드 삭제

3. querying data 코드에서 SalesReporter가 너무 많은 responsibility를 갖고 있음

“too many reasons to change, or too many consumer of this class”

예를 들어, persistence layer(데이터 처리 담당 계층)가 향후 변경된다면 아래 코드를 변경해야만 할 것. 또한, output의 포맷을 바꿔야하는 경우도 아래 코드를 변경해야 할 것이다. 이러한 두가지의 이유로 기 클래스는 SRP를 준수하고 있지 않다고 할 수 있다.

return DB::table('sales')->whereBetween('created_at', [$startDate, $endDate])->sum('charged') / 100;

persistance layer가 무엇이고 어떻게 정보를 갖고올건지는 SalesReporter 클래스의 responsibility가 아님.

  • 이러한 역할을 하는 인터페이스를 생성자 메소드에 주입(SalesRepository ; 이론적으로 SalesRepositoryInterface 라 지어야 하나, 이부분은 뒤부분에서 다룰 예정)
  • 사용을 위해 use Acme\Repositories\SalesRepository; 후, 디렉토리 & 파일 생성
// SalesReporter.php
use Acme\Repositories\SalesRepository;

SalesRepository 클래스가 이제 database specific interaction 을 담당함.

  • SalesReporter@queryDBForSalesBetweenSalesRepository 클래스로 이동
  • 접근 제어자는 public으로 변경
  • 메소드 명은 좀 더 친화적으로 queryDBForSalesBetween 에서 between 으로 변경
  • SalesReporter@between 에서 불러오는 메소드 명 변경
// Repositories\SalesRepository.php

namespace Acme\Repositories;

class SalesRepository {
  protected function between($startDate, $endDate)
  {
    return DB::table('sales')->whereBetween('created_at', [$startDate, $endDate])->sum('charged') / 100;
  }
}

// SalesReporter.php
public function between($startDate, $endDate)
{
  $sales = $this->repo->between($startDate, $endDate)

4. why should this class care or be this class’s responsibility to ouput/format/print the result?

현재 코드에서 다른 포맷으로 아웃풋을 넘기는 것이 가능한가? 현재 코드는 HTML을 assume 하고 있는 상태임. 만약에 Json으로 포맷을 바꾸고 싶다면? 또는 다른 포맷들을 함께 넘기고 싶다면? 그럴때마다 코드를 업데이트 해줘야하는 번거로움이 발생함. 이에 대한 방안은 여러개가 있음.

protected function format($sales)
{
  return "<h1>Sales: $sales</h1>";
}
  1. we are going to leave the formatting to the consumer of the class
  2. class based formatting을 원할 경우. 포맷팅을 describe하는 인터페이스 생성
    • 이후, 인터페이스를 호출하여 코드 업데이트
    • format 메소드는 삭제가능
    • routes.php 최종 다듬기(인스턴스 넘기기)
// SalesOutputInterface.php
namespace Acme\Reporting;

interface SalesOutputInterface {
  public function output($sales);
  
}

// HtmlOutput.php
namespace Acme\Reporting;
use Acme\Reporting\SalesOutputInterface;

class HtmlOutput implements SalesOutputInterface{
  public function output($sales)
  {
    return "<h1>Sales: $sales</h1>";
  }
}

// SalesReporter.php
public function between($startDate, $endDate, SalesOutputInterface $formatter)
{
  $sales = $this->repo->between($startDate, $endDate)
  
  $formatter->output($sales);
  
// routes.php
Route::get('/', function()
{
	$reporter = new Acme\Reporting\SalesReporter(new \Acme\Repositories\SalesRepository);
  
  $begin = Carbon\Carbon::now()->subDays();
  $end = Carbon\Carbon::now();
  
  return $report->between($begin, $end, new Acme\Reporting\HtmlOutput);
})  

5. Summary

routes.php

Route::get('/', function()
{
	$reporter = new Acme\Reporting\SalesReporter(new \Acme\Repositories\SalesRepository);
  
  $begin = Carbon\Carbon::now()->subDays();
  $end = Carbon\Carbon::now();
  
  return $report->between($begin, $end, new Acme\Reporting\HtmlOutput);
})  

SalesReporter.php

use Acme\Repositories\SalesRepositories;
use Auth, DB, Exception;

class SalesReporter {
  
	private $repo;
  
  public function __construct(SalesRepository $rep)
  {
    $this->repo = $repo
  }
  public function between($startDate, $endDate, SalesOutputInterface $formatter)
  {
    $sales = $this->repo->between($startDate, $endDate)

    $formatter->output($sales);
  }
}

Repositories\SalesRepository.php

namespace Acme\Repositories;

class SalesRepository {
  protected function between($startDate, $endDate)
  {
    return DB::table('sales')->whereBetween('created_at', [$startDate, $endDate])->sum('charged') / 100;
  }
}

SalesOutputInterface.php

namespace Acme\Reporting;

interface SalesOutputInterface {
  public function output($sales);
  
}

HtmlOutput.php

namespace Acme\Reporting;
use Acme\Reporting\SalesOutputInterface;

class HtmlOutput implements SalesOutputInterface{
  public function output($sales)
  {
    return "<h1>Sales: $sales</h1>";
  }
}

Object-Oriented Bootcamp 03 - Static, Constant / Interface / Interface vs Abstract

|

Laracasts - Object-Oridented Bootcamp 강의를 듣고 정리한 포스팅 입니다.

1. Statics

간단하게 들어온 숫자들의 합을 구하는 메소드를 구현해보도록 하자.

// option 1
class Math {
    public function add()
    {
        return array_sum(func_get_args());
    }
}

// option 2. applicable for recent version of PHP
class Math {
    public static function add(...$nums)
    {
        return array_sum($nums);
    }
}

$math = new Math;

var_dump($math->add(1, 2, 3, 4));

현재, add 메소드는 다른 클래스에서 호출하는 등 dynamic할 필요가 없음 (input을 받아서 계산 후 output을 반환하기만 하면 됨). 이처럼 dynamic 하지 않게 메소드를 구현하고 싶을 경우, 함수를 정의할 때 static 을 추가하면 됨.

메서드를 호출할때는 클래스명::메소드명 으로 호출. 스태틱 메소드는 인스턴스를 생성하지 않고 바로 어디서든 사용이 가능함. 이러한 관점에서 볼 때, 스태틱 메소드는 Global function 이라고 할 수 도 있음.

만약 스태틱 메소드가 다른 클래스를 반환/호출하는 경우는 유지/테스트 하기에 매우 어려우므로 권장되는 방법이 아님.

class Math {
    public static function add(...$nums)
    {
        return array_sum($nums);
    }
}

echo Math::add(1,2,3);

2. Static with Example

Person 이라는 클래스에 프로퍼티 $age 를 스태틱하게 1로 정의해보도록 하자. 이론적으로 문제 없이 동작하나, 객체 & 클래스의 관점에서 볼 때는 적절하지 못함. “사람” 이라는 클래스를 만들었다면 모든 사람들이 동일한 나이를 Share(=static) 한다는 것은 적절하지 않음.

class Person {
    public static $age = 1;
}

echo Person::$age;

또한, 아래의 예시에서도 문제가 생길 수 있음. 스태틱 프로퍼티인 age 를 1씩 증가시키는 haveBirthday 메소드를 구현하고, harryron 의 나이를 개별적으로 증가시켜보자.

  • 우리는 harry 는 해당 메소드를 2번 호출하여 3이라는 값을 갖게 하고
  • ron 은 한번만 호출하여 2이라는 값을 갖게 하고 싶음.
  • 그러나 스태틱 프로퍼티는 특정 오브젝트에만 할당되는 것이 아니라 글로벌로 공유됨.
  • 따라서 ron 의 나이는 4라는 값을 갖게 된다 → break encapsulation
class Person {
  
    public static $age = 1;

    public function haveBirthday()
    {
        static::$age += 1;
    }
}

$harry = new Person;
$harry->haveBirthday(); // 2
$harry->haveBirthday(); // 3

echo $harry::$age; // 3

$ron = new Person;
$ron->haveBirthday(); // expected 2 but returns 4

echo $ron::$age; // 4

3. Constants

스태틱 프로퍼티는 어떠한 경우에 사용 될 수 있을까? 인스턴스마다 개별적인 값을 가지지 않고 모든 인스턴스에게 동일하게 적용되는 값들에게 스태틱 프로퍼티를 적용할 수 있을 것이다 (예를 들어, 세율).

아래의 예시의 경우, taxpublic 으로 정의된 스태틱 프로퍼티이므로 값을 변경할 수 있다. 만약에 반드시 고정된 값을 가지며 값을 변하게 하기 위해서는 priave 으로 변경할 수 도 있지만, 다른 방법으로 const (상수)를 사용할 수 있음.

class BankAccount {
    // public static $tax = .09;
  	// private static $tax = .09;
  	const TAX = 0.9;

}

// echo BankAccount::$tax = 1.5;
echo BankAccount::TAX;

마찬가지로, 스태틱 메소드 또한 전역으로 사용하며 인스턴스 생성 없이 바로 사용하기 위해 쓴다.

4. Interfaces

**“Think of interface as contract” ** 인터페이스 안에서 실제 로직을 작성하지 않음. 인터페이스는 어떠한 term이 적용되어야 하는지를 작성하는 개념.

아래 예시에서 Animal 이라는 인터페이스 내에 “모든 동물은 의사소통을 한다” 라는 term을 적용시키기 위해 communication 함수를 작성하였음. 인터페이스는 실제 로직을 작성하지 않으므로 함수의 body( {} )를 입력하지 않음.

interface Animal {
    public function communicate();
}

we want to make sure that any implementation that we have will at this contract. on the other words, I want any type of animal to offer communicate method.

Animal 인터페이스를 implements 함으로써, 인터페이스(계약서)에 작성된 메서드 communicate 를 반드시 사용해야함.

interface Animal {
    public function communicate();
}

class Dog implements Animal {
    public function communicate()
    {
        return 'bark';
    }
}

class Cat implements Animal {
    public function communicate()
    {
        return 'meow';
    }
}

5-1. Interfaces vs Abstract Classes (1)

인터페이스와 추상 클래스의 차이는 무엇일까? 로그 기능을 구현한다고 가정해보자. 로그에는 파일/데이터베이스/온라인 서비스 등 다양한 영역에 접근할 수 있음. 이에 따라 기본적인 클래스를 구현하면 다음과 같음. 그리고 로그 데이터에 접근 & 출력에 대한 로직을 구현하는 컨트롤러도 작성해보자.

class LogToFile {
    public function execute($message)
    {
        var_dump('log the message to a file: '. $message);
    }
}

class LogToDatabase {
    public function execute($message)
    {
        var_dump('log the message to a database: '. $message);
    }
}

class UsersController {

    protected $logger;

  	// LogToFile 클래스를 하드코딩으로 파라미터에 입력
    public function __construct(LogToFile $logger)
    {
        $this->logger = $logger;
    }

    public function show()
    {
        $user = 'harrylee';

        // log this information
        $this->logger->execute($user);
    }
}

$controller = new UsersController(new LogToFile);
$controller->show();

이제, 회사의 보스가 말하길 “파일”에는 더이상 로깅하지 않으니 “데이터베이스”에 로깅하는 코드로 변경하라, 고 새로운 지시를 내렸다고 가정해보자. 이제 문제는 LogToFile 이라는 specific implementation을 하드코딩하여 여러 군 데 사용했다는 것임. (과장해서, 여러 어플리케이션에 적용했다고 가정) => 코드를 찾아서 일일이 다 변경해줘야함.

“The problem was that we were too specific. on another word, the problem was that we assumed an implementation. we didn’t think the possibilty that it might be changed”

“Coding to interfaces, not implementation(concretion)”

이 말은 위의 예시와 정확히 일치한다. 위의 예시에서 UserController 클래스의 생성자 메소드를 작성할 때 우리는 implementation(concrete class)을 사용하였음. 그리고 이것을 다른 것으로 변경해야할 때 broke down 되었음.

if there are ever classess or tasks or you could imgine having multiple implementations (multiple different message excuting this task/behavior, then that is a sign that you need to create interface)

따라서 두개의 클래스를 인터페이스와 연결한 후, UserController 클래스의 생성자 메서드의 인자로 클래스가 아니라 인터페이스인 Logger 를 넘김.

interface Logger {
    public function execute($message);
}

class LogToFile implements Logger {
    public function execute($message)
    {
        var_dump('log the message to a file: '. $message);
    }
} 

class LogToDatabase implements Logger {
    public function execute($message)
    {
        var_dump('log the message to a database: '. $message);
    }
}

class UsersController {

    protected $logger;

    public function __construct(Logger $logger)
    {
        $this->logger = $logger;
    }

인자로 인터페이스를 넘기는 것과 특정 클래스를 넘기는데는 커다란 차이가 있음.

  1. 클래스를 넘긴 경우, 유저 컨트롤러는 바깥 세상에 “기능이 동작하려면 이 특정 인스턴스가 필요해” 라고 말함.
  2. 인터페이스를 넘긴 경우, 유저 컨트롤러는 “기능이 동작하려면 어떤 종류의 함수가 필요해. 정확히 어떤 클래스인지는 알 필요 없어. 너가 정해. 난 그냥 유저 컨트롤러를 동작할 때 어떤 functionality가 있기만 하면 돼!”

이제 LogToDatabase 클래스로 일일이 하드하게 변경할 필요가 없으며 UserController 또한 그대로 둬도 됨. 그저 인스턴스를 생성할 때 LogToDatabase 를 호출하기만 하면 됨.

$controller = new UsersController(new LogToFile);
$controller->show();

$controller = new UsersController(new LogToDatabase);
$controller->show();

인터페이스를 활용한 추가 예시

interface Repository {
    public function save($data);
}

class MongoRepository implements Repository {
    public function save($data)
    {
    }
}

class FileRepository implements Repository {
    public function save($data)
    {
    }
}
interface CanBeFiltered {
    public function filter();
}

class Favorited implements CanBeFiltered{
    public function filter()
    {
    }
}

class Unwatched implements CanBeFiltered{
    public function filter()
    {
    }
}

class Difficulty implements CanBeFiltered{
    public function filter()
    {
    }
}

5-2. Interfaces vs Abstract Classes (2)

어플리케이션에서 Github으로 로그인하는 함수를 구현한다고 가정해보자. 다른 SNS(페이스북, 카카오톡) 등으로 로그인을 시도하기 전까지는 문제가 없음.

function login(GithubProvider, $provider)
{
    $provider->authorize();
}

위의 예시는 provider implementation을 하드코딩한 상태임. 만약 페이스북의 경우는? 혹은 카카오톡의 경우는? 분기문을 따로 작성하여 프로바이더의 종류에 따라 다르게 동작하도록 코드를 작성해야할까?

이처럼 오브젝트의 타입을 체크해야하는 경우, 99%의 경우 you should be leveraging polymorphism(다형성). 다형성의 관점에서 접근하여, 우리는 특정한 클래스를 참조할 것이 아니라 인터페이스를 활용하여 코드를 재작성할 수 있음. 이 경우, login 함수는 더이상 어떠한 프로바이더가 오는지 신경쓸 필요가 없게 됨. (다형성)

interface Provider {
    public function authorize();
}

function login(Provider $provider)
{
    $provider->authorize();
}

인터페이스는 퍼블릭 메소드만 정의 가능함. 이를 통해 어떠한 호출가능한 클래스는 인터페이스의 메소드를 호출 할 수 있게 됨.

추상 클래스는 인스턴스를 만들 수 없음. 대신 서브 클래스에서 상속받아 인스턴스를 생성 할 수 있음.

abstract class Provider {

    abstract Protected function getAuthorizationUrl();
}

class FacebookProvider extends Provider{

    protected function getAuthorizationUrl()
    {
    }
}

PHP는 기본적으로 다중상속을 지원하지 않음.

5-3. Summary

Interface defines public API. It defines contract that any implementation has to buy it. however No logic will ever be stored within interface.

Abstract class, something is in common. I can enforce contract by creating abstract method. so, subclass must buy it. However we’re also having result to inheritance.

6. Method Injection vs Constructor Injection

Src\AuthController 생성 - receiving Http request and return response.

  • Method Injection: if it is the only place that dependency is referenced, if it is single controller method, use method injection

  • Constructor Injection: if you are going to reference this object in multiple places of your class, use constructor injection.

Object-Oriented Bootcamp 02 - Message 101 / Namespacing / Autoloading

|

Laracasts - Object-Oridented Bootcamp 강의를 듣고 정리한 포스팅 입니다.

1. Messages 101

사람들은 직업을 갖고 있으며 비즈니스를 위해 일을 함. 비즈니스는 사람들을 고용함. 고용된 사람들은 스태프로써 일을 함.

  • Person , Business , Staff 라는 클래스로 구성 가능

비즈니스 클래스는 사람을 고용해야 하므로 hire 메소드 구현. 이때 당연히 사람을 고용하므로 $person을 인자로 넣을 수 있음. 이 개념을 확장하여 PHP에서 지원하는 Type hinting 을 사용하여 오브젝트를 인자로 넣을 수 있음 Person $person

class Person {

    protected $name;

    public function _construct($name)
    {
        $this->name = $name;
    }

}

class Business {

    protected $staff;

    public function __construct(Staff $staff)
    {
        $this->staff = $staff;
    }

    public function hire(Person $person)
    {
        $this->staff->add($person);
    }
}

class Staff {
  
    protected $members = [];

    public function add(Person $person)
    {
        $this->members[] = $person;
    }
}

$harry = new Person('harry lee');
$staff = new Staff([$harry]);
$laracasts = new Business($staff);

메세지를 보냄으로써 클래스간 상호작용을 하는 것이 가능함. 스태프를 추가하고싶을때는 스태프에 add 메세지를 보내며, 모든 스태프의 리스트를 보고 싶을 때는 스태프에 members 메세지를 보냄

// Business Class
public function hire(Person $person)
{
  $this->staff->add($person);
}

public function getStaffMembers()
{
  return $this->staff->members();
}

// Staff Class
public function add(Person $person)
{
  $this->members[] = $person;
}

public function Members()
{
  return $this->members;
}

최종 결과 코드

<?php

class Person {

    protected $name;

    public function __construct($name)
    {
        $this->name = $name;
    }
}

class Business {

    protected $staff;

    public function __construct(Staff $staff)
    {
        $this->staff = $staff;
    }

    public function hire(Person $person)
    {
        $this->staff->add($person);
    }

    public function getStaffMembers()
    {
        return $this->staff->members();
    }
}

class Staff {
    
    protected $members = [];

    public function __construct($members = [])
    {
        $this->members = $members;
    }

    public function add(Person $person)
    {
        $this->members[] = $person;
    }

    public function Members()
    {
        return $this->members;
    }
}

$harry = new Person('harry lee');
$staff = new Staff([$harry]);
$laracasts = new Business($staff);

$laracasts->hire(new Person('Ron Wizlie'));

//$laracasts->hire($harry);

var_dump($staff);
var_dump($laracasts->getStaffMembers());

2. Namespacing

앞 포스팅에서 작성한 Person, Business, Staff 클래스를 단일 파일로 ungrouping 하도록 하자(1 class per file)

  • \src\Person.php
  • \src\Business.php
  • \src\Staff.php

새로운 디렉토리를 생성하여 각 클래스 별로 파일을 만든 후에 클래스를 호출하는 가장 기본적인 방법은 다음과 같음. 하지만 아래의 방법은 준비하는데 시간이 많이 걸릴 뿐더러 효과적인 방법이 아님.

<?php

require 'src/Person.php';
require 'src/Business.php';
require 'src/Staff.php';

  
$harry = new Person('harry lee');
$staff = new Staff([$harry]);
$laracasts = new Business($staff);

$laracasts->hire(new Person('Ron Wizlie'));

var_dump($staff);
var_dump($laracasts->getStaffMembers());

3. Composer

클래스를 Autoloading 하는 방법을 이용할 수 있음( composer 사용). 루트 디렉토리에 composer.json 생성 (파일안에는 어떠한 dependency도 선언할 필요는 없음)

// composer.json
{
}

composer install 을 진행하면 vendor 디렉토리가 생성된 것을 알 수 있음. 이후 composer.json에서 autoload 관련 dependecy를 선언

{
  "autoload": {
    "psr-4": {
      "Acme\\": "src"
    }
  }
}

src 디렉토리 내 클래스 파일에 각각 namespace 설정한 후 composer dump-autoload 입력을 하면 \composer\autoload_psr4.php에 코드가 추가된 것을 알 수 있음.

// Person.php
<?php

namespace Acme;

// autoload_psr4.php

<?php

$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);

return array(
    'Acme\\' => array($baseDir . '/src'),
);

4. Autoloading

autoloading 설정이 완료되었으므로 base 파일로 돌아가 Acme\를 앞에 붙여주며 클래스를 호출 하면됨.

<?php

$harry = new Acme\Person('harry lee');
$staff = new Acme\Staff([$harry]);
$laracasts = new Acme\Business($staff);
$laracasts->hire(new Acme\Person('Ron Wizlie'));

var_dump($laracasts->getStaffMembers());

그러나 아직까지 에러가 발생함.

PHP Fatal error: Uncaught Error: Class ‘Acme\Person’ not found in …

that’s because we have an autoloading component, but we haven’t yet pulled it into a project. for most frameworks and packages, usually that would be done at the entry point.

for example, index.php would require autoaloading. you only have to do that once for the whole project. => require 'vendor/autoloading.php 입력

구조를 좀 더 클린하게 바꾸기 위해 index.php를 만들고 autoloading과 클래스를 호출하는 파일을 import 하자. ex.php에서는 클래스 앞에 붙여진 Acme\를 제거하고 파일 상단에 use 로 대체.

// index.php
<?php

require 'vendor/autoload.php';
require 'ex.php';

// ex.php
<?php

use Acme\Person;
use Acme\Staff;
use Acme\Business;

$harry = new Person('harry lee');
$staff = new Staff([$harry]);
$laracasts = new Business($staff);
$laracasts->hire(new Person('Ron Wizlie'));


var_dump($laracasts->getStaffMembers());

현재, 이렇게 구조를 바꿨음에도 불구하고, Staff.php의 add 메서드의 파라미터에 Person을 그대로 쓸수 있는 것은 동일한 namespace를 공유하고 있기 때문임.

만약에 src 디렉토리 내에 Users 디렉토리를 생성한 후 Person.php를 집어넣는다면 에러가 발생함. 네임스페이스는 디렉토리 구조를 반드시 따라야함. 아울러, Person 클래스를 파라미터로 받고 있는 Business와 staff는 Person 클래스가 동일한 네임스페이스를 공유하고 있지 않으므로 클래스를 따로 export 해줘야함.

// Users\Person.php
namespace Acme\Users;

// Business.php
use Acme\Users\Person;

// Staff.php
use Acme\Users\Person;

// ex.php
use Acme\Users\Person;

5. Summary

  1. composer.json - reference that you are going to use “psr-4” autolading.
    1. as the key, you specify your root namespace (corresponding to your product)
    2. as its value, you specify what directory should be associated with root namespace.
  2. Person.php is in Users directory, thus change the namespace as namespace Acme\Users;
  3. if you follow this convention, remember to require autoloader at some point of your project