www.digitalmars.com         C & C++   DMDScript  

digitalmars.D.learn - Is there a way to forward-declare interfaces to avoid module interdependencies?

reply =?ISO-8859-1?Q?Per_=C5ngstr=F6m?= <d-news autark.se> writes:
I hope we can all agree that minimizing module interdependencies is a 
good thing, that a highly interdependent set of modules is generally 
harder to understand and more difficult to test than if the 
module-dependency chain is acyclic.

With that in mind, suppose I have the following C++ code, organized with 
a separation of  interface and implementation, with no circular include 
dependencies:

// IStudent.h
class ITeacher; // forward reference

class IStudent {
public:
	virtual void ask(ITeacher &) = 0;
	virtual void learn(ITeacher &, const char * knowledge) = 0;
};

// ITeacher.h
class IStudent; // forward reference

class ITeacher
{
public:
	virtual void teach(IStudent &) = 0;
};

// Student.h
#include "IStudent.h"
class Student : public IStudent {
public:
	void ask(ITeacher &);
	void learn(ITeacher &, const char * knowledge);
};
// Teacher.h
#include "ITeacher.h"
class Teacher: public ITeacher {
public:
	void teach(IStudent &);
};
// Student.cpp
#include "Student.h"
#include "ITeacher.h"

void Student::ask(ITeacher & teacher)
{
	teacher.teach(*this);
}

void Student::learn(ITeacher &, const char * )
{
}

// Teacher.cpp
#include "Teacher.h"
#include "IStudent.h"

void Teacher::teach(IStudent & student)
{
	student.learn(*this, "knowledge");
}

// main.cpp
#include "Student.h"
#include "Teacher.h"
int main()
{
	Student student;
	Teacher teacher;
	student.ask(teacher);
	return 0;
}

Below is my attempt at porting the code to D2. I hope I'm missing 
something due to my limited experience with D, but it seems D forces me 
to create circular dependencies between modules ITeacher and IStudent, 
since I cannot find a way to forward-declare types external to the 
current module in D like in C/C++.

// IStudent.d
import ITeacher; // would like to forward-declare the interface instead

interface IStudent {
	void ask(ITeacher);
	void learn(ITeacher, string knowledge);
}

// ITeacher.d
import IStudent;

interface ITeacher
{
	void teach(IStudent);
}
// Student.d
import IStudent;

class Student : IStudent {
	void ask(ITeacher teacher)
	{
		teacher.teach(this);
	}
	void learn(ITeacher teacher, string  knowledge)
	{
	}	
}
// Teacher.d
import ITeacher;
import IStudent;

class Teacher: ITeacher {
	void teach(IStudent student)
	{
		student.learn(this, "knowledge");
	}
}

// main.d
import Student;
import Teacher;
void main()
{
	auto student = new Student;
	auto teacher = new Teacher;
	student.ask(teacher);
}

So my question is: Am I missing something, or is this the D way to do it?

Cheers,
-- 
Per Å.
Nov 11 2010
parent reply Jacob Carlborg <doob me.com> writes:
On 2010-11-11 13:10, Per Ångström wrote:
 I hope we can all agree that minimizing module interdependencies is a
 good thing, that a highly interdependent set of modules is generally
 harder to understand and more difficult to test than if the
 module-dependency chain is acyclic.

 With that in mind, suppose I have the following C++ code, organized with
 a separation of interface and implementation, with no circular include
 dependencies:

 // IStudent.h
 class ITeacher; // forward reference

 class IStudent {
 public:
 virtual void ask(ITeacher &) = 0;
 virtual void learn(ITeacher &, const char * knowledge) = 0;
 };

 // ITeacher.h
 class IStudent; // forward reference

 class ITeacher
 {
 public:
 virtual void teach(IStudent &) = 0;
 };

 // Student.h
 #include "IStudent.h"
 class Student : public IStudent {
 public:
 void ask(ITeacher &);
 void learn(ITeacher &, const char * knowledge);
 };
 // Teacher.h
 #include "ITeacher.h"
 class Teacher: public ITeacher {
 public:
 void teach(IStudent &);
 };
 // Student.cpp
 #include "Student.h"
 #include "ITeacher.h"

 void Student::ask(ITeacher & teacher)
 {
 teacher.teach(*this);
 }

 void Student::learn(ITeacher &, const char * )
 {
 }

 // Teacher.cpp
 #include "Teacher.h"
 #include "IStudent.h"

 void Teacher::teach(IStudent & student)
 {
 student.learn(*this, "knowledge");
 }

 // main.cpp
 #include "Student.h"
 #include "Teacher.h"
 int main()
 {
 Student student;
 Teacher teacher;
 student.ask(teacher);
 return 0;
 }

 Below is my attempt at porting the code to D2. I hope I'm missing
 something due to my limited experience with D, but it seems D forces me
 to create circular dependencies between modules ITeacher and IStudent,
 since I cannot find a way to forward-declare types external to the
 current module in D like in C/C++.

 // IStudent.d
 import ITeacher; // would like to forward-declare the interface instead

 interface IStudent {
 void ask(ITeacher);
 void learn(ITeacher, string knowledge);
 }

 // ITeacher.d
 import IStudent;

 interface ITeacher
 {
 void teach(IStudent);
 }
 // Student.d
 import IStudent;

 class Student : IStudent {
 void ask(ITeacher teacher)
 {
 teacher.teach(this);
 }
 void learn(ITeacher teacher, string knowledge)
 {
 }
 }
 // Teacher.d
 import ITeacher;
 import IStudent;

 class Teacher: ITeacher {
 void teach(IStudent student)
 {
 student.learn(this, "knowledge");
 }
 }

 // main.d
 import Student;
 import Teacher;
 void main()
 {
 auto student = new Student;
 auto teacher = new Teacher;
 student.ask(teacher);
 }

 So my question is: Am I missing something, or is this the D way to do it?

 Cheers,

