如何建模区域和该区域中的一个点?

时间:2016-11-03 13:09:14

标签: oop language-agnostic

我需要为具有Region方法的contains(point)建模。该方法确定点是否落在Region的边界内。

我目前看到Region的两个实现:

  • 区域由开始和结束邮政编码定义的区域。
  • 区域由纬度/经度和半径定义的区域。

现在,我的主要问题是如何为contains()方法定义接口。

可能的解决方案#1:
一个简单的解决方案是让一个点也由一个区域定义:

PostalcodeRegion implements Region

region = new PostalcodeRegion(postalStart: 1000, postalEnd: 2000);
point = new PostalcodeRegion(postalStart: 1234, postalEnd: 1234);
region.contains(point); // true

界面可能如下所示:

Region
+ contains(Region region):bool

问题在于contains()方法并不具体,我们滥用Region会让它不是特定的:Point

可能的解决方案#2:

或者,我们定义一个新的点类:

PostalcodeRegion implements Region {}
PostalcodePoint implements Point {}

region = new PostalcodeRegion(postalStart: 1000, postalEnd: 2000);
point = new PostalcodePoint(postalCode: 1234);
region.contains(point); // true

接口:

Region
+ contains(Point point)

此方法存在以下几个问题:

  • contains()方法仍然不具体
  • 有一个毫无意义的Point概念。它本身就是/什么都不做,它只是一个标记界面。

我想要的一件事,但我不确定是否可能,是不必对point方法中传递的contains()进行运行时检查。最好是通过合同强制执行我获得正确的数据(适合所选的Region实现)来使用。

我大多只是大声思考。我倾向于使用方法#2,并对point实现中传递的contains() var进行运行时类型检查。

我想听到一些关于其中一个或更好的想法:一个我没有想过的新建议。

它应该不是真正相关,但目标平台是PHP。所以我不能以泛型为例。

4 个答案:

答案 0 :(得分:1)

邮政编码和Point是不同的概念性事物,它们是两种不同的类型。邮政编码是一个标量值,点是一个地理项目。实际上,您的PostalCodeRegion类是标量值的范围,您的LatLngRegion类是具有中心坐标和半径的地理区域。您尝试合并两个不兼容的抽象。试图为两个截然不同的事物创建一个接口是错误的方法,从而导致代码不明确和隐式抽象。您应该重新考虑您的抽象。例如:

什么是邮政编码?在最简单的情况下,它是一个正数。您可以将Postcode类创建为value object,并实现简单的方法来处理其数据。

class Postcode
{
    private $number;

    public function __constuct(int $number)
    {
        assert($value <= 0, 'Postcode must be greater than 0');
        $this->number = $number;
    }

    public function getNumber(): int
    {
        return $this->number;
    }

    public function greatOrEqual(Postalcode $value): bool
    {
        return $this->number >= $value->getNumber();
    }

    public function lessOrEqual(Postalcode $value): bool
    {
        return $this->number <= $value->getNumber();
    }
}

什么是邮政编码范围??它是一组包含起始邮政编码和结束邮政编码的邮政编码。因此,您还可以创建一个范围的value object并在其中实现contains方法。

class PostcodeRange
{
    private $start;

    private $end;

    public function __construct(Postcode $start, Postcode $end)
    {
        assert(!$start->lessOrEqual($end));
        $this->start = $start;
        $this->end = $end;
    }

    public function contains(Postcode $value): bool
    {
        return $value->greatOrEqual($this->start) && $value->lessOrEqual($this->end);
    }
}

什么是点?这是具有一定坐标的地理项目。

class Point
{
    private $lan;

    private $lng;

    public function __constuct(float $lan, float $lng)
    {
       $this->lan = $lan;
       $this->lng = $lng;
    }

    public function getLan(): float
    {
       return $this->lan;
    }

    public function getLng(): float
    {
       return $this->lng;
    }
}

