减少类的复杂性


10

我已经查看了一些答案并在Google上进行了搜索,但是找不到任何有用的东西(即不会产生尴尬的副作用)。

总的来说,我的问题是我有一个对象,需要对它执行长时间的操作。我将其视为一种装配线,例如制造汽车。

我相信这些对象将称为“ 方法对象”

因此,在此示例中的某个点上,我将拥有一个CarWithoutUpholstery,然后需要在其上运行installBackSeat,installFrontSeat,installWoodenInserts(这些操作不会互相干扰,甚至可以并行执行)。这些操作由CarWithoutUpholstery.worker()执行,并产生一个新的对象,该对象将是CarWithUpholstery,然后在其上运行cleanInsides(),verifyNoUpholsteryDefects()等。

单个阶段中的操作已经是独立的,也就是说,我已经在努力应对可以按任何顺序执行的子集(可以按任何顺序安装前排和后排座椅)。

我的逻辑当前使用反射来简化实现。

也就是说,一旦有了CarWithoutUpholstery,该对象就会检查自身是否有名为performSomething()的方法。此时,它执行所有这些方法:

myObject.perform001SomeOperation();
myObject.perform002SomeOtherOperation();
...

同时检查错误和内容。虽然操作的顺序不重要的,我已经在指定情况下,我曾经发现毕竟一些顺序很重要,一个字典顺序。这与YAGNI矛盾,但是它花费很少-简单的sort()-并且可以节省大量的方法重命名(或引入其他执行测试的方法,例如方法数组)的费用。

一个不同的例子

让我们说,除了要制造汽车外,我还必须编辑某人的秘密警察报告,并将其提交给我的邪恶霸主。我的最后一个对象将是ReadyReport。为了构建它,我首先收集基本信息(姓名,姓氏,配偶...)。这是我的A阶段。根据是否有配偶,我可能不得不进入B1或B2阶段,并收集一两个人的性行为数据。这是由对不同的Evil Minions的几个不同的查询所组成,它们控制着夜生活,街拍,性用品商店的销售收据以及不收什么。等等等等。

如果受害者没有家庭,我什至不会进入GetInformationAboutFamily阶段,但是如果我这样做了,那么我是首先瞄准父亲还是母亲还是兄弟姐妹(如果有)就没有关系了。但是,如果我还没有执行FamilyStatusCheck,那我就无法做到这一点,因此它属于早期阶段。

一切都很棒...

  • 如果我需要一些其他操作,我只需要添加一个私有方法,
  • 如果该操作在多个阶段都是通用的,那么我可以让它从超类继承,
  • 操作简单且自成体系。任何其他操作都不需要从任何操作中获得任何价值(在不同阶段执行的操作),
  • 线下的对象不需要执行许多测试,因为如果其创建者对象首先没有验证这些条件,它们甚至可能不存在。即,在将插入物放入仪表板,清洁仪表板并验证仪表板时,我不需要验证仪表板是否确实存在
  • 它便于测试。我可以轻松模拟部分对象并在其上运行任何方法,并且所有操作都是确定性的黑匣子。

...但...

当我在我的方法对象之一中添加最后一个操作时,就会出现问题,这导致整个模块超过了强制性的复杂性指标(“少于N个私有方法”)。

我已经在楼上解决了这个问题,并建议在这种情况下,私有方法的丰富并不是灾难的故事。复杂性存在的,但它的存在,因为操作复杂的,实际上它不是那么复杂-它只是

以邪恶的霸王为例,我的问题是邪恶的霸王(又名不容否认的他)要求了所有饮食信息,我的饮食奴才告诉我,我需要查询餐馆,小厨房,街头小贩,无牌街头小贩,温室所有者等,以及邪恶的(sub)Overlord(众所周知的也不应被拒绝的他)抱怨我在GetDietaryInformation阶段执行了太多查询。

注意:我知道从多个角度来看这根本不是问题(忽略可能的性能问题等)。发生的一切是,一个特定的指标不满意,对此有充分的理由。

什么我想我能做到

