一门课程可以有多种活动,即培训,考试,项目,书籍,文章和任务。 以下是要求:
以上要求使我创建了两个聚合。
为什么? 可以在没有任何活动的情况下创建课程,但只能在草稿状态下创建。可以为另一组学生安排课程。 可以独立于课程创建活动,然后将其链接到课程。 只能针对给定的学生使用日期范围获取活动。
protected abstract class Activity
{
public Guid Id {get; private set;}
}
protected class Training : Activity
{
..... Addiontal properties
}
protected class Exam : Activity
{
....Addiontal properties and behavior.
public bool AllowGrading => true;
}
.... Other childern of activity..hence more classes.
问题:
[Test]
public void ActivityFactoryShouldCreateCorrectActivityType(){
var activity= ActivityFactory.CreateActivity(activityType:"Training", title:"Training", DueDate: "date".......)
}
问题是,每个子类型可能都想对要正确创建的实体实施不同的不变式。例如,考试活动需要有关评分范围的信息。
如何正确实施它,或者哪种模式更适合这里?
答案 0 :(得分:0)
这是使用C#或Java这样的语言时经常出现的问题之一。这是一个实现问题,而不是建模问题。
问题是您确实具有以下具体概念:Exam
,Training
等。另一方面,您可以为它们得出一个通用概念:Activity
。
在考虑实施之前,我们需要问几个问题。
Exam
,Training
等感兴趣,其中有多少部分对Activity
的通用概念感兴趣?Activities
概念?这将影响您开发系统的方式。比方说,您的系统没有过多使用Activity
的概念,也不会添加更多活动。在这种情况下,我们可以忽略Activity
并仅使用具体概念。这表示创建它们没有问题。
假设您的系统将使用Activity
的概念,并且您需要添加更多类型的活动。
这不会破坏您的系统将了解不同的具体活动类型的事实。它将创建它们,与它们一起使用等。即使您的系统正在使用活动的概念,它仍可能仍需要知道活动的具体类型,以便可以对其进行处理。
当使用OOP语言(例如Java C#)时,这种逻辑显示了我们的思维方式问题。我们接受过开发人员培训。通常人们说铸造不好。您可以通过某种方式定义基类或接口,并让接口实现者的子类定义行为,而系统的其他部分则不应该知道具体类型。
这对于系统的某些部分以及以后的概念都是如此。以Serializer
为例。您可以使用方法ISerializer
定义接口Serialize
。使用串行器的系统可以使用该接口而不必知道具体类型,因为实现ISerializer
接口的每个类都将添加同一interface
的不同实现。
并非每个问题都是这样。有时您的系统需要知道其处理的内容。我可以在这里从JavaScript之类的语言中学到一些东西。您所拥有的是一个非特定的对象,使用该对象只需将其附加属性即可。该对象是其属性定义的对象。
Duck Typing的概念很有趣:“如果它走路像鸭子,却像鸭子一样嘎嘎叫,那肯定是鸭子”
如果您的系统需要使用Exam
,则应该使用它而不是Activity
。如果它有一个Activity
,它应该能够弄清楚它确实是一个Exam
,因为这是需要的。
现在,我们生活在强大的打字世界中,它拥有良好的组成部分。我喜欢强力打字及其能为您带来的好处,但是有些问题更难以处理。
您可以使用具有继承性的类来实现此目的。您还可以使用接口而不是使用类来捕获不同的概念。但是,您的系统将需要进行一些转换,以确定所处理对象的具体类型。如果我们清楚地知道我们拥有不同类型的Activities
这是一个例子:
public enum ActivityType { Exam, Trainig, Project, Task }
public class Activity {
public Guid ID { get; private set; }
public abstract ActivityType Type { get; }
// other stuff
}
public class Exam : Activity {
public override ActivityType Type {
get { return ActivityType.Exam; }
}
// other stuff
}
public class SomeClass {
public void SomeAction(Activity activity) {
if(activity.Type == ActivityType.Exam) {
var examActivity = (Exam)activity;
// do something with examActivity
}
}
}
如果创建活动具有与之相关的逻辑,则可以使用Factory
通过使用其具体类型来创建活动。
public class ExamFactory {
public Exam CreateSummerExam(string name, //other stuff) {
// Enfore constraints
return new Exam(new Guid(), name,....);
}
}
或在具体类型中添加Factory
:
public class Exam : Activity {
public static Exam CreateSummerExam() {
// Enfore constraints
return new Exam();
}
private Exam() { }
}
或者,如果创建这些对象并不复杂,则只需使用公共构造函数即可。
如果您确实想隐藏类以允许自己实现一些自由,请使用接口:
// **Domain.dll**
public enum ActivityType { Exam, Training }
public interface IActivity {
ActivityType Type { get; }
}
public interface IExam : IActivity { }
internal class Exam : IExam { }
public class ActivityFactory {
public IExam CreateExam() { return new Exam(); }
public ITraining CreateTraining() { return new Training(); }
// other concrete activities
}
这样,您不允许客户代码访问类。您可以授予他们访问公共接口的权限,并将其他特定于实现的方法保留在Domain.dll内部。这些概念的客户仍然可以使用强制转换来使用所需的适当类型,但是这次他们将使用接口。
这是一个很好的article。马丁·福勒(Martin Fowler)在其中说:
使用OO程序,您尝试避免询问对象是否为对象。 类型的实例。但是有时候这是合法信息 客户端使用,也许用于GUI显示。记住,问一个 对象(如果它是类型的实例)与 询问它是否是一个类的实例,因为类型 (接口)和类(实现)可以不同。
编辑:
此方法的另一种实现方式是将Activity
视为容器,可以将不同的内容附加到该容器上。这将为您提供一个更灵活的系统。不幸的是,这不会消除切换和检查实体中是否存在各种功能的需要。在某种程度上是可能的,但是根据您的具体情况,您可能需要处理某个外部组件中的Activity
,并且需要进行检查。
例如,您可能要生成一些报告。您可能需要进行一些活动,对其进行处理,然后根据其中存储的某些数据生成一些报告。将组件附加到一个活动不会发生这种情况,因为此操作需要多个活动而不是单个活动。
有很多系统可以做这种事情。以下是一些示例:
计算机游戏使用称为Entity Component System的东西。这些系统是面向数据的,其中Entity
由不同的Components
组成。然后,每个系统都会检查Component
是否附加了Entity
。例如,您有一个Rendering system
,可以使用所有玩家和物品来渲染场景。该系统将检查实体是否已附加3D model component
。如果有它将渲染它。
Flow Based Programming中使用了相同的方法。它也是数据驱动的,您在其中发送由不同属性组成的信息包。这些属性可以是简单的也可以是复杂的。然后,您已连接Processes
,并在彼此之间传递数据。每个Process
都会在IP中搜索特定类型的数据,以检查其是否支持该数据。
Unity还支持使用实体组件系统。但是,它也支持另一种类似的方法,即使用包含行为和逻辑的主动组件,而不是从外部系统处理的被动数据。
Feature based programming。使用可以添加到对象的功能的概念。它用于CAD / CAM系统,银行系统等
当具有需要处理的动态数据时,这是一种很好的使用方法。不幸的是,这并不能消除执行某些if/else
和swich
的需要。如前所述,当您需要处理Activities
的集合时,需要进行一些检查。
请注意,上述系统不会尝试避免这种情况。反之。他们接受这一事实并使用动态数据。在活动上加上类型switching
并没有什么不同。只是他们的方法在进行大量检查时提供了一个更灵活的系统。
如果系统不需要那种动态数据,则可以只使用具体的类,而不是可以存储无数事物的数据对象。这将简化应用程序的某些部分。如果确实需要组成其他对象,则可以使用上述方法之一。
答案 1 :(得分:0)
感谢您抽出宝贵的时间来详细回答问题并提供了深刻的见解。 考虑我们正在开发https://coursera.org网站。系统必须实现两个主要的高层目标。 -教师/课程创建者应该能够创建(安排)课程。从创作者的角度来看,他/她想在课程中添加考试,培训或其他活动。但是他/她将其称为“我将在以下日期安排该课程的考试活动,并具有以下不及格的标准”或“我正在此课程中安排培训活动”。现在,如果我们将IActivity接口方法与ActvityType Enum一起使用,则所有客户端代码都将使用switch或if / else来查看活动的类型,这将流到顶层(即UI或控制器)或消费者阶层,
if(activity.type==exam){ ((Exam)IActivity).DoSomething();}
但是,鉴于没有很好的选择,这看起来可以接受,但是它确实会使您的代码混乱。 -从学生的角度来看,他/她仅对以下内容感兴趣 -向我显示我必须执行的所有活动的列表,但请告诉我它们是什么类型的活动 -一旦我尝试/进行某项活动,他/她也会期望有不同的行为,例如,培训没有附加任何评分,而考试却具有任何评分。 ---考试只允许参加一次。 ---摘要考试等级与完全考试不同。 ---摘要考试允许延迟提交,而考试根本没有该功能。
再次,为了调用IActivity的正确行为,枚举很有用,但它使必须做出决定的所有级别的代码库变得混乱。而且IActivity根本不了解行为考试,并且考试可以是多种类型,因此增加了复杂性,因此另一个枚举可以查看它是哪种考试,因为汇总考试和全面考试仅在评分行为和所有方面有所不同其他都一样。现在,在所有使用者类上使用另一个switch语句或if / else。 *工厂会对此提供帮助,但是我担心工厂会采用不同的方法,因为它将以不同的属性组合处于有效状态(草稿),因此它将变得过于复杂,在工厂中使用不同的方法。 因此,系统既对活动也对具体类型感兴趣,例如考试,培训等,但范围不同。
**额外的复杂性,如果老师想创建一种新的活动类型,该活动说“ Its Path活动,只有在学生接受此培训后才能进行考试”。现在,该学生仍然有兴趣查看所有活动的列表,只是想知道它的类型(作为标签)。
最近,我一直在考虑合成而不是继承,因为只有一种类型,即Activity,并且它具有一组要素集合。每个功能在其自己的类中都包含其自己的行为。以前没有做过,不确定这种方法是否存在甚至是有益的。
再次感谢您提供详细的答案,很想听听您的想法。