class Person {
constructor(firstname, lastname, ssn) {
this._firstname = firstname;
this._lastname = lastname;
this._ssn = ssn;
this._address = null;
this._birthYear = null;
}
get ssn() {
return this._ssn;
}
get firstname() {
return this._firstname;
}
get lastname() {
return this._lastname;
}
get address() {
return this._address;
}
get birthYear() {
return this._birthYear;
}
set birthYear(year) {
this._birthYear = year;
}
set address(addr) {
this._address = addr;
}
toString() {
return `Person(${this._firstname}, ${this._lastname})`;
}
peopleInSameCountry(friends) {
var result = [];
for(let idx in friends) {
var friend = friends[idx];
if(this.address.country === friend.address.country) {
result.push(friend);
}
}
return result;
}
}
class Student extends Person {
constructor(firstname, lastname, ssn, school) {
super(firstname, lastname, ssn);
this._school = school;
}
get school() {
return this._school;
}
studentsInSameCountryAndSchool(friends) {
var closeFriends = super.peopleInSameCountry(friends);
var result = [];
for(let idx in closeFriends) {
var friend = closeFriends[idx];
if(friend.school === this.school) {
result.push(friend);
}
}
return result;
}
}
class Address {
/**
* Construct a new address object
* @param country Country code (required)
* @param state State code
* @param city City name
* @param zip Zip code value object instance
* @param street Street name
*
*/
constructor(country, state = null, city = null, zip = null, street = null) {
this._country = country;
this._state = state;
this._city = city;
this._zip = zip;
this._street = street;
}
get street() {
return this._street;
}
get city() {
return this._city;
}
get state() {
return this._state;
}
get zip() {
return this._zip;
}
get country() {
return this._country;
}
set country(country) {
this._country = country;
return this;
}
};
const s1 = new Student('111-11-1111', 'Haskell', 'Curry', 'Princeton', 1900, new Address('US'));
s1.address = new Address('US');
const s2 = new Student('222-22-2222', 'Barkley', 'Rosser', 'Princeton', 1907, new Address('Greece'));
s2.address = new Address('England');
const s3 = new Student('333-33-3333', 'John', 'von Neumann', 'Princeton', 1903, new Address('Hungary'));
s3.address = new Address('US');
const s4 = new Student('444-44-4444', 'Alonzo', 'Church', 'Princeton', 1903, new Address('US'));
s4.address = new Address('US');
s4.studentsInSameCountryAndSchool([s1,s2,s3]) // [s1, s2]
위의 코드를 바탕으로 분석해보면(나만의 관점)
studentsInSameCountryAndSchool
를 만들기 위해서는 Student 클래스에 정의를 해야하고, 나머지 기능은 다른 자식 클래스에서도 사용하기 위해선 상위 클래스인 Person에 만들게 된다. 이런 설계를 하다보면, 나중엔 특별히 사용하지 않는 메서드를 강제로(?) 상속받게 되는 일도 생긴다. 어쨌든 메서드를 하나 만들 때에도 객체의 데이터 설계에 종속되는 부분이 있다. 또 다른 종속성 측면에서의 나름의 불평(?)을 해보면, 만약에 peopleInSameCountry
내부 로직을 바꿔야 한다고 했을 때, 사이드 이펙트로 studentsInSameCountryAndSchool
도 영향을 받는다. 이건 근데 함수형도 마찬가지긴할텐데, peopleInSameCountry2
같은걸 하나 만들어서 쓰면 된다. 물론 이게 선언형이 아니라는 점은 다르겠지만, 내가 느끼기에 절차형 vs 선언형 이 싸움은 논리적인 경쟁으로는 결판이 안날만한 대결 같다..(물론 나는 선언형에 한표).여기까지 함수형 코드를 보여주지도 않고, 내 관점을 적어봤는데, 그럼 이쯤에서 위의 코드를 함수형으로 만들어보자(객체는 그대로 사용한다).
const selector = (country, school) => (student) => student.address.country === country && student.school === school;
const findStudentsBy = (friends, selector) => friends.filter(selector);
findStudentsBy([s1,s2,s3], selector(s4.address.country, s4.school)); // [s1, s2]
이렇게 보면, 결국 함수형 vs 객체지향은 선언형 vs 절차형.. 이 맞나 싶기도 하다(앞서 말했듯이 나는 선언형 쪽이다..). 어쨌든, 객체지향에서는 각 클래스 내에 메서드가 종속됐고, 클래스 간의 관계에 따라 메서드를 가져다 쓰고 등등 이런 종속성이 확실히 존재했다. 하지만, 함수형으로 만든 함수를 보면, findStudentsBy 같은 경우에는 굳이 이 기능을 위해서만 쓰지 않아도, 다른 함수와 합성 혹은 다른 객체를 위해서 이 함수를 또 사용할 수 있다. 즉, 클래스와 객체 데이터에 종속되는 정도가 상대적으로 낮다. 또한, 재사용성 측면에서도 더 효율적이다.
이에 더해서 함수형 프로그래밍에 익숙해지고, 이 방향으로 사고하다보면, 이제 객체 혹은 데이터라는 것을 고려하는 자세에서 잘게 쪼갠 함수를 어떻게 합성해서 새로운 기능을 만들까 혹은 하나의 함수를 어떻게 더 잘게 쪼개서 재사용하기 좋게할까 등의 고민으로 바뀌게 된다. 나같은 초짜 함수형 프로그래머도 저 로직을보고 lens, reduce 등을 써서 property 조회하는 로직을 또다른 함수로 만들면, selector 자체도 school, country만을 체킹하는 함수에서 좀 더 범용적으로 재사용할 수 있는 함수로 바꿀 수 있지 않을까 라는 생각을 하게 되는데, 이게 그냥 함수형 프로그래밍의 핵심인 것 같다.