I'm creating a new simple app to tokenize and remove stop words within given text. To do so, I've created the files bellow:
// Stopword.js
class StopWord {
constructor (stopWords) {
this.stopWords = stopWords
}
remove (value) {
// do whatever we need to remove this.stopWords from the passed value and return it
}
}
// PreProcess.js
const StopWord = require('./StopWord')
class PreProcess {
setValue (value) {
this.value = value
return this
}
removeStopWords (stopWords) {
const stopWord = new StopWord(stopWords)
return stopWord.remove(this.value)
}
}
// Indexed.js
class Indexer {
setValue (value) {
this.value = value
return this
}
setStopWords (stopWords) {
this.stopWords = stopWords
return this
}
index () {
return this.preprocess
.setValue(this.value)
.removeStopWords(stopWords)
}
}
// main.js
const indexer = new Indexer()
const result = indexer
.setValue('a sample text ')
.setStopWords(['a', 'an'])
.index()
console.log(result)
Suppose, in some cases, we want to load stop words dynamically from database (different stop words for different user). The first question which comes up is in which class do we need to load stop words from database?
It is obvious that class PreProcess
is inject into Indexer using Dependency Injection. Class StopWord
also can be injected using DI but I wondered that is that good enough. The second question is that which classes should be injected into which?
Don't try to make things more complicated as needed :-) From the names you currently have, I wouldn't know how to use the functionality. What is an Indexer? What is a Preprocessor? Those things could be anything. So instead keep it simple and from a domain perspective straight to the point (i'll give you the example in TypeScript so you can see what types are being used where):
First we'll define the stop words domain services (our business domain):
/** we'll use this to provide stop words as array of strings */
class StopWords {
public stopWords: string[] = [];
constructor(words?: string[]) { this.stopWords = words }
}
/** single responsibility of this class: actually remove the stopwords */
class StopWordsRemovalService {
/** inject stopwords through constructor */
constructor(stopWords: StopWords) {}
/** here we do the real removal work */
public removeAllStopWordsFrom(text: string): string { return ''; }
}
Now we can provide stop words from any location (more on the infrastructure side):
/** TypeScript provids interfaces, which you don't have in plain JS. It merely
* defines the class method signatures, which is quite useful in software design */
interface StopWordsProvider {
getStopWords(): StopWords;
}
class DefaultStopWordsProvider implements StopWordsProvider {
getStopWords(): StopWords {
return new StopWords(['a', 'an']);
}
}
class DbStopWordsProvider implements StopWordsProvider {
getStopWords(): StopWords {
return db.query("SELECT stopWords FROM ...");
}
}
And finally we'll wire together:
const swremoval: StopWordsRemovalService = new StopWordsRemovalService(new DefaultStopWordsProvider().getStopWords());
swremoval.removeAllStopWordsFrom('a sample text');
To wire things together, you may now use a dependency injection framework such as InversifyJS
The first question that comes to my mind here is how important is the user ID for the business? In case the user ID is always required to determine the stop words, the user ID is an integral part of our domain! In case the user ID is sometimes required to determine the stop words, it might not be an integral part of our domain. Let's examine the two cases:
If the user ID is important to the domain and always required to determine the stop words, then let's make it part of the contract:
/** TypeScript provids interfaces, which you don't have in plain JS. It merely
* defines the class method signatures, which is quite useful in software design */
interface StopWordsProvider {
/** Returns all the stop words for the given user */
getStopWords(userID: number): StopWords;
}
Now all classes implementing this interface need to respect the user ID.
If the User ID is only required for some lookups, we'll not change the stop words contract (i.e. the interface)! Instead, we'll provide a UserSpecificStopWordsProvider
that does the lookup. To configure the user ID, we'll use the factory pattern:
/** The user-specific stopwords provider is configured through
* constructor-based injection */
class UserSpecificStopWordsProvider implements StopWordsProvider {
constructor(private userId: number) {}
getStopWords(): StopWords {
return db.query("SELECT * FROM sw WHERE userId = :userId", this.userId);
}
}
/** To simplify the usage, we'll provide a factory to do this. */
class UserSpecificStopWordsProviderFactory {
factoryProvider(userId: number): StopWordsProvider {
// potentially inject more dependencies here
return new UserSpecificStopWordsProvider(userId);
}
}
/** We can then use it as follows: */
const factory = new UserSpecificStopWordsProviderFactory();
factory.factoryProvider(5).getStopWords();