除了第一个以外,所有这些选择都是可行的,而且我认为是可以辩护的。

  • 我已经证实自己可以偷偷摸摸,并声明一半的方法protected。但是我会利用测试程序中的弱点,除了在被抓住时证明自己是合理的之外,我也不喜欢这样。另外,这是权宜之计。如果所需操作次数加倍怎么办?不太可能,但是那又如何呢?
  • 我可以将此阶段任意拆分为AnnealedObjectAlpha,AnnealedObjectBravo和AnnealedObjectCharlie,并使每个阶段要执行的操作占三分之一。我的印象是,这实际上增加了复杂性(增加了 N-1个类),除了通过测试之外没有任何好处。我当然可以认为,CarWithFrontSeatsInstalled和CarWithAllSeatsInstall在逻辑上是连续的阶段。Alpha后来要求使用Bravo方法的风险很小,如果我打得很好的话,风险甚至会更低。但是还是。
  • 我可以将一个远程相似的不同操作组合在一起。performAllSeatsInstallation()。这只是权宜之计,并且确实增加了单个操作的复杂性。如果我需要以不同的顺序执行操作A和B,并且将它们打包在E =(A + C)和F(B + D)内,则必须解开E和F并重新整理代码。
  • 我可以使用lambda函数数组,并完全避开检查,但是我发现这很笨拙。但是,这是迄今为止最好的选择。它会摆脱反思。我遇到的两个问题是,可能会要求我重写所有方法对象,不仅是假设对象,CarWithEngineInstalled而且这虽然是很好的工作安全性,但实际上并没有那么大的吸引力。并且代码覆盖率检查器存在lambda 问题(可以解决,但仍然存在)。

所以...

  • 您认为哪个是我最好的选择?
  • 有没有我没有考虑过的更好的方法?(也许我最好打扫卫生,直接问一下是什么?
  • 这种设计是否存在绝望的缺陷,我最好承认失败与否-这种体系结构?这对我的职业生涯不利,但是从长远来看,编写错误的代码会更好吗?
  • 我当前的选择是否真的是“唯一的方式”,我需要努力获得更好的质量指标(和/或仪器)?对于这最后一个选项,我需要参考...我不能只在抱怨的同时对@PHB挥手,这不是您要查找的指标不管我想要多少

3
或者,您可以忽略“超出最大复杂度”警告。
罗伯特·哈维

如果可以的话,我会的。某种程度上,这种衡量标准本身已经获得了神圣的品质。我将继续努力说服PTB接受它们作为有用的工具,但仅仅是一种工具。我可能
仍会

这些操作实际上是在构造对象吗,或者您只是在使用组装线隐喻,因为它共享您提到的特定属性(许多操作具有部分依赖关系)?其意义在于前者表明建造者模式,而后者可能会建议更多的细节填充一些其他模式。
outis

2
指标的专制。指标是有用的指标,但是在忽略为什么使用该指标的同时强制实施指标无济于事。
Jaydee

2
向楼上的人们解释本质复杂性和意外复杂性之间的区别。zh.wikipedia.org/wiki/No_Silver_Bullet如果您确定违反规则的复杂性是必不可少的,那么如果您按照程序员的意愿进行重构。stackexchange.com/ a / 297414/51948您可能会在规则周围溜达并传播消除复杂性。如果将事物封装到阶段不是随意的(阶段与问题有关),并且重新设计可以减轻开发人员维护代码的认知负担,那么这样做是有意义的。
Fuhrmanator

Answers:


15

冗长的操作序列似乎应该是它自己的类,然后它需要其他对象来执行其职责。听起来您已经接近此解决方案。这个漫长的过程可以分为多个步骤或阶段。每个步骤或阶段都可以是自己的类,该类公开一个公共方法。您可能希望每个阶段都实现相同的接口。然后,您的装配线会跟踪阶段列表。

要构建您的汽车装配示例,请想象一下Car该类:

public class Car
{
    public IChassis Chassis { get; private set; }
    public Dashboard { get; private set; }
    public IList<Seat> Seats { get; private set; }

    public Car()
    {
        Seats = new List<Seat>();
    }

    public void AddChassis(IChassis chassis)
    {
        Chassis = chassis;
    }

    public void AddDashboard(Dashboard dashboard)
    {
        Dashboard = dashboard;
    }
}

我要离开了一些组件,如的实现IChassisSeatDashboard为简洁起见。

Car不负责建筑本身。组装线是,所以让我们将其封装在一个类中:

public class CarAssembler
{
    protected List<IAssemblyPhase> Phases { get; private set; }

    public CarAssembler()
    {
        Phases = new List<IAssemblyPhase>()
        {
            new ChassisAssemblyPhase(),
            new DashboardAssemblyPhase(),
            new SeatAssemblyPhase()
        };
    }

    public void Assemble(Car car)
    {
        foreach (IAssemblyPhase phase in Phases)
        {
            phase.Assemble(car);
        }
    }
}

这里的重要区别是CarAssembler具有实现的组装阶段对象的列表IAssemblyPhase。现在,您正在显式地处理公共方法,这意味着可以单独测试每个阶段,并且您的代码质量工具更加快乐,因为您不必在一个类中投入太多。组装过程也非常简单。只需循环各个阶段,然后调用Assemble通行证即可。做完了 该IAssemblyPhase接口也非常简单,只需一个带有Car对象的公共方法即可:

public interface IAssemblyPhase
{
    void Assemble(Car car);
}

现在,我们需要为每个特定阶段实现此接口的具体类:

public class ChassisAssemblyPhase : IAssemblyPhase
{
    public void Assemble(Car car)
    {
        car.AddChassis(new UnibodyChassis());
    }
}

public class DashboardAssemblyPhase : IAssemblyPhase
{
    public void Assemble(Car car)
    {
        Dashboard dashboard = new Dashboard();

        dashboard.AddComponent(new Speedometer());
        dashboard.AddComponent(new FuelGuage());
        dashboard.Trim = DashboardTrimType.Aluminum;

        car.AddDashboard(dashboard);
    }
}

public class SeatAssemblyPhase : IAssemblyPhase
{
    public void Assemble(Car car)
    {
        car.Seats.Add(new Seat());
        car.Seats.Add(new Seat());
        car.Seats.Add(new Seat());
        car.Seats.Add(new Seat());
    }
}

每个阶段都可以非常简单。有些只是一个班轮。有些人可能只是盲目的增加四个座位,而另一个人必须先配置仪表板,然后才能将其添加到汽车中。这个漫长的过程的复杂性分为多个可重用的类别,这些类别易于测试。

即使您说现在的顺序无关紧要,但将来可能会如此。

为了完成这一点,让我们看一下组装汽车的过程:

Car car = new Car();
CarAssembler assemblyLine = new CarAssembler();

assemblyLine.Assemble(car);

您可以将CarAssembler子类化,以组装特定类型的汽车或卡车。每个阶段都是更大整体中的一个单元,因此更易于重用代码。


这种设计是否存在绝望的缺陷,我最好承认失败与否-这种体系结构?这对我的职业生涯不利,但是从长远来看,编写错误的代码会更好吗?

如果确定我的写作需要重写是我职业生涯中的一个污点,那么我现在将作为一名挖沟机工作,您不应该接受我的任何建议。:)