First you have to import ITeacher in Student.d (if I'm reading this right). Second, D has (generally) no problems with circular references. You only get problems when two modules is a part of a circular reference and both have module constructors. If you porting C++ code you will not have this problem since C++ doesn't have module constructors. BTW, you generally don't separate your interface and implementation in D (you can to that if you want to hide your implementation). The import/module system in D is more like the one in Java than the one in C/C++. -- /Jacob Carlborg
Nov 11 2010
next sibling parent reply =?ISO-8859-1?Q?Per_=C5ngstr=F6m?= <d-news autark.se> writes:
On 2010-11-11 15:51, Jacob Carlborg wrote:
 First you have to import ITeacher in Student.d (if I'm reading this
 right).

No, the code works fine as is, but if I move the import of ITeacher from IStudent.d to Student.d, IStudent.d won't compile: IStudent.d(4): Error: identifier 'ITeacher' is not defined IStudent.d(4): Error: ITeacher is used as a type
 Second, D has (generally) no problems with circular references.

No, but I have! ;-) To be honest, I would have wanted D to outright outlaw circular references.
 You only get problems when two modules is a part of a circular reference
 and both have module constructors. If you porting C++ code you will not
 have this problem since C++ doesn't have module constructors.

Actually, I'm more thinking whether D would be a good language for building really big systems. If the language more or less requires many modules to import each other, that's a big drawback in my opinion, since that makes a mess of the dependency graph. It also makes independent testing of individual modules a lot harder.
 BTW, you generally don't separate your interface and implementation in D
 (you can to that if you want to hide your implementation).

I think information hiding is essential for a large-scale design to be comprehensible as a whole. Agreed, the module system in D hides a lot of implementation detail from being included in each compilation unit, but we must also consider not overloading the programmer with needless information. But then again, maybe I just need to stop thinking in C++.
 The import/module system in D is more like the one in Java than the one
 in C/C++.

OK, I'm more familiar with C++. But I really like the concept of importing modules instead of file inclusion. Cheers, -- Per Å.
Nov 11 2010
parent reply =?UTF-8?B?UGVyIMOFbmdzdHLDtm0=?= <d-news autark.se> writes:
On 2010-11-11 17:21, Steven Schveighoffer wrote:
 First, you can't forward-declare classes in one file that are defined in
 another file, instead of importing. The reason is because in D, the
 module is the namespace that the class is declared in.

 So for instance, when you define IStudent in IStudent.d, it's full name
 is IStudent.Istudent

 if you did something like:

 interface IStudent;

 in ITeacher.d, then it's full name would be ITeacher.IStudent, not
 IStudent.IStudent. So it's not the same.

I get it now. So there is no way out for me: I have to import any module whose interfaces I use, even though I'm only referring to their names.
 Second, you are only having fits about forward declarations because you
 aren't used to it :)

Actually, I would say it's the other way around: I am used to them (being able to refer to an unknown identifier as long as I don't use it) in C and C++ and find it lacking in D.
 That being said, D has had many forward reference
 bugs (I believe there are still a few active ones), so it doesn't always
 *just work*. Plus there's the issue of circular imports causing code to
 fail at runtime due to module construction dependencies.

 But in any case, you shouldn't worry about it, just import away, and the
 compiler can take it.

