2012-09-05 8 views
8

Demeter의 법칙 (http://en.wikipedia.org/wiki/Law_of_Demeter, http://misko.hevery.com/code-reviewers-guide/flaw-digging-into-collaborators/ 참조)을 따르려고 노력하고 있지만 장점을 볼 수는 있지만 도메인 객체에 관해서는 조금만 붙어 있습니다.Demeter의 법칙 - 데이터 객체

도메인 객체는 자연스럽게 체인을 가지며 때로는 전체 체인 정보를 표시해야합니다. 예를 들어

는 쇼핑 바구니 :

각 순서는 사용자, 배달 정보와 각 주문 항목은 각 제품의 이름과 가격을 가진 제품과 수량 을 포함 항목 의 목록이 포함되어 있습니다. 각 사용자는 이름과 주소를 포함합니다.

주문 정보를 표시하는 코드는 주문, 사용자 및 제품에 대한 모든 정보를 사용해야합니다.

주문 정보를 통해이 정보를 얻는 것이 더 낫고 재사용 가능합니다. "위에 나열된 모든 객체에 대한 쿼리를 수행하기 위해 일부 코드보다 높은"order.user.address.city "가 코드에 별도로 전달됩니다.

의견/제안/팁을 환영합니다!

+0

무엇이 당신 질문입니까? – hakre

+1

나는 여기에 특정한 질문이 없다는 것을 안다. 나는 현상금을 놓을 수 있었고 그 주제는 논의 할 가치가 있었기 때문에 현상금을 배치했다. 아마도 OP가 약간의 질문을 명확히 할 수 있을까요? – rdlowrey

+0

@Tom 문제는 클래스 User, class Info 및 Order 클래스에 Product 클래스의 배열을 삽입하는 것을 좋아하지 않는다는 것입니다. 그들은 다소 독립적이다 그래도 –

답변

7

하나의 문제, 높은 순서 종속성이 클래스 외부 코드의 구조 "로 구운"얻을 것입니다.

리팩터링 할 때 "강제 변경"은 리팩터링되는 클래스의 메서드로 제한되어야합니다. 클라이언트 코드에 체인화 된 참조가 여러 개 있으면 리팩터링을 통해 코드의 다른 위치에서 변경 작업을 수행 할 수 있습니다.

예를 들어, UserOrderPlacingParty (사용자, 회사 및 주문할 수있는 전자 에이전트를 캡슐화하는 추상화)으로 대체한다고 가정 해보십시오. 이 리팩토링은 즉시 여러 문제를 제시한다

  • User 속성이 다른 것을 호출되며, 주문이 접수 될 때 새로운 속성의 경우 city이있는 address이 없을 수 있습니다 다른 종류의
  • 이있을 것이다 전자 대리인에 의해
  • 주문과 관련된 사람 User (법적 이유로 시스템이 필요하다고 가정)이 간접적으로 주문과 관련 될 수 있습니다 (예 : OrderPlacingParty.

이러한 문제를 해결하려면 전달 된 개체의 구조를 "이해"하는 대신 직접 필요한 모든 내용을 주문 프레젠테이션 로직에 전달해야합니다. 이렇게하면 변경 내용을 지역화 할 수 있습니다 잠재적으로 안정적인 다른 코드로 변경 사항을 전파하지 않고 리팩토링되는 코드에 적용 할 수 있습니다.

interface OrderPresenter { 
    void present(Order order, User user, Address address); 
} 
interface Address { 
    ... 
} 
class PhysicalAddress implements Address { 
    public String getStreetNumber(); 
    public String getCity(); 
    public String getState(); 
    public String getCountry(); 
} 
class ElectronicAddress implements Address { 
    public URL getUrl(); 
} 
interface OrderPlacingParty { 
    Address getAddress(); 
} 
interface Order { 
    OrderPlacingParty getParty(); 
} 
class User implements OrderPlacingParty { 
} 
class Company implements OrderPlacingParty { 
    public User getResponsibleUser(); 
} 
class ElectronicAgent implements OrderPlacingParty { 
    public User getResponsibleUser(); 
} 
+0

감사합니다. 이것은 제가 TarkaDaal의 대답에 대한 제 의견에서 설명하려고했던 문제입니다.하지만 당신은 그것을 능숙하게 표현했습니다. 이것은 확실히 더 나은 해결책입니다! 올바른 방향으로 나를 가리켜 주셔서 감사합니다. –

1

당신은 올바른이야 그리고 당신은 당신이 데이터베이스에이 데이터를 유지하는 방법을 고려하고 시작하면 당신은거야 가능성이 가장 높은 모델 당신의 값이

class Order { 
    User user; 
} 

class User { 
    Address shippingAddress; 
    Address deliveryAddress; 
} 

class Address { 
    String city; 
    ... 
} 

같은 것을 객체 (예 : ORM) 당신에 대해 생각하기 시작 할 공연. 느슨한로드 트레이드 오프와 열렬한로드 트레이드 오프를 생각해보십시오.

+0

감사합니다. 네, 그건 제가 생각한 것입니다. 여기서 명백한 다른 문제는 캡슐화입니다. 주문 객체는 사용자 또는 제품의 구현 세부 사항을 알 수 없어야하지만 응용 프로그램은 어딘가에 만져야합니다. 주로 데이터 구조로 제공되는 객체가 다르게 처리되어야합니까? –

+0

이런 식으로 * 모든 클래스 (데이터 및 기능)에 접근하는 것이 좋습니다. 멋진 컴퓨터 토크에서 이것은 낮은 [Cyclomatic Complexity] (http://en.wikipedia.org/wiki/Cyclomatic_complexity)와 높은 수준의 [Cohesion] (http://en.wikipedia.org/wiki/Cohesion_ % 28computer_science % 29) – Brad

1

일반적으로 말하자면 Demeter의 법칙은 변경 사항을 축소 된 범위에서 유지하는 데 도움이되므로 새로운 요구 사항이나 버그 수정이 시스템 전체로 확산되지 않습니다. 이 방향에서 도움이되는 다른 디자인 가이드 라인이 있습니다 (예 : this article에 나열된 것들. 그렇게 말하면서 Demeter의 법칙 (디자인 패턴과 기타 유사한 것들)은 유용하다고 생각되는 디자인 가이드 라인으로 간주하며, 그렇게한다고 판단되면 깨뜨릴 수 있습니다. 예를 들어 나는 주로 test private methods을 사용하지 않습니다. 주로 fragile tests을 생성하기 때문입니다. 그러나 아주 특별한 경우에는 객체의 구현이 변경되면 특정 테스트가 변경 될 수 있음을 알고 있기 때문에 객체 민간 메소드를 테스트했습니다. 이는 앱에서 매우 중요하다고 생각했기 때문입니다. 물론 이러한 경우에는 신중해야하며 다른 개발자가 왜 그렇게하는지 설명하는 문서를 남겨 두어야합니다. 그러나, 결국, 당신은 좋은 판단 :)을 사용해야합니다.

이제 원래 질문으로 돌아갑니다. 지금까지 내가 쓴 문제점은 메시지 체인을 통해 액세스 할 수있는 객체 그래프의 루트 인 객체에 대한 GUI를 작성하는 것입니다. 이 경우에는 모델의 각 객체에 대한보기 구성 요소를 할당하여 모델을 만든 것과 비슷한 방식으로 GUI를 모듈화합니다. 결과적으로 해당 모델에 대해 HTML을 만드는 방법을 알고있는 OrderView, AddressView 등과 같은 클래스를 갖게됩니다. 그런 다음 해당보기를 작성하여 최종 레이아웃을 만들 수 있습니다 (예 : OrderViewAddressView을 생성 함). 또는 Mediator을 작성하여 모델에 연결하여 최종 레이아웃을 만들 수 있습니다.다음 뷰를

class ShoppingBasket 
{ 
    protected $orders; 
    protected $id; 

    public function getOrders(){...} 
    public function getId(){...} 
} 

class Order 
{ 
    protected $user; 

    public function getUser(){...} 
} 

class User 
{ 
    protected $address; 

    public function getAddress(){...} 
} 

과 : 첫 번째 방법의 예를 들어이 같은 것을 가질 수있다 (필자는, 예를 들면 PHP를 사용하는 것을, 나는 당신이 사용하는 언어를 모르는)

class ShoppingBasketView 
{ 
    protected $basket; 
    protected $orderViews; 

    public function __construct($basket) 
    { 
    $this->basket = $basket; 
    $this->orederViews = array(); 
    foreach ($basket->getOrders() as $order) 
    { 
     $this->orederViews[] = new OrderView($order); 
    } 
    } 

    public function render() 
    { 
    $contents = $this->renderBasketDetails(); 
    $contents .= $this->renderOrders();  
    return $contents; 
    } 

    protected function renderBasketDetails() 
    { 
    //Return the HTML representing the basket details 
    return '<H1>Shopping basket (id=' . $this->basket->getId() .')</H1>'; 
    } 

    protected function renderOrders() 
    { 
    $contents = '<div id="orders">'; 
    foreach ($this->orderViews as $orderView) 
    { 
     $contents .= orderViews->render(); 
    } 
    $contents .= '</div>'; 
    return $contents; 
    } 
} 

class OrderView 
{ 
//The same basic pattern; store your domain model object 
//and create the related sub-views 

    public function render() 
    { 
    $contents = $this->renderOrderDetails(); 
    $contents .= $this->renderSubViews(); 
    return $contents; 
    } 

    protected function renderOrderDetails() 
    { 
    //Return the HTML representing the order details 
    } 

    protected function renderOrders() 
    { 
    //Return the HTML representing the subviews by 
    //forwarding the render() message 
    } 
} 
하고 view.php에서 당신은 같은 것을 할 것 :이 방법은 뷰가 작성 가능 구성 요소로 취급되는 구성 요소 모델을 기반으로

$basket = //Get the basket based on the session credentials 
$view = new ShoppingBasketView($basket); 
echo $view->render(); 

합니다. 이 스키마에서는 객체의 경계를 존중하며 각 뷰에는 단일 책임이 있습니다. (영업 의견에 따라 추가)

편집

난 당신이 바구니 ID, 주문 날짜 및 사용자 이름을 렌더링 할 필요가 파단에와 있다는 견해를 정리하는 방법이 없다고 가정합니다 한 줄. 코멘트에서 말했듯이,이 경우에 대해 "잘 못된"액세스가 잘 문서화 된 단일 장소에서 수행되어이를 인식하지 못하게합니다.

class MixedView 
{ 
    protected $basketId; 
    protected $orderDate; 
    protected $userName; 

    public function __construct($basketId, $orderDate, $userName) 
    { 
    //Set internal state 
    } 


    public function render() 
    { 
    return '<H2>' . $this->userName . "'s basket (" . $this->basketId . ")<H2> " . 
      '<p>Last order placed on: ' . $this->orderDate. '</p>'; 
    } 
} 

class ViewBuilder 
{ 
    protected $basket; 

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

    public function getView() 
    { 
    $basketId = $this->basket->getID(); 
    $orderDate = $this->basket->getLastOrder()->getDate(); 
    $userName = $this->basket->getUser()->getName(); 
    return new MixedView($basketId, $orderDate, $userName); 
    } 
} 

나중에에 도메인 모델을 재 배열하고 ShoppingBasket 클래스가 더 이상 getUser() 메시지를 구현할 수 없습니다 다음, 응용 프로그램에서 하나의 지점을 변경하는 모든 시스템에 걸쳐 그 변화를 피하기 위해이됩니다. 일부 속성에 액세스하는 데 사용되는 체인 경우

HTH는

+0

보기가 참으로 뚜렷하게 구분 될 수있는 좋은 방법입니다. 하지만 "Hi $ name, 마지막 주문은 $ lastOrderDate"또는 데이터 세트가 혼합 된 다른 경우와 같이 사소한 것입니다. 이 경우 고객과 최신 주문이라는 두 세트의 데이터가 필요합니다. 이 접근법에 대한 이점을 볼 수는 있지만 잠재적으로 최소한의 이익을위한 많은 추가 코드입니다.확실히 더 나은 해결책 인 것처럼 보이지만 어딘가에 중간 지점이 있다고 확신합니다. –

+0

이것이 문제라면 (하나의 속성 체인에서 액세스 할 수있는 하나의 속성) 내부적으로 위임하고 결국 Demeter의 법칙에 유연하게 대처할 것입니다. 그래서,'User'는'getName()'과'getLatestOrder()'를 구현하고 당신의 시각에서 그 날짜를 요청할 것입니다. 그게 시스템에있는 일회성 사건이라면 나는 그다지 걱정하지 않을 것입니다. 혼합 된 데이터의 경우, 하위 뷰에서 분해 할 수있는 방법이 없다면, 문서화 된 단일 장소에서 Demeter의 법칙을 깨뜨려야합니다 (예를 들어 답을 편집합니다). –

2

나는 생각한다, 그것은 두 개 (또는 그 이상이) 다른 상황에서 이루어집니다. 예를 들어 프리젠 테이션 모듈에서 주문 개체가 있고 도시의 소유자/사용자 주소 또는 세부 정보 만 표시하려는 경우를 예로들 수 있습니다. 그럴 경우 그렇게하는 것이별로 문제가되지 않는다고 생각합니다. 왜? 액세스 가능성이있는 속성에 비즈니스 로직을 수행하지 않으므로 (잠재적으로) 밀 결합이 발생할 수 있습니다.

그러나 액세스 한 속성에 대해 일부 논리를 수행 할 목적으로 이러한 연결을 사용하면 사물이 달라집니다. 예를 들어, 가지고 있다면,

String city = order.user.address.city; 
... 
order.user.address.city = "New York"; 

이것은 문제가됩니다. 왜냐하면이 논리는 대상 속성 인 도시에 가까운 모듈에서보다 적절하게 수행되어야합니다. 예를 들어, User가 엔터티이고 값 유형을 처리한다고하면, User 객체가 구성 될 때 Address 객체가 처음 생성되거나 그렇지 않으면 User 객체가 생성 될 때와 마찬가지입니다. 그러나 그것이 그것보다 멀리 간다면 더 멀리 갈수록 더 비논리적이고 문제가됩니다. 왜냐하면 소스와 타겟 사이에는 너무 많은 중개자가 관련되어 있기 때문입니다. 당신이 클래스의 "도시"속성에 대한 몇 가지 논리를 수행하는 경우

따라서, 데메테르의 법칙에 따라, order.user.address 같은 체인의 도시 특성에 액세스 OrderAssmebler을 말한다. 도시라면,이 논리를 표적에 가까운 장소/모듈로 옮겨야한다고 생각해야합니다.

0

법칙은 속성/필드에 액세스하지 않고 메서드를 호출하는 것에 관한 것입니다. 나는 기술적으로 속성이 메서드이지만, 논리적으로는 데이터를위한 것임을 알고 있습니다. 따라서 order.user.address.city의 예는 나에게 잘 들립니다.

이 문서는 흥미로운 추가 읽기입니다 : 같은 order.user.address.city을 체인 참조를 사용와 http://haacked.com/archive/2009/07/13/law-of-demeter-dot-counting.aspx

+0

하지만 demeter의 법칙은 캡슐화를 통해 종속성의 사슬을 피하는 것입니다. 의존성이 대체 될 수 있기 때문에 가장 근접한 캡슐화의 API 만 알면된다. 코드를 위반하지 않고 테스트에서 모의 ​​객체를 사용합니다. 예를 들어, 사용자를 모의 사용자 개체로 바꾸면 상황이 깨지거나 달성하기 위해 상당한 양의 모의 설정이 필요합니다. –

+0

틀림없이 뷰는 그런 식으로 테스트되지는 않지만 테스트 목적으로 모의 객체를 실제 뷰에로드하는 것이 유용합니다. 사용자의 API가 변경된 경우 하나의 주소 대신 배달 주소와 배송지 주소를 모두 가지게되면 사실상 "주소"를 종속성으로 나열하지 않는 곳에서 예기치 않게 끊어 질 것입니다. 이는 이상적인 것에서 멀리 떨어져 있습니다. –

+0

@TomB 예, 그 정신은 다음과 같습니다. 데이터 숨기기에 대해. 귀하의 질문은 명시 적으로 데이터 *에 대한 것입니다 *. 보기 (또는 무엇이든)는 아마도 "User", "Address", "City"등에 대한 레이블을 가질 것입니다. 정의에 따르면,이 항목에 대해 * 알고 있어야합니다. 나는이 정보를 가장 간단한 방법으로 얻을 수있는 문제를 보지 못합니다. – TarkaDaal