软件工程是学习,应用和重新学习的永恒过程。仅仅因为您决定重写代码并不意味着您将被解雇。如果真是这样,将没有软件工程师。

我当前的选择是否真的是“唯一的方式”,我需要努力获得更好的质量指标(和/或仪器)?

你所概述不是唯一正确的方法,但记住,代码度量承担没有唯一正确的方法。代码指标应被视为警告。他们可能会指出将来的维护问题。它们是准则,而不是法律。当您的代码指标工具指出一些问题时,我将首先调查代码以查看其是否正确实现了SOLID主体。多次重构代码以使其更可靠将满足代码指标工具。有时并非如此。您必须根据具体情况采用这些指标。


1
是的,这比参加一个神职班要好得多。
2015年

这种方法行之有效,但是主要是因为您可以在没有参数的情况下获得car的元素。如果您需要通过它们怎么办?然后,您可以将所有这些信息扔出窗外,然后完全重新考虑该过程,因为您实际上并没有一家拥有150个参数的汽车制造厂,这真是个难题。
安迪

@DavidPacker:即使必须参数化一堆东西,也可以将其封装在传递给“汇编器”对象的构造函数的某种Config类中。在此汽车示例中,可以自定义油漆颜色,内部颜色和材料,装饰套件,引擎等,但您不一定要进行150个自定义。您可能会有十几个左右的位置,这很适合使用options对象。
格雷格·伯格哈特

但是,仅添加配置将无济于for循环的目的,这是一个巨大的耻辱,因为您必须解析配置并将其分配给适当的方法,这样才能为您提供适当的对象。因此,您最终可能会得到与原始设计非常相似的东西。
安迪

我知道了。实际上,我没有一个类和五十个方法,而是有五十个汇编程序精益类和一个编排类。最终结果似乎是相同的,也是性能明智的选择,但是代码布局更加简洁。我喜欢。添加操作会有点尴尬(我将不得不在他们的类中进行设置,并通知协调类;我认为没有这种必要的简便方法),但是我仍然非常喜欢它。请允许我花几天时间探索这种方法。
LSerni 2015年