But I still think cutting down on intermodule dependencies is a good thing, for ease of comprehension and unit testing. Fortunately, by separating interface and implementation, one can at least keep the implementation modules free from interdependencies. It's not perfect but it's not a complete mess either. Cheers, -- Per Ã….
Nov 14 2010
parent reply =?UTF-8?B?UGVyIMOFbmdzdHLDtm0=?= <d-news autark.se> writes:
On 2010-11-15 16:52, Steven Schveighoffer wrote:
 On Sun, 14 Nov 2010 05:28:29 -0500, Per Ångström <d-news autark.se> wrote:
 Fortunately, by separating interface and implementation, one can at
 least keep the implementation modules free from interdependencies.
 It's not perfect but it's not a complete mess either.

It is one way to keep dependencies down. And less dependencies means less coupling, meaning importing one module doesn't make you import (and include the code for) many other modules. The compiler isn't yet mature enough to trim out some code that is never used.

I have found a way to detect unwanted interdependencies between implementation modules, by declaring a static constructor in those modules that should not depend on other implementation modules. // Student.d import IStudent; import ITeacher; class Student : IStudent { // ... void ask(ITeacher teacher) { teacher.teach(this); } static this() {} } // Teacher.d import ITeacher; import IStudent; class Teacher: ITeacher { void teach(IStudent student) { student.learn(this, "knowledge"); } static this() {} } Now if Teacher and Student were to accidentally import each other, I would get a runtime error: object.Exception: Cyclic dependency in module Student However, I wish the situation could be detected at compile-time. -- Cheers, Per Ã….
Nov 28 2010
parent =?UTF-8?B?UGVyIMOFbmdzdHLDtm0=?= <d-news autark.se> writes:
On 2010-11-29 16:39, Steven Schveighoffer wrote:
 On Sun, 28 Nov 2010 09:58:42 -0500, Per Ångström <d-news autark.se> wrote:
 However, I wish the situation could be detected at compile-time.

It could only be detected at link-time, and the linker doesn't do any custom processing, it just links.

Actually, I should have written "without running the program", be it compile-time or link-time.
 But in any case, I still don't understand why you want to avoid
 interdependencies. D is supposed to handle them well.

It's something I caught from reading Lakos's _Large Scale C++ Software Design_ back in the 90's. Of course, D's module system alleviates a lot of the problems with interdependencies in C++, but still, highly interdependent systems are harder to test and less easy to change. I don't think D has changed the game completely, making interdependencies largely a non-issue. -- Cheers, Per Ã….
Nov 30 2010
prev sibling next sibling parent "Steven Schveighoffer" <schveiguy yahoo.com> writes:
On Thu, 11 Nov 2010 11:07:42 -0500, Per Ångström <d-news autark.se> wrote:

 On 2010-11-11 15:51, Jacob Carlborg wrote:
 First you have to import ITeacher in Student.d (if I'm reading this
 right).

No, the code works fine as is, but if I move the import of ITeacher from IStudent.d to Student.d, IStudent.d won't compile: IStudent.d(4): Error: identifier 'ITeacher' is not defined IStudent.d(4): Error: ITeacher is used as a type
 Second, D has (generally) no problems with circular references.

No, but I have! ;-) To be honest, I would have wanted D to outright outlaw circular references.

First, you can't forward-declare classes in one file that are defined in another file, instead of importing. The reason is because in D, the module is the namespace that the class is declared in. So for instance, when you define IStudent in IStudent.d, it's full name is IStudent.Istudent if you did something like: interface IStudent; in ITeacher.d, then it's full name would be ITeacher.IStudent, not IStudent.IStudent. So it's not the same. Second, you are only having fits about forward declarations because you aren't used to it :) That being said, D has had many forward reference bugs (I believe there are still a few active ones), so it doesn't always *just work*. Plus there's the issue of circular imports causing code to fail at runtime due to module construction dependencies. But in any case, you shouldn't worry about it, just import away, and the compiler can take it.
 You only get problems when two modules is a part of a circular reference
 and both have module constructors. If you porting C++ code you will not
 have this problem since C++ doesn't have module constructors.

Actually, I'm more thinking whether D would be a good language for building really big systems. If the language more or less requires many modules to import each other, that's a big drawback in my opinion, since that makes a mess of the dependency graph. It also makes independent testing of individual modules a lot harder.

um... Java anyone? I think there's been one or two big systems built with Java, but I might be guessing :)
 BTW, you generally don't separate your interface and implementation in D
 (you can to that if you want to hide your implementation).