什么是区域??这是一个具有某些边界的地理区域。在您的情况下,这些边界用一个具有中心点和一定半径的圆定义。

class Area
{
    private $center;

    private $radius;

    public function __constuct(Point $center, int $radius)
    {
        $this->center = $center;
        $this->radius = $radius;       
    }

    public function contains(Point $point): bool
    {
        // implementation of method
    }  
}

因此,每个公司都有一个邮政编码和由其坐标定义的某些位置。

class Company
{
    private $postcode;

    private $location;

    public function __construct(Postcode $postcode, Point $location)
    {
        $this->postcode = $postcode;
        $this->location = $location;
    }

    public function getPostcode(): Postcode
    {
        return $this->postcode;
    }

    public function getLocation(): Point
    {
        return $this->location;
    }
}

因此,您说的是公司列表,请尝试按邮政编码范围或区域查找。因此,您可以创建可以包含所有公司并可以实施算法以按必要条件进行搜索的公司集合。

class CompanyCollection
{

    private $items;

    public function __constuct(array $items)
    {
        $this->items = $items;
    }

    public function findByPostcodeRange(PostcodeRange $range): CompanyCollection
    {
        $items = array_filter($this->items, function(Company $item) use ($range) {
            return $range->contains($item->getPostcode());
        });

        return new static($items);
    }

    public function findByArea(Area $area): CompanyCollection
    {
        $items = array_filter($this->items, function(Company $item) use ($area) {
            return $area->contains($item->getLocation());
        });       
        return new static($items);
    }
}

用法示例:

$collection = new CompanyCollection([
    new Company(new Postcode(1200), new Point(1, 1)),
    new Company(new Postcode(1201), new Point(2, 2)),
])

$range = new PostcodeRange(new Postcode(1000), new Postcode(2000));
$area = new Area(new Point(0, 0), 50);

// find only by postcode range
$collection->findByPostcodeRange($range);

// find only by area
$collection->findByArea($area);

// find by postcode range and area
$collection->findByPostcodeRange($range)->findByArea($area);

答案 1 :(得分:1)

我认为最好不要在数据对象中包含contains的实现,并为每个contains实现创建一个单独的类。像这样:

class Region
{
    ...
}

class RegionCheckManager
{
    function registerCheckerForPointType(string $pointType, RegionCheckerInterface $checkerImplementation): void
    {
    ...
    }

    function contains(PointInterface $point, Region $region): bool
    {
        return $this->getCheckerForPoint($point)->check($region, $point);
    }

    /** get correct checker for point type **/
    private function getCheckerForPoint(PointInterface $point): RegionCheckerInterface
    {
        ...
    }
}

interface RegionCheckerInterface
{
    public function contains(PointInterface $point): bool;
}

class PostcodeChecker implements RegionCheckerInterface
{
   ...
}

class PointChecker implements RegionCheckerInterface
{
    ...
}

答案 2 :(得分:1)

鉴于Region必须对两个没有共同点的抽象(PointPostcode)进行操作,那么通用接口是一种构造干净的强类型共同点的方法接口,但您应该质疑该抽象是否对建模有用。作为开发人员,很容易迷失过多的抽象,例如也许Region<T>只是Container<T>,依此类推,突然之间,在域的Ubiquitous Language中找不到您使用的概念。

public interface Region<T> {
    public boolean contains(T el);
}

class PostalRegion implements Region<Postcode> {
    public boolean contains(Postcode el) { ... }
}

class GeographicRegion implements Region<Point> {
    public boolean contains(Point el) { ... }
}

这个问题的问题在于,它专注于如何实现特定的设计,而不是解释实际的业务问题,这使得很难判断该解决方案是否合适或哪种替代解决方案是合适的。

如果实现了公共接口,系统将如何利用该公共接口?它会使模型更容易使用吗?

由于我们被迫假设有问题的领域,所以这里是有关开发城市区域系统的虚构场景(我对该领域一无所知,因此示例可能很愚蠢)。

  