1

我不知道这对您是否可行,但我正在考虑使用一种面向数据的方法。这个想法是,您捕获规则和约束,操作,依赖项,状态等的(声明性)表达式。然后,您的类和代码将更加通用,围绕这些规则,初始化实例的当前状态,执行操作以执行以下操作:通过使用声明式捕获规则和状态更改来更改状态,而不是直接使用代码。人们可能会使用编程语言,某些DSL工具,规则或逻辑引擎中的数据声明。


实际上,某些操作是以这种方式实现的(作为规则列表)。以汽车为例,在安装一盒保险丝,照明灯和插头时,我实际上并没有使用三种不同的方法,而只使用了一种方法,该方法通常将两针电气物件安装到位,并采用了三个保险丝列表。 ,指示灯和插头。不幸的是,绝大多数其他方法都不容易采用这种方法。
LSerni 2015年

1

这个问题使我想起了依赖关系。您想要一辆具有特定配置的汽车。像格雷格·伯格哈特(Greg Burghardt)一样,我将为每个步骤/项目/…处理对象/类。

每个步骤都声明了它添加到混合中的内容provides(可能是什么)(可以是多件事),也说明了它需要什么。

然后,您可以在某处定义最终所需的配置,并具有查看所需内容的算法,并确定需要执行的步骤/需要添加的部分/需要收集的文档。

依赖性解析算法并不难。最简单的情况是,每个步骤提供一件事,而没有两件事提供同一件事,并且依赖项很简单(在依赖项中没有“或”)。对于更复杂的情况:只需看一下像debian apt之类的工具。

最后,建造您的汽车然后减少到可能的步骤,然后让CarBuilder找出所需的步骤。

但是,重要的是您必须找到一种使CarBuilder知道所有步骤的方法。尝试使用配置文件来执行此操作。添加一个额外的步骤将只需要在配置文件中添加一个即可。如果需要逻辑,请尝试在配置文件中添加DSL。如果所有其他方法均失败,则您将不得不在代码中定义它们。


0

您提到的几件事对我来说很糟糕

“我的逻辑当前使用反射来简化实现”

确实没有任何借口。您是否真的使用方法名称的字符串比较来确定要运行什么?!如果更改顺序,是否必须更改方法的名称?!

“单个阶段的操作已经是独立的”

如果他们确实彼此独立,则表明您的班级承担了不止一项责任。对我来说,您的两个示例似乎都适合于某种单一

MyObject.AddComponent(IComponent component) 

这种方法可以让您将每个运算的逻辑分为自己的一个或多个类。

实际上,我想象它们并不是真正独立的,我想象每个对象都检查对象的状态并对其进行修改,或者至少在开始之前执行某种形式的验证。

在这种情况下,您可以:

  1. 将OOP扔到窗外,并具有对类中公开的数据进行操作的服务,即。

    Service.AddDoorToCar(汽车)

  2. 有一个builder类,该类通过首先组装所需的数据并传递回完整的对象(即)来构造您的对象。

    Car = CarBuilder.WithDoor()。WithDoor()。WithWheels();

(withX逻辑可以再次拆分为子生成器)

  1. 采用功能性方法并将功能/委托传递给修改方法

    Car.Modify((c)=> c.doors ++);


Ewan说:“把OOP扔到窗外,让服务在您的类中公开的数据上运行” ---这不是面向对象的吗?
格雷格·伯格哈特

?? 它不是,这是一个非面向对象的选择
Ewan 2015年

您正在使用对象,这意味着它是“面向对象的”。仅仅因为您不能为代码识别编程模式并不意味着它不是面向对象的。SOLID原则是一个不错的规则。如果您实施了部分或全部SOLID原则,则它是面向对象的。实际上,如果您正在使用对象,而不仅仅是将结构传递给不同的过程或静态方法,那么它是面向对象的。“面向对象的代码”和“良好的代码”之间是有区别的。您可以编写错误的面向对象的代码。
Greg Burghardt

它的“不是OO”,因为您将数据当作结构进行公开,并像过程中那样在一个单独的类中对其进行操作
Ewan

tbh我只添加了开头的注释,以试图阻止不可避免的“那不是OOP!”。评论
Ewan 2015年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.