I think information hiding is essential for a large-scale design to be comprehensible as a whole. Agreed, the module system in D hides a lot of implementation detail from being included in each compilation unit, but we must also consider not overloading the programmer with needless information. But then again, maybe I just need to stop thinking in C++.

Yes :) -Steve
Nov 11 2010
prev sibling next sibling parent bearophile <bearophileHUGS lycos.com> writes:
Jacob Carlborg:

 The import/module system in D is more like the one in Java than the one 
 in C/C++.

To me the D module system seems closer to the Python one than to the Java one. Bye, bearophile
Nov 11 2010
prev sibling next sibling parent Jonathan M Davis <jmdavisProg gmx.com> writes:
On Thursday, November 11, 2010 09:42:02 bearophile wrote:
 Jacob Carlborg:
 The import/module system in D is more like the one in Java than the one
 in C/C++.

To me the D module system seems closer to the Python one than to the Java one.

It may be - I haven't really used python, so I can't say - but in comparison to C++, the D and Java module systems are almost identical. Sure, there _are_ differences (like how D's modules don't have to have exactly one public class declaration which has the same name as the module), but they're very similar. Python is probably very similar as well but with fewer differences. Regardless however, in comparison to C++, Java's module system is very close to D's. - Jonathan M Davis
Nov 11 2010
prev sibling next sibling parent "Steven Schveighoffer" <schveiguy yahoo.com> writes:
On Sun, 14 Nov 2010 05:28:29 -0500, Per Ångström <d-news autark.se> wrote:

 On 2010-11-11 17:21, Steven Schveighoffer wrote:
 First, you can't forward-declare classes in one file that are defined in
 another file, instead of importing. The reason is because in D, the
 module is the namespace that the class is declared in.

 So for instance, when you define IStudent in IStudent.d, it's full name
 is IStudent.Istudent

 if you did something like:

 interface IStudent;

 in ITeacher.d, then it's full name would be ITeacher.IStudent, not
 IStudent.IStudent. So it's not the same.

I get it now. So there is no way out for me: I have to import any module whose interfaces I use, even though I'm only referring to their names.
 Second, you are only having fits about forward declarations because you
 aren't used to it :)

Actually, I would say it's the other way around: I am used to them (being able to refer to an unknown identifier as long as I don't use it) in C and C++ and find it lacking in D.

I said that wrong, I meant forward *references* not forward *declrations*. That is, referring to things that aren't declared yet.
  > That being said, D has had many forward reference
 bugs (I believe there are still a few active ones), so it doesn't always
 *just work*. Plus there's the issue of circular imports causing code to
 fail at runtime due to module construction dependencies.

 But in any case, you shouldn't worry about it, just import away, and the
 compiler can take it.

But I still think cutting down on intermodule dependencies is a good thing, for ease of comprehension and unit testing. Fortunately, by separating interface and implementation, one can at least keep the implementation modules free from interdependencies. It's not perfect but it's not a complete mess either.

It is one way to keep dependencies down. And less dependencies means less coupling, meaning importing one module doesn't make you import (and include the code for) many other modules. The compiler isn't yet mature enough to trim out some code that is never used. -Steve
Nov 15 2010
prev sibling parent "Steven Schveighoffer" <schveiguy yahoo.com> writes:
On Sun, 28 Nov 2010 09:58:42 -0500, Per Ångström <d-news autark.se> wrote:

 On 2010-11-15 16:52, Steven Schveighoffer wrote:
 On Sun, 14 Nov 2010 05:28:29 -0500, Per Ångström <d-news autark.se>  
 wrote:
 Fortunately, by separating interface and implementation, one can at
 least keep the implementation modules free from interdependencies.
 It's not perfect but it's not a complete mess either.

It is one way to keep dependencies down. And less dependencies means less coupling, meaning importing one module doesn't make you import (and include the code for) many other modules. The compiler isn't yet mature enough to trim out some code that is never used.

I have found a way to detect unwanted interdependencies between implementation modules, by declaring a static constructor in those modules that should not depend on other implementation modules. // Student.d import IStudent; import ITeacher; class Student : IStudent { // ... void ask(ITeacher teacher) { teacher.teach(this); } static this() {} } // Teacher.d import ITeacher; import IStudent; class Teacher: ITeacher { void teach(IStudent student) { student.learn(this, "knowledge"); } static this() {} } Now if Teacher and Student were to accidentally import each other, I would get a runtime error: object.Exception: Cyclic dependency in module Student However, I wish the situation could be detected at compile-time.

It could only be detected at link-time, and the linker doesn't do any custom processing, it just links. But in any case, I still don't understand why you want to avoid interdependencies. D is supposed to handle them well. -Steve
Nov 29 2010