在城市分区管理的背景下,我们拥有独特的   由邮政编码范围和   地理区域。我们需要一个可以回答是否   邮政编码和/或一个点包含在特定区域内。

这为我们提供了更多的合作背景,并提出了可以满足需求的模型。

enter image description here

我们可以假设像RegionService这样的应用程序服务可能看起来像这样:

class RegionService {
    IRegionRepository regionRepository;
    ...

    boolean regionContainsPoint(regionId, lat, lng) {
        region = regionRepository.regionOfId(regionId);
        return region.contains(new Point(lat, lng));
    }

    boolean regionContainsPostcode(regionId, postcode) {
        region = regionRepository.regionOfId(regionId);
        return region.contains(new Postcode(postcode));
    }
}

然后,在您有Locator<T>接口或显式PostcodeLocatorPointLocator接口(由{{ 1}}或其他服务,并由Region使用,或者是它们自己的服务。

如果回答问题需要复杂的处理等,那么也许应该从RegionServicePostalRange中提取逻辑。应用ISP将有助于使设计更加灵活。

请务必注意,Interface Segregation Principle (ISP)会照亮domain model,以保护不变量并计算复杂的规则和状态转换,但是查询需求通常可以更好地表达为利用强大的基础架构组件的无状态服务(例如数据库)。

编辑1:

我没有意识到您提到“没有泛型”。仍然将答案留在b / c上,我认为它仍然可以提供良好的建模洞察力,并警告您没有那么有用的抽象。始终考虑客户端将如何使用API​​,因为它有助于确定抽象的有用性。

编辑2(添加说明后):

这里的write side似乎是有用的建模工具。 可以说客户具有维修公司的资格规范...

例如

Specification Pattern

Area

请注意,我并没有真正理解“我希望能够安全地绕过区域和点”的意思,或者至少未能理解为什么这需要一个公共接口,所以也许拟议的设计不会适当。明确制定政策/规则/规范具有多个优点,并且规范模式易于扩展以支持诸如描述公司为何不合格的功能等功能。

例如

class Customer {
    ...
    repairCompanyEligibilitySpec() {
        //The factory method for creating the specification doesn't have to be on the Customer
        postalRange = new PostalRange(this.postalCode - 500, this.postCode + 500);
        postalCodeWithin500Range = new PostalCodeWithinRange(postalRange);
        locationWithin50kmRadius = new LocationWithinRadius(this.location, Distance.ofKm(50));

        return postalCodeWithin500Range.and(locationWithin50kmRadius);
    }
}

//Usage

repairCompanyEligibilitySpec = customer.repairCompanyEligibilitySpec();
companyEligibleForRepair = repairCompanyEligibilitySpec.isSatisfiedBy(company);

最终,规范本身不必实现这些操作。您可以使用enter image description here来将新操作添加到一组规范和/或按逻辑层隔离操作。

答案 3 :(得分:0)

如果我对问题的理解正确,那么您有一些模块M,需要接受一些3个对象:

  • 区域的实现(邮政编码与半径,我们称其为R1与R2)
  • 点的实现(邮政编码vs lat / lng,P1 vs P2)
  • 一些API C来检查该点是否在该区域内

然后将第三个对象应用于第一个对象。

(可能是C是R1或R2,对于问题定义而言并不重要)。

所以要解决这个问题:您可以在R1 + P1或R2 + P2上应用C,但不能在R1 + P2或R2 + P1上应用C。

恐怕以类型安全的方式实现它的唯一方法如下:

  1. C是接口apply()
  2. C1实现C,并且具有R1,P1类型的字段。
  3. C2实现C,并且具有R2,P2类型的字段。
  4. 呼叫者生成C1或C2,并将其传递给M,然后M调用c.apply()

请注意,M甚至看不到点,只有检查器接口C。这是因为P1和P2之间没有C以外的任何人可以使用的共同点。